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.

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:

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!
}

See the ListQueryBuilder docs for more information 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 or TypedBaseListComponent class.

This example assumes you have set up your project to use GraphQL Code Generator with the TypedDocumentNode plugin.

import { ChangeDetectionStrategy, Component } from '@angular/core';
import { TypedBaseListComponent } from '@vendure/admin-ui/core';
import { gql } from 'apollo-angular';

// This is the TypedDocumentNode generated by GraphQL Code Generator
import { GetReviewListDocument } from './generated-types';

const GET_REVIEW_LIST = gql`
  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,
})
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()
    .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.

<vdr-page-header>
  <vdr-page-title></vdr-page-title>
</vdr-page-header>
<vdr-page-body>

  <!-- optional if you want some button 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>
  
  <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 [heading]="'common.id' | translate" [hiddenByDefault]="true">
      <ng-template let-review="item">
        {{ review.id }}
      </ng-template>
    </vdr-dt2-column>
    <vdr-dt2-column
        [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
        [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 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 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 heading="Author" [sort]="sorts.get('authorName')">
      <ng-template let-review="item">{{ review.authorName }}</ng-template>
    </vdr-dt2-column>
  </vdr-data-table-2>

</vdr-page-body>

Route config

For an example of how the route config would look for this list view component, see the full example in the Creating detail views guide.