Paginating Search Results

There are several ways of letting website visitors browse search results, downloads, news articles, or similar items. From the perspective of a website developer, the easiest way to go is to always display all items, which is obviously sufficient if their number is known to be small. Another approach is to offer a “More” button for repeatedly expanding the item list, which is perfect for keeping loading and rendering times short but doesn’t provide any clues about the remaining, not-yet-revealed results.

No doubt, these two approaches aren’t suitable for letting users pick items - one after the other - from a large result set in order to find the ones that best cover what they are after. This is where pagination becomes attractive to visitors as it creates clearness and transparency regarding the pile in front of them.

Let’s get started.

We’re not going to reinvent the wheel but build on a known-to-be-reliable, Bootstrap-compatible third-party solution for the pagination part, react-js-pagination. We’ll combine the “Pagination” component in this package, a search, and a nicely styled results list into a widget for editors to use wherever they want to provide pagination for browsing search results.

Note that ”search results” doesn’t necessarily refer to the outcome of a visitor’s site search. In Scrivito-based apps, the SDK’s built-in search is often used internally for collecting navigation items, creating tag lists, and similar tasks. In this guide, we’ll show you how to find all pages of a specific type and use pagination to render a subset of them. 

Install the pagination package

We are assuming that you have a Scrivito CMS as well as a working Example App. So change to the root of your project directory and execute:

Copy
$ npm install --save react-js-pagination

The react-js-pagination package should now be ready to be used. As indicated, it provides a React component named “Pagination” we’ll make use of in our widget.

Create the “PaginatedListWidget”

Widgets consist of at least two parts, a class definition and a React component. For specifying what editors are presented with when selecting a widget for adding it to a page, or when opening a widget’s properties view, an editing configuration can be provided. First things first, so let’s define the widget class:

src/Widgets/PaginatedListWidget/PaginatedListWidgetClass.js
Copy
import * as Scrivito from 'scrivito';

Scrivito.provideWidgetClass('PaginatedListWidget', {
  attributes: {
    itemsCountPerPage: 'integer',
    pageRangeDisplayed: 'integer',
  },
});

The “PaginatedListWidget” class includes two attributes each of its instances should have, the number of items per result page (itemsCountPerPage), and the maximum number of result pages for which links are generated in the pagination bar (pageRangeDisplayed). The attribute names used here are identical to the corresponding props of the “Pagination” component provided by the react-js-pagination package. In the widget component below, we pass the values of these widget attributes to the “Pagination” component.

But let us first enable editors to alter the values of these attributes by providing an editing configuration for “PaginatedListWidget” instances:

src/Widgets/PaginatedListWidget/PaginatedListWidgetEditingConfig.js
Copy
import * as Scrivito from 'scrivito';
import linkListWidgetIcon from '../../assets/images/link_list_widget.svg';

Scrivito.provideEditingConfig('PaginatedListWidget', {
  title: 'Paginated List',
  description: 'Displays paginated search results',
  thumbnail: `/${linkListWidgetIcon}`,

  properties: [
    'itemsCountPerPage',
    'pageRangeDisplayed',
  ],

  attributes: {
    itemsCountPerPage: {
      title: 'Items per result page',
      description: 'Default: 5',
    },

    pageRangeDisplayed: {
      title: 'Maximum number of linked result pages',
      description: 'Default: 10',
    },
  },

  initialContent: {
    itemsCountPerPage: 5,
    pageRangeDisplayed: 10,
  }
});

As you can see, we’re not only specifying the texts to display for the attributes in the properties dialog, as well as their defaults, but also the widget’s title, description, and thumbnail for the widget selection dialog.

Let’s turn to the part of the widget that actually renders the search results and the pagination, the widget component. In its constructor, it initializes the state variable that holds the currently selected page (subset of items) to be rendered, activePage. Clicking a different page link in the pagination triggers the handlePageChange handler, which updates activePage for having the item subset rerendered, and for making the first item visible again in the browser window (Scrivito.navigateTo) in case the user has scrolled it out of sight.

src/Widgets/PaginatedListWidget/PaginatedListWidgetComponent.js
Copy
import * as React from 'react';
import * as Scrivito from 'scrivito';
import Pagination from 'react-js-pagination';

class PaginatedListWidget extends React.Component {
  constructor(props) {
    super(props);

    this.state = { activePage: 1 };
    this.handlePageChange = this.handlePageChange.bind(this);
  }

  handlePageChange(pageNumber) {
    this.setState({ activePage: pageNumber });
    Scrivito.navigateTo(Scrivito.currentPage(), { hash: 'item-list' });
  }

  render() {
    const itemsCountPerPage = this.props.widget.get('itemsCountPerPage');
    const pageRangeDisplayed = this.props.widget.get('pageRangeDisplayed');

    const query = Scrivito.getClass('Page')
        .all().order('title', 'asc')
        .offset((this.state.activePage - 1) * itemsCountPerPage);

    const totalItemsCount = query.count();

    if (totalItemsCount === 0) {
      return <p>No items found.</p>;
    }

    const items = query.take(itemsCountPerPage);

    return (
      <div>
        <div className='result-content'>
          <p className='strong'>Exactly { totalItemsCount } results.</p>
        </div>

        <div id='item-list'>
          { items.map(item =>
            <div className='result-item' key={ item.id() }>
              <div className='result-content'>
                <p>
                  <Scrivito.LinkTag className='h3' to={ item }>
                    { item.get('title') || 'Untitled' }
                  </Scrivito.LinkTag>
                  <br />
                  <Scrivito.LinkTag to={ item }>
                    { Scrivito.urlFor(item) }
                  </Scrivito.LinkTag>
                </p>
                <p>{ item.get('metaDataDescription') || 'No description' }</p>
              </div>
            </div>
          ) }
        </div>

        <div className='nav'>
          <Pagination
            activePage={ this.state.activePage }
            itemsCountPerPage={ itemsCountPerPage }
            totalItemsCount={ totalItemsCount }
            pageRangeDisplayed={ pageRangeDisplayed }
            itemClass='nav-item'
            linkClass='nav-link'
            activeClass='active'
            onChange={ this.handlePageChange }
          />
        </div>
      </div>
    );
  }
}

Scrivito.provideComponent('PaginatedListWidget', PaginatedListWidget);

The widget component’s render function first performs a search for “Page” objects, orders them by their title, and computes the first one to be displayed (offset), based on the activePage state and itemsCountPerPage attribute value. The result, query, is an ObjSearch which is then used to determine the total number of hits (totalItemsCount) as well as the array of items in the subset (items). These items are then iterated over to display them nicely and link them to the corresponding “Page” objects.

Finally, the “Pagination” component is rendered. We are passing to it all it needs to know to create the correct number of page links and handle the clicks on them. “Pagination” features a lot of other parameters, mainly for fine tuning the CSS classes and texts to use for the navigation bar; see react-js-pagination for details.

What’s next?

If you like the above approach, you could:

  • Add more customization options to the widget, e.g., let editors change texts or the CSS classes via the widget properties.
  • Add a form with an input field for the term to search for and a submit button (“Search”) that causes the widget to be rerendered and the search to be performed.
  • Provide more details about the individual search results, e.g. the creation date of a page, its author, the tags assigned to it, etc.