Have you noticed how the Drupal view sort filters look like when you expose them?
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:
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.
::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.
::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:
I hope it helps you somehow someday!
Pura Vida!
Comments