Terra-core: Create Paginator Component

Created on 12 Apr 2018  Â·  20Comments  Â·  Source: cerner/terra-core

Issue Description

We need to build a common paginator component for Terra.

Issue Type

New Feature

Expected Behavior

Render a paginator component from provided props, in accordance with UX designs.

terra-paginator

All 20 comments

Tech Design (v1)

NOTE: THIS DESIGN IS OUT-OF-DATE PER DISCUSSION AT LAST FRIDAY'S WAR ROOM (04/13/2018). A NEW ONE WILL BE POSTED BELOW.

Proposed Inputs

Forward/Back: URL, start, limit, startParam, limitParam
First/Last: Total count
Paged: Page selected

General: Variant

The basic proposed functionality is the forward/back option. This requires a base URL and the key/values associate with start and limit. I'm taking the params as props because we can't guarantee that all consumers will use start/limit for their query params.

The first/last functionality is dependent on knowing how many pages exist, which can be calculate with the limit and total count.

Listing the page numbers is also dependent on knowing how many pages exist, but additionally needs to know currently selected page to build the relative linking and selected-page-display.

The variant will allow the consumer to flex between the Search Paginator and the Progressive Paginator. The progressive paginator will additionally be dependent on selected page as a "required" prop.

Proposed API

<Paginator
    baseUri={base_uri_for_pagination}
    startParam={some_start_param}
    limitParam={some_limit_param}
    start={some_start}
    limit={some_limit}
    page={selected_page} // Optional
    totalCount={totalCount} // Optional
    // onPageChange is intended to support optional Ajax loading of content on pagination.
    onPageChange={callbackToLoadContentToContainer()} // Optional
    variant={‘search’ || ‘progressive’} // Hate these names. Don’t have better ones. Optional. Defaults ‘search’.
/>

Proposed URL Builder Utility

var isPaged = (totalCount, page) => (!(typeof(totalCount) === 'undefined' || typeof(page) === 'undefined'));

var getPageCount = (limit, totalCount) => (Math.ceil(totalCount / limit));

// May redesign into a more complex function to account for more situations and be a little more readable.
// Ex: Setting previous to undefined if previous would result in a negative page.
var urlBuilder = (baseUrl, startParam, limitParam, start, limit, totalCount = undefined, page = undefined) => (
  {
    href: baseUrl,
    next: `${baseUrl}?${startParam}=${start + limit}&${limitParam}=${limit}` + (page ? `page=${page + 1}` : ''),
    previous: `${baseUrl}?${startParam}=${start - limit}&${limitParam}=${limit}` + (page ? `page=${page - 1}` : ''),
    first: totalCount ? `${baseUrl}?${startParam}=0&${limitParam}=${limit}&page=1` : undefined,
    last: totalCount ? `${baseUrl}?${startParam}=${getPageCount(limit, totalCount) - limit}&${limitParam}=${limit}&page=${getPageCount(limit, totalCount)}` : undefined,
    // The 'page' property below may be pulled out and built in the main component, or I might parse and modify as-needed.
    page: isPaged(totalCount, page) ? `${baseUrl}?${startParam}=${start}&${limitParam}=${limit}&page=${page}` : undefined,
  }
);

export default urlBuilder;

Let's rename the page prop to currentPage so that it's more clear as to it's intent.

@neilpfeiffer Some of our old pagination used show more style paginators that didn't know how many pages existed. Do we need to account for scenarios when we are unsure of how many total pages there are?

How about themability variables? We have posted them on several tech designs.

Also, should we do these as two different paginators instead of using a prop to switch between the two?

@neilpfeiffer Some of our old pagination used show more style paginators that didn't know how many pages existed. Do we need to account for scenarios when we are unsure of how many total pages there are?

The various components (like page numbers) are listed as conditional at the top of the basic implementation. I took this to mean that the component should flex based on what information is provided.

Also, should we do these as two different paginators instead of using a prop to switch between the two?

Is there a technical/UX/whatever reason you think that splitting them into separate top-level components is better than just providing a variant that achieves basically the same thing?

If props or other aspects change for the API on one paginator but not on the other, it's easier to adjust for. Otherwise, we could end up having a large paginator component that has a large set of props for which only half are used at a given time.

Is that a realistic possibility, though? Sure, the design could change, but that is true for literally all of our components. Having said that, I know the updates to button caused a bunch of headaches for everyone involved.

@bjankord: Thoughts?

The above tech design is out-of-date per discussion last week. Rather than providing a mechanism to build and manage URLs in-component, we will require the user to pass in a callback function that has the Ajax/page-reload logic on their end.

A new formal design will be forthcoming.

Tech Design (v2)

Proposed Inputs

  • __onPageChange__ (Required): A callback function that will be called to let the application know how to handle the change in pagination state.
  • __selectedPage__ (Optional): The currently selected or starting page for the paginator. Not including this will result in numbered pagination not displaying.
  • __totalCount__ (Optional): The total number of items being paginated. Not including this will result in numbered pagination not displaying.
  • __itemCountPerPage__ (Optional): The count of items displayed per page. Used to calculate the number of pages. Not including this will result in numbered pagination not displaying.

Toggling between Search and Progressive pagination will be done by explicitly importing the desired paginator.

Proposed API

<Paginator 
  onPageChange={my_callback_function}
  selectedPage={selected_page}
  totalCount={total_count}
  itemCountPerPage={number_of_items_per_page}
/>

Example Callback

const handlePageChange = (event, selectedPage) => {
  switch (selectedPage) {
    case typeof(parseInt(selectedPage)) === 'number':
      window.location.href = "http://www.terra-ui.com";
    case 'previous':
      // Set window.location to previous.
    case 'next':
      // Set window.location to next.
    case 'first':
      // Set window.location to first.
    case 'last':
      // Set window.location to last.
    default:
      return null;
  }
}

If your goal is to use async calls to reload content, just replace the window.location code with whatever async call you need.

Proposed Callback Handling

Disclaimer: Pseudocode

Paginator = (props) => {

  this.handleOnChange = (event, selectedPage) => {
    if (this.props.onPageChange) {
      this.props.onPageChange(event, selectedPage)I
    }

    if (this.props.selectedPage && this.props.selectedPage !== selectedPage) {
      this.setState({ selectedPage: selectedPage })
    }
  }

  render() {
    <div className={cx('paginator')}>{...}</div>
  }

}

Theme Support

Theme support will be provided to customize the following attributes:

  • Button foreground color
  • Button background color
  • Button border(-radius, -color)
  • Control alignment/positioning

+1 on tech design

+1 on tech design

+1 on tech design

+1 on tech design

Get get the page number to display, will the selectedPage, totalCount, and itemCountPerPage all need to be provided? Also, will itemCountPerPage have a default?

I say for now that we make all those props required. As for default itemCountPerPage, I think different teams across Cerner had different values for that, so I wouldn't default that for now.

They shouldn't be required because the design accounts for showing fewer UI elements (just next/previous) when page number isn't known.

I could see having totalCount and itemCountPerPage required, but not 100% sold on it.

Sorry. I was vague. I meant requiring them all in order to get the display. I'm fine with leaving them as optional with the descriptions you provided.

Going with the v2 design for my PR.

JIRA created

Was this page helpful?
0 / 5 - 0 ratings