Создаём кнопки «предыдущий» и «следующий» в Drupal 8: туториал научит вас делать навигационные кнопки для перехода между материалами.
Кнопки на предыдущий и следующий материалы достаточно популярны в наши дни, особенно на сайтах с упором на контент. Периодически такие вопросы всплывают в друпал чатиках, а иногда просят и клиенты.
Давайте пробежимся по очень простой реализации и сделаем кнопки на предыдущий и следующий материал. Также данный материал будет как пример использования hook_theme() и псевдо-полей вместе. Далее по тексту подразумевается что код пишется в модуле dummy.
Первым делом нам потребуется сделать свой theme hook с
использованием hook_theme()
, который будет генерировать разметку для
предыдущего и следующего материала. В нём и будет вся логика, как и 90% всего
материала. Для начала регистрируем наш theme hook, назовем
его dummy_previous_next
:
/**
* Implements hook_theme().
*/
function dummy_theme($existing, $type, $theme, $path) {
$file = 'dummy.theme.inc';
return [
'dummy_previous_next' => [
'variables' => [
'entity' => NULL,
],
'file' => $file,
],
];
}
Итак, наш theme hook dummy_previous_next
объявлен. Мы указали что нам будет
требоваться переменная entity
. В неё мы будем передавать объект сущности для
которой нужно сделать кнопки. Наш код будет максимально универсальным и не
привязан к какой-либо сущности или материалу, вы сможете пользоваться им в любом
месте. И для того чтобы оно так работало, нам нужно написать логику. А логика
для theme hook пишется в обработчиках (preprocess). Препроцессор мы вынесем в
файл dummy.theme.inc, который мы указываем в массиве объявляющий наш theme
hook.
? Напоминаю что препроцессоры для собственных theme hook имеют
вид template_preprocess_HOOK()
. Объявляем такой препроцессор в файле *
dummy.theme.inc*:
<?php
/**
* @file
* Contains preprocess functions for custom theme hooks.
*/
/**
* Implements template_preprocess_HOOK().
*/
function template_preprocess_dummy_previous_next(&$variables) {
/** @var \Drupal\Core\Entity\EntityInterface $entity */
$entity = $variables['entity'];
$variables['next'] = [];
$variables['previous'] = [];
// Trying to find next entity ID.
$next_entity_id = \Drupal::entityQuery($entity->getEntityTypeId())
->condition('type', $entity->bundle())
->condition('created', $entity->getCreatedTime(), '>')
->range(0, 1)
->sort('created', 'ASC')
->execute();
if (!empty($next_entity_id)) {
$next_entity = \Drupal::entityTypeManager()
->getStorage($entity->getEntityTypeId())
->load(reset($next_entity_id));
$variables['next']['label'] = $next_entity->label();
$variables['next']['url'] = $next_entity->toUrl()->toString(TRUE)->getGeneratedUrl();
}
// Trying to find previous entity ID.
$previous_entity_id = \Drupal::entityQuery($entity->getEntityTypeId())
->condition('type', $entity->bundle())
->condition('created', $entity->getCreatedTime(), '<')
->range(0, 1)
->sort('created', 'DESC')
->execute();
if (!empty($previous_entity_id)) {
$previous_entity = \Drupal::entityTypeManager()
->getStorage($entity->getEntityTypeId())
->load(reset($previous_entity_id));
$variables['previous']['label'] = $previous_entity->label();
$variables['previous']['url'] = $previous_entity->toUrl()->toString(TRUE)->getGeneratedUrl();
}
}
А теперь пройдемся по порядку:
- Обратите внимание, что вместо
HOOK
в название функции мы подставили название нашего theme hook, который мы хотим обрабатывать. - Первым делом в обработчике мы записываем переменную переданную в наш theme
hook в локальную переменную
$entity
, для удобства обращения. - Далее мы задаем две новые переменные для нашего будущего темплейта
кнопок:
next
иprevious
. Это будут массивы с данными, по умолчанию пустые. - Затем, мы пытаемся получить ID следующего материала. Это получается следующим
образом — мы выполняем запрос в пределах типа
сущности (
$entity->getEntityTypeId()
) которая была передана нашему theme hook. Также мы добавляем условие чтобы подтип (bundle$entity->bundle()
) сущности был аналогичный тому, что у текущей сущности. Это значит, если вы передадите сущностьnode
с подтипомnews
, он будет искать материал в пределах новостей, и если у вас есть ещёpage
иblog
, они не будут учитываться. Затем, указываем что дата создания материала должна быть больше текущей — так мы определяем что материал следующий, так как опубликован позже текущего. Далее, ограничиваем кол-во результатов до одного, сортируем результаты по возрастанию — таким образом, если после текущего материала вышло 2 новых, то мы получим тот что был опубликован сразу после текущего, ведь у более позднего (свежего) дата создания будет больше. - Если ID нашелся, мы загружаем сущность следующего материала.
- Из материала мы добавляем в наш массив для
next
заголовок материала и URL на него. - Далее мы проделываем аналогично для предыдущего материала. Отличие лишь в том, что дата создания должна быть меньше текущего материала, а сортировать результаты нужно по убыванию.
В результате данного обработчика будет формироваться два массива, каждый из
которых будет в переменной next
и previous
соответственно. Если ссылки на
следующий или предыдущий материал нет, то массив останется пустым.
Далее нам нужно объявить и описать темплейт, который будет готовить разметку для наших кнопок на основе этих двух массивов. Мы не указали определенный темплейт, поэтому название его будет идентично нашему theme hook, а Drupal будет искать его в templates папке модуля.
<div class="previous-next">
<div class="previous-next__previous">
{% if previous %}
<a href="{{ previous.url }}" class="previous-next__link previous-next__link--previous">
<span>← {{ 'Previous'|t }}</span>
{{ previous.label }}
</a>
{% endif %}
</div>
<div class="previous-next__next">
{% if next %}
<a href="{{ next.url }}" class="previous-next__link previous-next__link--next">
<span>{{ 'Next'|t }} →</span>
{{ next.label }}
</a>
{% endif %}
</div>
</div>
В темплейте ме объявляем всю разметку. Мы проверяем пустые ли переменные next
и previous
(Twig возвращает FALSE
если массив пустой), и если нет, выводим
url и заголовок сущности.
? Обратите внимание что условия на кнопки сделаны внутри их основных обёрток. Это сделано нарошно, чтобы в CSS при помощи флексбоксов эти элементы могли занимать свою область даже будучи пустыми. Это необходимо, когда ссылка на предыдущий материал отсутствует (слева), а ссылка на следующий присутствует ( справа), и чтобы при этом ссылка на следующий не прыгала на место предыдущего. ?
Theme hook полностью готов, им можно пользоваться в любом месте. Но мы добавим
его вызов в качестве псевдо-поля, чтобы можно было цеплять к материалам из
админки и управлять его положением и видимостью. Мы добавим соответствующее
псевдо-поле только для сущности node
, как пример.
Для добавления псевдо-поля нам нужно будет объявить два
хука: hook_entity_extra_field_info()
и hook_ENTITY_TYPE_view()
. Оба они
объявляются в dummy.module.
use Drupal\Core\Entity\Display\EntityViewDisplayInterface;
use Drupal\Core\Entity\EntityInterface;
use Drupal\node\Entity\NodeType;
/**
* Implements hook_entity_extra_field_info().
*/
function dummy_entity_extra_field_info() {
$extra = [];
foreach (NodeType::loadMultiple() as $bundle) {
$extra['node'][$bundle->id()]['display']['previous_next'] = [
'label' => t("Buttons to previous and next entity"),
'weight' => 100,
'visible' => FALSE,
];
}
return $extra;
}
/**
* Implements hook_ENTITY_TYPE_view().
*/
function dummy_node_view(array &$build, EntityInterface $entity, EntityViewDisplayInterface $display, $view_mode) {
if ($display->getComponent('previous_next')) {
$build['previous_next'] = [
'#theme' => 'dummy_previous_next',
'#entity' => $entity,
];
}
}
Как можете заметить, мы добавляем в $build
сущности наш render array с вызовом
нашего theme hook и передачей ему текущей сущности, для которой это псевдо-поле
готовится. Таким образом, нам нет нужды заботиться о текущей сущности и её
получении.
После чего включаем модуль, если включен, сбрасываем кэш и заходим в любой тип
материала node
, и в управлении отображением включаете данное псевдо-поле
перетаскивая его в любой активный регион, так как по умолчанию оно будет
отключено.
Заходим на страницу и смотрим результат!
А дальше дело за малым, немного стилей, например таких:
.previous-next {
display: flex;
flex-flow: wrap;
&__previous {
flex: 0 1 49%;
}
&__next {
flex: 0 1 49%;
margin-left: auto;
}
&__link {
display: block;
border: 2px solid #CCC;
padding: 18px;
height: 100%;
color: #333;
&:hover {
border-color: #007bff;
}
span {
display: block;
margin-bottom: gap();
font-size: 80%;
color: #757575;
}
&--next {
text-align: right;
}
}
}
И вуаля!
Вот у нас и готовы универсальные кнопочки на предыдущий и следующий материал для всех типов сущностей.
Ссылки
Комментарии
Классное решение!
Приветствую. Как добавить вывод первой картинки материалов определенного стиля? В Семерке это делал достаточно легко, а с Восьмеркой - пока напряг
Примерно так можно посмотреть ещё вот этот кусок где оно и пишется.
Если я правильно понял о чем речь. Вот тут пример таких кнопок.
Благодарю, Никлан
А при высоких нагрузках, будет ли это решение понижать производительность? Вот ещё, что нашел: http://www.drupalthemez.com/blog/creating-nextprevious-node-pager-using-views-drupal-8 - здесь, используется Views с хуком, как я понимаю.
Не знаю на счёт вьюсового варианта. Но вариант из материала и тот что я в пример скинул - нет, не будет. Он кэшируется очень жестко. Причем даже туповато, он не изменится автоматически если изменится заголовок или фотка у следующего или предыдущего материала. ?
А ведь из данного решения, можно сделать полностью готовы модуль для сообщества. Подобного не нашел. осталось добавить к полю настраиваемый виджет с выбором, типа материала, термина и стиля картинки (учесть бы еще Paragraphs, который может быть прикреплен к ноде с полем image) - стал бы лучшем модулем из всех существующих
День добрый. У меня возникла проблема - сделал все так как было в видео о создании блога (часть 3) и у меня вылезает такая ошибка, при переходе на страницу данного типа материала - The website encountered an unexpected error. Please try again later. Error: Call to a member function label() on null in template_preprocess_kava_previous_next() (line 28 of modules/custom/kava/kava.theme.inc). Модуль присутствует в Home ->Admin -> Structure -> Types -> Coffee blog -> Display при отключении модуля ошибки нет. Подскажите в чем может быть проблема.
сори за беспокойство, проблему решил))
Спасибо! Модуль достоин публикации на drupal.org.
Очень нравится Друпал, наверно лучшая система для построения сайтов. Решал аналогичную задачу недавно на одном сайте, сделал навигацию в виде стрелок. Отлично смотрится.