Combine Sort filters

By kenneth, Fri, 06/21/2024 - 15:54

Authored on

Image

Have you noticed how the Drupal view sort filters look like when you expose them?

A view of articles exposing a sort filter

A little parenthesis, if you want to check some articles, browse here: https://www.keboca.com/articles

Getting back on track, I mean, the sorting filter looks decent enough, it works, with a couple of extra clicks, selecting the order (Either ASC or DESC) plus the field to apply the criteria. What if I told you, we can combine them and build them dynamically based on the fields available in the current view?

Goal

We hope to have the options of both dropdowns merged to allow the user to select the field and order at the same time:

  • Authored on, A-Z
  • Authored on, Z-A

 On top of that, we want to do it dynamically to support more fields and different types of sort filter types (string, number, and date).

 

Combine Sort module

To interact with Drupal core views, we need to implement one hook (it needs to be inside the "module_name.views.inc" file) and add a new Views sort plugin, to handle this new feature. 

<?php
/**
 * @file
 * Provide views data that isn't tied to any other module.
 */
/**
 * Implements hook_views_data().
 */
function combine_sort_views_data() {
  $data = [];
  $data['views']['combine_sort'] = [
    'title' => t('Combine Sort'),
    'help' => t('Sort by a combination of fields.'),
    'sort' => [
      'field' => 'combine_sort_order',
      'id' => 'combine_sort',
    ],
  ];
  return $data;
}

It will expose the new sort filter globally for all views on the site:

Form to include a new sort criteria

The new plugin will look as follows:

<?php
namespace Drupal\common\Plugin\views\sort;
use Drupal\Core\Form\FormStateInterface;
use Drupal\views\Plugin\views\sort\SortPluginBase;
/**
 * Defines a combine sort plugin.
 *
 * @ViewsSort("combine_sort")
 */
class CombineSort extends SortPluginBase {
  /**
   * {@inheritdoc}
   */
  public function query() {
    if (!empty($this->options['combine_sort_order']) &&
      is_string($this->options['combine_sort_order'])) {
      $pieces = explode('_', $this->options['combine_sort_order']);
      $order = array_pop($pieces);
      $field_name = implode('_', $pieces);
      if (method_exists($this->query, 'sort')) {
        $this->query->sort($field_name, $order);
      }
      else {
        /** @var \Drupal\views\Plugin\views\field\EntityField $field */
        $field = $this->view->field[$field_name];
        $this->query->addOrderBy($field->table, $field->realField, $order);
      }
    }
  }
  /**
   * {@inheritdoc}
   */
  protected function defineOptions() {
    $options = parent::defineOptions();
    $options['combine_sort_order'] = ['default' => ''];
    $options['sort_string_desc'] = ['default' => ''];
    $options['sort_string_asc'] = ['default' => ''];
    $options['sort_date_desc'] = ['default' => ''];
    $options['sort_date_asc'] = ['default' => ''];
    $options['sort_number_desc'] = ['default' => ''];
    $options['sort_number_asc'] = ['default' => ''];
    return $options;
  }
  /**
   * {@inheritdoc}
   */
  public function buildOptionsForm(&$form, FormStateInterface $form_state) {
    parent::buildOptionsForm($form, $form_state);
    unset($form['order']);
    $form['combine_sort_order'] = [
      '#type' => 'checkboxes',
      '#title' => $this->t('Sort by'),
      '#options' => $this->view->getDisplay()->getFieldLabels(),
      '#default_value' => $this->options['combine_sort_order'],
    ];
    // Textfield for custom sort order: string, date, number.
    $form['order_labels'] = [
      '#markup' => '<h6>' . $this->t('Order labels') . '</h6>',
    ];
    $form['sort_string_desc'] = [
      '#type' => 'textfield',
      '#title' => $this->t('String (DESC)'),
      '#default_value' => $this->options['sort_string_desc'],
    ];
    $form['sort_string_asc'] = [
      '#type' => 'textfield',
      '#title' => $this->t('String (ASC)'),
      '#default_value' => $this->options['sort_string_asc'],
    ];
    $form['sort_date_desc'] = [
      '#type' => 'textfield',
      '#title' => $this->t('Date (DESC)'),
      '#default_value' => $this->options['sort_date_desc'],
    ];
    $form['sort_date_asc'] = [
      '#type' => 'textfield',
      '#title' => $this->t('Date (ASC)'),
      '#default_value' => $this->options['sort_date_asc'],
    ];
    $form['sort_number_desc'] = [
      '#type' => 'textfield',
      '#title' => $this->t('Number (DESC)'),
      '#default_value' => $this->options['sort_number_desc'],
    ];
    $form['sort_number_asc'] = [
      '#type' => 'textfield',
      '#title' => $this->t('Number (ASC)'),
      '#default_value' => $this->options['sort_number_asc'],
    ];
    $form['expose']['#access'] = FALSE;
  }
  /**
   * {@inheritdoc}
   */
  public function buildExposedForm(&$form, FormStateInterface $form_state) {
    unset($this->options['expose']['field_identifier']);
    $options = ['' => '- Sort by -'];
    $fields = $this->view->getHandlers('field');
    $combine_sort_order = array_filter($this->options['combine_sort_order']);
    foreach ($combine_sort_order as $key => $value) {
      if (isset($fields[$key])) {
        [$label_desc, $label_asc] = match (TRUE) {
          (str_contains($fields[$key]['type'], 'date') || str_contains($fields[$key]['type'], 'timestamp')) => [
            $this->options['sort_date_desc'],
            $this->options['sort_date_asc'],
          ],
          str_contains($fields[$key]['type'], 'number') => [
            $this->options['sort_number_desc'],
            $this->options['sort_number_asc'],
          ],
          default => [
            $this->options['sort_string_desc'],
            $this->options['sort_string_asc'],
          ]
        };
        $options["{$key}_desc"] = $fields[$key]['label'] . (empty($label_desc) ? ' (DESC)' : ", {$label_desc}");
        $options["{$key}_asc"] = $fields[$key]['label'] . (empty($label_asc) ? ' (ASC)' : ", {$label_asc}");
      }
    }
    $form['combine_sort_order'] = [
      '#type' => 'select',
      '#title' => $this->t('Sort by'),
      '#options' => $options,
      '#default_value' => isset($form_state->getUserInput()['combine_sort_order']) ?
        $form_state->getUserInput()['combine_sort_order'] : '',
      '#weight' => 10,
    ];
  }
  /**
   * {@inheritdoc}
   */
  public function acceptExposedInput($input) {
    if (!empty($input['combine_sort_order'])) {
      $this->options['combine_sort_order'] = $input['combine_sort_order'];
    }
    return parent::acceptExposedInput($input);
  }
}

Let me briefly explain it:

  • ::query()

We extract the "combine_sort_order" value from the options while the view executes the query itself. Then either we add the criteria directly to the query if the method is available or dynamically add it based on the selected field.

 

  • ::defineOptions()

The plugin needs to have new options to store the values to work with, such as the "combine_sort_order" to have the field name and sort order, and also to have the labels for the different combinations based on the sort type.

 

  • ::buildOptionsForm()

Here, we are exposing the existing fields from the current view, and the labels to append to the field name to describe the order of the given option in the dropdown.

Form configure sort criterionForm configure field
  • ::buildExposedForm()

The form exposed in the view to render the combined sort field is generated here, where we take the label value configured in the field, plus the sort order label based on the configured definition.

Combine sort exposed filter
  • ::acceptExposedInput()

Finally, we catch the value selected after the filter is applied, this is the same value available at the time the view runs the query.

 

Conclusion

This implementation provides an extra in the experience of the final users to the list of content available by using the Drupal core views. The following screenshot shows how the module will be structured:

custom module structure

 

I hope it helps you somehow someday!

Pura Vida!

Comments

Restricted HTML

  • Allowed HTML tags: <a href hreflang> <em> <strong> <cite> <blockquote cite> <code> <ul type> <ol start type> <li> <dl> <dt> <dd> <h2 id> <h3 id> <h4 id> <h5 id> <h6 id>
  • Lines and paragraphs break automatically.
  • Web page addresses and email addresses turn into links automatically.