@vendasta/indeterminatepaging

```ts import { IndeterminatePagingDataSource, IndeterminatePaginatorText } from '@vendasta/indeterminatepaging'; ```

Usage no npm install needed!

<script type="module">
  import vendastaIndeterminatepaging from 'https://cdn.skypack.dev/@vendasta/indeterminatepaging';
</script>

README

IndeterminatePaging

import { IndeterminatePagingDataSource, IndeterminatePaginatorText } from '@vendasta/indeterminatepaging';

Paginating VStore-backed tables is difficult. This DataSource aims to make it a bit less difficult to show a very basic table with pagination. Let's dig into the two pieces that make this whole thing work.

Domain concepts

MatPaginatorIntl

This class can be extended to provide internationalization or, in our case, "many". VStore doesn't currently provide us with a "total results" when doing queries, so to get around this we can have the paginator show "1 to 20 of many" rather than showing nothing after the "1 to 20". This is achieved with the IndeterminatePaginatorText from this package.

DataSource

The @angular/cdk/table provides a DataSource class that has two methods: connect and disconnect. It works with generics and thus we can make our own generic version that can support VStore models. This is achived with the IndeterminatePagingDataSource<T> from this package.

Wiring things up

The first and easy part is the paginator override. Add the following to your providers array in your module (if you already have a providers array, just add the object to it):

providers: [
  {
    provide: MatPaginatorIntl,
    useClass: IndeterminatePaginatorText
  }
]

Now the more complicated piece, the DataSource. You need a class that can be provided to the DataSource that implements the following ListApi<T> interface. Here is what it and its constituents look like:

interface ListApi<T> {
  list(r: PagedListRequestInterface): Observable<PagedResponseInterface<T>>;
}

interface PagedListRequestInterface {
  pagingOptions?: PagedRequestOptionsInterface;
}

interface PagedRequestOptionsInterface {
  cursor?: string;
  pageSize?: number;
}

interface PagedResponseInterface<T> {
  data: T[];
  pagingMetadata: PagedResponseMetadata;
}

interface PagedResponseMetadata {
  nextCursor: string;
  hasMore: boolean;
}

What this boils down to is you need an api that can be provided with a cursor and a pageSize and it must return an object which has a data namespace which is just a list of your entity. It also has the ability to pass along the next cursor and whether or not there are more results than this. Most of our basic VStore list implementations have these things on them, though sometimes not the data namespace. This can be achieved pretty simply with an adapter class written in TypeScript if you don't want to change your API surface to support it.

Once we have a conforming API class we can use it in our DataSource like so:

// In a Service Level component, like in sre-reporting
constructor(
  private slApi: ServiceLevelApiService
) {}

ngOnInit() {
  this.dataSource = new IndeterminatePagingDataSource<ServiceLevelData>(this.slApi);
}

Then in our HTML we can finally get to the meat of what this component does which is working with a mat-paginator + your data table.

<table mat-table [dataSource]="dataSource" matSort class="mat-elevation-z4">
  <!-- Table definition stuff -->
</table>
<mat-paginator
  [pageSizeOptions]="[20, 50, 100]"
  (page)="dataSource.pageChanged($event)"
  [length]="dataSource.totaldataMembers$ | async"
  [disabled]="dataSource.loading$ | async"
  [pageIndex]="dataSource.pageIndex$ | async"></mat-paginator>

Note the different inputs and outputs being fulfilled by the datasource. It will use these inputs to do the following:

  • disabled - while we are loading the next page of results we want to prevent the user from continuing to page, so we disable the paginator buttons while in a loading state
  • length - if length is set to JavaScript's Infinity, the paginator will show "of many". We use this because once we get to the end of the result set we set it to the full amount we know about so that we do get "240 to 252 of 252" at some point!
  • pageIndex - this keeps the paginator in sync with which page of data we're looking at from our backend
  • pageSizeOptions - You can specify these as whatever you like 👍

We now have a table with pagination that can show an indeterminate amount of data.

What doesn't IndeterminatePagingDataSource do?

Well, it doesn't support filtering or sorting your data... yet. That would be pretty simple to add to the ListApi interface, then you'd have to add your own filter bar somewhere and call a funciont on the data source to set your filtering text, then it'd make a new call to your backend. Sorting would be similar in that you could interface with the matSort stuff from Angular Material and pass that along on the ListApi's list call too. Then it'd be a matter of making sure the right column had the right indication of sorting.

Currently there aren't plans to support sorting or filtering on this DataSource. We need a hero to add those features! Or maybe there's just something better than this.

Appendix

Adapter class for non-conforming list implementations

export class ListApiConformer<MyEntity>() {
  constructor(private myListAPI: MyListApi) {}

  list(request: PagedListRequestInterface): Observable<PagedResponseInterface<MyEntity>> {
    // non-conforming name gets adapted
    return this.myListAPI.listEntities({
      // non-conforming request gets adapted
      pageSize: request.pagingOptions.pageSize,
      cursor: request.pagingOptions.cursor,
    }).pipe(
      map(resp => {
        // Non-conforming response gets adapted
        return {
          data: resp.entities,
          pagingMetadata: {
            nextCursor: resp.next,
            hasMore: resp.hasMore,
          }
        };
      })
    );
  }
}

We now have a class that can be passed into our IndeterminatePagingDataSource.