---
title: 'Creating List Views'
---

The two most common type of components you'll be creating in your UI extensions are list components and detail components.

In Vendure, we have standardized the way you write these components so that your ui extensions can be made to fit seamlessly into the rest of the app.

:::note
The specific pattern described here is for Angular-based components. It is also possible to create list views using React components, but 
in that case you won't be able to use the built-in data table & other Angular-specific components.
:::

## Example: Creating a Product Reviews List

Let's say you have a plugin which adds a new entity to the database called `ProductReview`. You want to create a new list view in the Admin UI which displays all the reviews submitted.

### Use the PaginatedList interface

To use the standardized list component, you need to make sure your plugin exposes this list in the GraphQL API following the [PaginatedList interface](/reference/typescript-api/common/paginated-list/):

```graphql
type ProductReview implements Node {
  id: ID!
  createdAt: DateTime!
  updatedAt: DateTime!
  title: String!
  rating: Int!
  text: String!
  authorName: String!
  product: Product!
  productId: ID!  
}

type ProductReviewList implements PaginatedList {
  items: [ProductReview!]!
  totalItems: Int!
}
```

:::info
See the [Paginated Lists guide](/how-to/paginated-list/) for details on how to implement this in your server plugin code.
:::

### Create the list component

The list component itself is an Angular component which extends the [BaseListComponent](/reference/admin-ui-api/list-detail-views/base-list-component/) or [TypedBaseListComponent](/reference/admin-ui-api/list-detail-views/typed-base-list-component) class.

This example assumes you have set up your project to use code generation as described in the [GraphQL code generation guide](/how-to/codegen/#codegen-for-admin-ui-extensions).

```ts title="src/plugins/reviews/ui/components/review-list/review-list.component.ts"
import { ChangeDetectionStrategy, Component } from '@angular/core';
import { TypedBaseListComponent, SharedModule } from '@vendure/admin-ui/core';
// This is the TypedDocumentNode generated by GraphQL Code Generator
import { graphql } from '../../gql';

const getReviewListDocument = graphql(`
  query GetReviewList($options: ReviewListOptions) {
    reviews(options: $options) {
      items {
        id
        createdAt
        updatedAt
        title
        rating
        text
        authorName
        productId
      }
      totalItems
    }
  }
`);

@Component({
    selector: 'review-list',
    templateUrl: './review-list.component.html',
    styleUrls: ['./review-list.component.scss'],
    changeDetection: ChangeDetectionStrategy.OnPush,
    standalone: true,
    imports: [SharedModule],
})
export class ReviewListComponent extends TypedBaseListComponent<typeof getReviewListDocument, 'reviews'> {

    // Here we set up the filters that will be available
    // to use in the data table
    readonly filters = this.createFilterCollection()
        .addIdFilter()
        .addDateFilters()
        .addFilter({
            name: 'title',
            type: {kind: 'text'},
            label: 'Title',
            filterField: 'title',
        })
        .addFilter({
            name: 'rating',
            type: {kind: 'number'},
            label: 'Rating',
            filterField: 'rating',
        })
        .addFilter({
            name: 'authorName',
            type: {kind: 'text'},
            label: 'Author',
            filterField: 'authorName',
        })
        .connectToRoute(this.route);

    // Here we set up the sorting options that will be available
    // to use in the data table
    readonly sorts = this.createSortCollection()
        .defaultSort('createdAt', 'DESC')
        .addSort({name: 'createdAt'})
        .addSort({name: 'updatedAt'})
        .addSort({name: 'title'})
        .addSort({name: 'rating'})
        .addSort({name: 'authorName'})
        .connectToRoute(this.route);

    constructor() {
        super();
        super.configure({
            document: getReviewListDocument,
            getItems: data => data.reviews,
            setVariables: (skip, take) => ({
                options: {
                    skip,
                    take,
                    filter: {
                        title: {
                            contains: this.searchTermControl.value,
                        },
                        ...this.filters.createFilterInput(),
                    },
                    sort: this.sorts.createSortInput(),
                },
            }),
            refreshListOnChanges: [this.filters.valueChanges, this.sorts.valueChanges],
        });
    }
}
```

### Create the template

This is the standard layout for any list view. The main functionality is provided by the [DataTable2Component](/reference/admin-ui-api/components/data-table2component/).

```html title="src/plugins/reviews/ui/components/review-list/review-list.component.html"
<!-- optional if you want some buttons at the top -->
<vdr-page-block>
    <vdr-action-bar>
        <vdr-ab-left></vdr-ab-left>
        <vdr-ab-right>
            <a class="btn btn-primary" *vdrIfPermissions="['CreateReview']" [routerLink]="['./', 'create']">
                <clr-icon shape="plus"></clr-icon>
                Create a review
            </a>
        </vdr-ab-right>
    </vdr-action-bar>
</vdr-page-block>

<!-- The data table -->
<vdr-data-table-2
        id="review-list"
        [items]="items$ | async"
        [itemsPerPage]="itemsPerPage$ | async"
        [totalItems]="totalItems$ | async"
        [currentPage]="currentPage$ | async"
        [filters]="filters"
        (pageChange)="setPageNumber($event)"
        (itemsPerPageChange)="setItemsPerPage($event)"
>
    <!-- optional if you want to support bulk actions -->
    <vdr-bulk-action-menu
            locationId="review-list"
            [hostComponent]="this"
            [selectionManager]="selectionManager"
    />
    
    <!-- Adds a search bar -->
    <vdr-dt2-search
            [searchTermControl]="searchTermControl"
            searchTermPlaceholder="Filter by title"
    />
    
    <!-- Here we define all the available columns -->
    <vdr-dt2-column id="id" [heading]="'common.id' | translate" [hiddenByDefault]="true">
        <ng-template let-review="item">
            {{ review.id }}
        </ng-template>
    </vdr-dt2-column>
    <vdr-dt2-column
            id="created-at"
            [heading]="'common.created-at' | translate"
            [hiddenByDefault]="true"
            [sort]="sorts.get('createdAt')"
    >
        <ng-template let-review="item">
            {{ review.createdAt | localeDate : 'short' }}
        </ng-template>
    </vdr-dt2-column>
    <vdr-dt2-column
            id="updated-at"
            [heading]="'common.updated-at' | translate"
            [hiddenByDefault]="true"
            [sort]="sorts.get('updatedAt')"
    >
        <ng-template let-review="item">
            {{ review.updatedAt | localeDate : 'short' }}
        </ng-template>
    </vdr-dt2-column>
    <vdr-dt2-column id="title" heading="Title" [optional]="false" [sort]="sorts.get('title')">
        <ng-template let-review="item">
            <a class="button-ghost" [routerLink]="['./', review.id]"
            ><span>{{ review.title }}</span>
                <clr-icon shape="arrow right"></clr-icon>
            </a>
        </ng-template>
    </vdr-dt2-column>
    <vdr-dt2-column id="rating" heading="Rating" [sort]="sorts.get('rating')">
        <ng-template let-review="item"><my-star-rating-component [rating]="review.rating"    /></ng-template>
    </vdr-dt2-column>
    <vdr-dt2-column id="author" heading="Author" [sort]="sorts.get('authorName')">
        <ng-template let-review="item">{{ review.authorName }}</ng-template>
    </vdr-dt2-column>
</vdr-data-table-2>
```

### Route config

```ts title="src/plugins/reviews/ui/routes.ts"
import { registerRouteComponent } from '@vendure/admin-ui/core';

import { ReviewListComponent } from './components/review-list/review-list.component';

export default [
    registerRouteComponent({ // [!code highlight]
        path: '', // [!code highlight]
        component: ReviewListComponent, // [!code highlight]
        breadcrumb: 'Product reviews', // [!code highlight]
    }), // [!code highlight]
]
```

## Supporting custom fields

From Vendure v2.2, it is possible for your [custom entities to support custom fields](/developer-guide/database-entity/#supporting-custom-fields).

If you have set up your entity to support custom fields, and you want custom fields to be available in the Admin UI list view,
you need to add the following to your list component:

```ts title="src/plugins/reviews/ui/components/review-list/review-list.component.ts"
@Component({
    selector: 'review-list',
    templateUrl: './review-list.component.html',
    styleUrls: ['./review-list.component.scss'],
    changeDetection: ChangeDetectionStrategy.OnPush,
    standalone: true,
    imports: [SharedModule],
})
export class ReviewListComponent extends TypedBaseListComponent<typeof getReviewListDocument, 'reviews'> {

    customFields = this.getCustomFieldConfig('ProductReview'); // [!code highlight]

    readonly filters = this.createFilterCollection()
        .addIdFilter()
        .addDateFilters()
        .addFilter({
            name: 'title',
            type: {kind: 'text'},
            label: 'Title',
            filterField: 'title',
        })
        .addFilter({
            name: 'rating',
            type: {kind: 'number'},
            label: 'Rating',
            filterField: 'rating',
        })
        .addFilter({
            name: 'authorName',
            type: {kind: 'text'},
            label: 'Author',
            filterField: 'authorName',
        })
        .addCustomFieldFilters(this.customFields) // [!code highlight]
        .connectToRoute(this.route);

    readonly sorts = this.createSortCollection()
        .defaultSort('createdAt', 'DESC')
        .addSort({name: 'createdAt'})
        .addSort({name: 'updatedAt'})
        .addSort({name: 'title'})
        .addSort({name: 'rating'})
        .addSort({name: 'authorName'})
        .addCustomFieldSorts(this.customFields) // [!code highlight]
        .connectToRoute(this.route);
    
    // rest of class omitted for brevity
}
```

and then add the `vdr-dt2-custom-field-column` component to your data table:

```html title="src/plugins/reviews/ui/components/review-list/review-list.component.html"
<vdr-data-table-2
    id="review-list"
    [items]="items$ | async"
    [itemsPerPage]="itemsPerPage$ | async"
    [totalItems]="totalItems$ | async"
    [currentPage]="currentPage$ | async"
    [filters]="filters"
    (pageChange)="setPageNumber($event)"
    (itemsPerPageChange)="setItemsPerPage($event)"
>
    <!-- rest of data table omitted for brevity -->
    <vdr-dt2-custom-field-column <!-- [!code highlight] -->
            *ngFor="let customField of customFields" <!-- [!code highlight] -->
            [customField]="customField" <!-- [!code highlight] -->
            [sorts]="sorts" <!-- [!code highlight] -->
    /> <!-- [!code highlight] -->
</vdr-data-table-2>
```