Pagination Component

Can’t we have a built pagination component? The ones available on emberobserver.com are either outdated or support old versions of bootstrap.

What all do you expect a pagination component to do? pagination itself always needs to be handled by you, the person who knows the remote APIs and what query params are needed.

For UI, if you want next/prev/go-to pages, there could be “headless” versions (bring your own styles), with some configuration. Depending on your requirements, we can type out a prototype component here in this thread.

1 Like

How would paginate data from such API call

import Route from '@ember/routing/route';
import fetch from 'fetch';

export default class SuppliersRoute extends Route {
    model() {
        return fetch('http://localhost:8000/api/suppliers')
        .then((response) => response.json())
        .then(function(data){
            console.log(data);
            return data;
        });
    }
}

template

<h3>Suppliers</h3>
<table class="table table-hover">
    <tr>
        <th>Name</th>
        <th>Phone</th>
        <th>Email</th>
        <th>Location</th>
    </tr>
   
    {{#each @model as |supplier|}}
     <tr>
        <td>{{supplier.name}}</td>
        <td>{{supplier.phone}}</td>
        <td>{{supplier.email}}</td>
        <td>{{supplier.location}}</td>
    </tr>
    {{/each}}
</table>

I tried following examples in these tutorials but I could figure out how to break data into pages:

In Angular this how I would do it using ngx-pagination component

<tr *ngFor="let product of products| paginate: { itemsPerPage: 5, currentPage: p, totalItems: total }">
          <td>{{ product.id }}</td>
          <td>{{ product.name }}</td>
          <td>{{ product.description }}</td>
          <td>
            <a [routerLink]="['/edit', product.id]" class="btn btn-info" role="button">Edit</a>
            &nbsp;
            <button type="button" (click)="deleteProduct(product.id)" class="btn btn-danger">Delete</button>
          </td>
        </tr>
      </tbody>
    </table>
    <pagination-controls (pageChange)="pageChangeEvent($event)"></pagination-controls>

**Disclaimer: ** Am not so good in JavaScript

How would paginate data from such API call

I’d use query params, and ditch the route (this is controversial tho).

To set this up, you’d want to add a controller (I usually use the application route, as I don’t like “allow listing” query params for each controller).

import Controller from '@ember/controller';

/**
 * See RFCs
 *  - https://github.com/emberjs/rfcs/pull/712/
 *  - https://github.com/emberjs/rfcs/pull/715/
 *
 *  After 715, this file may be deleted
 */
export default class ApplicationController extends Controller {
  queryParams = [
    // Pagination
    'page',
    'pageSize',
  ];
}

And then, in a component, I’d inject the router service.

import Component from '@glimmer/component';
import { service } from '@ember/service';

export default class Demo extends Component {
  @service router;

  get queryParams() {
    return this.router.currentRoute?.queryParams || {};
  }
}

And then to handle data loading, I’d use a Resource, such as what is provided by ember-resources. More info here.

import Component from '@glimmer/component';
import { service } from '@ember/service';

import { RemoteData } from 'ember-resources/util/remote-data';
import { use } from 'ember-resources';

export default class Demo extends Component {
   // ... code from above omitted for brevity

  @use request = RemoteData(() => {
    let { page, pageSize } = this.queryParams;
    return `http://localhost:8000/api/suppliers?page=${page}&pageSize=${pageSize}`;
  });

  get suppliers() {
    return this.request.value || [];
  }
}
{{#if this.request.isPending}}
  loading
{{else}}
  Render the suppliers
{{/if}}

This is rendered outside the if/else block, 
because we don't want to flash the UI while data is loading
<Pagination />

To read more about how RemoteData works, see the docs and the overview of function-based resources which implements RemoteData.

return `http://localhost:8000/api/suppliers?page=${page}&pageSize=${pageSize}`;

This is the most important part of how pagination is handled with remote APIs. you must specify which page, and how big the page is to get, so that the backend knows where in the dataset to load and return to you.

Then, to add a “pagination component”, you don’t even need the data / request, as all the information you need is in the router service.

import Component from '@glimmer/component';
import { service } from '@ember/service';

export default class Pagination extends Component {
  @service router;

  get queryParams() {
    return this.router.currentRoute?.queryParams || {};
  }

  get pages() {
    let { page } = this.queryParams;
    // note that query params are strings.
    let pageNumber = parseInt(page, 10) ?? 0;

    // generate a list of numbers +/-5 around the current page
    return Array.fill(10).map((_, i) => i + pageNumber - 5).filter(page => page > 0);
  }

  goTo = (page) => this.router.transitionTo({ queryParams: { page } });
  hrefFor = (page) => {
    let url = this.router.currentURL
    let path = url.split('?');
    return `${url}?page=${page}`;
  }
}
<a href={{this.hrefFor 0}} {{on 'click' (fn this.goTo 0)}}>First</a>

{{#each this.pages as |page|}}
  <a href={{this.hrefFor page}} {{on 'click' (fn this.goTo page)}}>page</a>
{{/each}}

Maybe, by API convention "-1" means "Last"
<a href={{this.hrefFor -1}} {{on 'click' (fn this.goTo -1)}}>Last</a>

In Angular this how I would do it using ngx-pagination component

looks like that assumes things about how your API works. This only works in very narrow scenarios. Folks can have different endpoints, different query param names, etc.


Hope this helps! I didn’t test or run any of this code, so your mileage may vary

3 Likes

Thanks I’ll try this out. In case I get stuck I’ll get in touch.