Drupal Planet

Creating Views Pager Plugins in Drupal 8

Submitted by djevans on Tue, 08/01/2017 - 18:55
Paragraphs

If you have a View with a pager in Drupal 8, you might need to display different numbers of results depending on the current page number. Here’s how to achieve that using Views plugins.

The Plugin Hierarchy

Just like in Drupal 7, Views plugins are a hierarchy of classes, with some differences in structure so that they conform with the other plugin types introduced in Drupal 8.

We can see that there are two pagers that display results on multiple pages: ‘Default pager’ (Drupal\views\Plugin\views\pager\Full) and ‘Mini pager’ (Drupal\views\Plugin\views\pager\Mini). We’ll extend the Default pager first.

Making the plugin visible

To make our plugin visible to the UI, we set properties on the class annotations. Set a unique ID for the plugin as the id key and descriptive information in title, short_title and help

/**
 * @ViewsPager(
 *  id = "first_page_full_pager",
 *  title = @Translation("Paged output, separate count on first page"),
 *  short_title = @Translation("Full pager with separate first page"),
 *  help = @Translation("Paged output with separate count on the first page"),
 *  theme = "pager",
 *  register_theme = FALSE
 * )
 */

 

Creating Custom Behaviour

To allow administrators to specify the number of items to show on the first page, we need to define an option for the pager:

public function defineOptions() {
  $options = parent::defineOptions();
  $options['items_first_page'] = ['default' => 10];
  return $options;
}

Once we have done that, we create a field on the settings form.

public function buildOptionsForm(&$form, FormStateInterface $form_state) {
  parent::buildOptionsForm($form, $form_state);
  $form['items_per_page']['#weight'] = -50;
  $form['items_first_page'] = [
    '#title' => t('Items on first page'),
    '#type' => 'number',
    '#description' => t('The number of items to show on the first page'),
    '#default_value' => $this->options['items_first_page'],
    '#weight' => -49,
  ];
}

getItemsPerPage() returns the number of results to display on a given page, and getPagerTotal() returns the total number of page links that should be displayed for a given number of results.

In both cases, we need to take the items_per_page and items_first_page options into account.

public function getItemsPerPage() {
  $key = $this->getCurrentPage() === 0
    ? 'items_first_page'
    : 'items_per_page';
  return isset($this->options[$key]) ? $this->options['key'] : 0;
}

public function getPagerTotal() {
  $first_page_items = $this->options['items_first_page'];
  $items_per_page = $this->options['items_per_page'];
  return $this->total_items > $first_page_items
    ? ceil(1 + (($this->total_items - $first_page_items) / $items_per_page))
    : 1;
}

We override SqlBase::query() in order to set the correct limit and offset:

public function query() {
  parent::query();
  $this->view->query->setLimit($this->getItemsPerPage());
  if ($this->current_page > 0) {
    $offset = $this->options['items_first_page'];
    $offset += ($this->current_page - 1) * $this->options['items_per_page'];
    $offset += $this->options['offset'];
    $this->view->query->setOffset($offset);
  }
}

Finally, we need to set a summary for the pager, to appear in the Views UI.

public function summaryTitle() {
  if (empty($this->options['items_first_page'])) {
    return parent::summaryTitle();
  }
  if (!empty($this->options['offset'])) {
    return $this->formatPlural($this->options['items_per_page'],
      '@count item, skip @skip, @first on first page',
      'Paged, @count items, skip @skip, @first on first page',
      [
        '@count' => $this->options['items_per_page'],
        '@skip' => $this->options['offset'],
        '@first' => $this->options['items_first_page'],
      ]
    );
  }
  return $this->formatPlural($this->options['items_per_page'],
    '@count item, @first on first page',
    'Paged, @count items, @first on first page',
    [
      '@count' => $this->options['items_per_page'],
      '@first' => $this->options['items_first_page'],
    ]
  );
}

 

Refactoring

To extend the mini pager, it looks like we need to extend the Mini class in pretty much the same way. In order to avoid duplicating the code we have just written, we can factor out the common methods into a trait. This way, our new plugins can share functionality while extending different superclasses.

(There's a good explanation of traits in PHP on Brendan Bates' site: Traits, The Right Way.)

Our individual plugin classes, FullFirstPage and MiniFirstPage will then both use the new FirstPageTrait, but have different implementations of summaryTitle(), as well as different metadata in their annotations.

Here's the implementation of summaryTitle() for the mini pager:

public function summaryTitle() {
  if (empty($this->options['items_first_page'])) {
    return parent::summaryTitle();
  }
  if (!empty($this->options['offset'])) {
    return $this->formatPlural($this->options['items_per_page'],
      'Mini pager, @count item, skip @skip, @first on first page',
      'Mini pager, @count items, skip @skip, @first on first page',
      [
        '@count' => $this->options['items_per_page'],
        '@skip' => $this->options['offset'],
        '@first' => $this->options['items_first_page'],
      ]
    );
  }
  return $this->formatPlural($this->options['items_per_page'],
    'Mini pager, @count item, @first on first page',
    'Mini pager, @count items, @first on first page',
    [
      '@count' => $this->options['items_per_page'],
      '@first' => $this->options['items_first_page'],
    ]
  );
}

 

The Views First Page module is now available on drupal.org.