Сделать такой каталог, в котором бы показывалось 8 карточек элементов и была кнопка «Еще» – клик по которой подгружал бы еще 8 карточек и т.д. – естественно без перезагрузки страницы (21 век на дворе!). Плюс не помешала бы сортировка по разным параметрам, а также фильтр (куда ж без него).

Делать решено на примере каталога книг, просто потому что в рабочих планах есть необходимость реализовывать похожий функционал. Ну и читать тоже люблю, да ))

Что получилось, можно смотреть здесь:


Инструменты:

– MODx Revolution версии 2.3.5-PL
– плагин getResources 1.6.1-pl
– плагин getPages 1.2.4-pl
– плагин Hits 1.3.0-pl
– руки, голова

Поехали:

Шаг 1. Подготовка

1.1. Устанавливаем MODx

1.2. Устанавливаем плагины

Ставим: собственно сам getResources (для выборки ресурсов), getPages (для разбивки выборки на подстраницы), Hits (для подсчета популярности того или иного ресурса, нужно для сортировки популярности). Все плагины есть в Установщике.

Отдельно пара слов про Hits – его пришлось немного модифицировать, чтобы в число посещений ресурса считалось не каждое обновление страницы, а только одно на каждую сессию. Для этого в коде стандартного плагина Hits (из Установщика) были сделаны такие изменения (добавленные строки выделены жирным):
if($depth < 1) $depth = 1;
if (!isset($_SESSION['count_hits'])) $_SESSION['count_hits'] = array();
if($punch && $amount) {
if (!in_array($punch, $_SESSION['count_hits'])) {
$hit = $modx->getObject('Hit',array(
'hit_key' => $punch
));
if($hit) {
// increment the amount
$hit->set('hit_count',($knockout ? 0 : (integer)$hit->get('hit_count')) + $amount); 
$hit->save();
} else {
// create a new hit record
$hit = $modx->newObject('Hit');
$hit->fromArray(array(
'hit_key' => $punch,
'hit_count' => $amount
));
$hit->save(); 
}
$_SESSION['count_hits'][] = $punch;
// записываем число просмотров в tv-параметр
$currentResource = $modx->getObject('modResource', $punch);
$currentResource->setTVValue('hits', $hit->hit_count);
}
}

Что конкретно добавлено:
– задаем переменную в сессии, чтобы сохранять в нее id просмотренных ресурсов
– в код записи числа просмотров для ресурса добавили проверку, есть ли уже этот ресурс в сессии
– если страница ранее еще не просматривалась, то увеличиваем для нее hits и записываем ее id в сессию
– добавлено сохранение числа просмотров в TV-параметр ресурса (его создадим чуть позже, и зачем это нужно, тоже далее поясню)

Дополнительно я поставила пакет BootstrapForMODX – чтобы не возиться со стилями, а использовать Bootstrap.

1.3. Создаем шаблоны

Мне понадобилось 3 шаблона:

1.3.1. Шаблон для главной страницы

(именно на ней будет выводиться наш ajax-каталог)
В нем ничего особого нет:
<!DOCTYPE html>
<html>
<head>
[[$header]]
</head>
<body>
<div class="container">
[[*content]]
</div>
[[$footer]]
<script src="/js/catalog.js"></script>
</body>
</html>

Только дополнительно к стилям и скриптам Bootstrap подключаем файл /js/catalog.js, где напишем скрипты для нашего каталога.

1.3.2. Шаблон элемента

Шаблон для страницы отдельной книги, например, http://catalog.26th.ru/catalog/devid-mitchell/oblachnyij-atlas/

Пока это только основа шаблона:
<!DOCTYPE html>
<html>
<head>
[[$header]]
</head>
<body>
<div class="container">
<div class="page-header">
<h1><a href="/">Каталог книг</a> <small>(на MODx Revolution)</small></h1>
<p>Пример каталога элементов на getResources с Ajax-подгрузкой</p>
</div>

<div class="row" style="margin-bottom:30px;">
<div class="col-md-3 item-left">
Здесь будет картинка книги
</div>
<div class="col-md-9">
Здесь будет название, описание и прочая информация
</div>
</div>
</div>
[[$footer]]
</body>
</html>

Мы добавим в него вывод информации позже. Также в нем не вызывается catalog.js

1.3.3. Пустой шаблон

Очень простой:
[[*content]]

Понадобится нам для той странички, у которой мы ajax-ом будем запрашивать данные. На ней лишний код не нужен, поэтому пустой шаблон создаем обязательно.

1.4. Создаем вспомогательные ресурсы

Для книг я планирую указывать их жанры. И хочу, чтобы добавлять дополнительные жанры впоследствии можно было без затруднений.

Для этого я создам ресурс «Жанры» и в нем создам дочерние ресурсы, где в pagetitle укажу название жанра.



Важно: поле «Псевдоним» (alias) у каждого жанра я заполняю на латинице и при этом добавляю в начале и в конце звездочку, вот так:



Зачем нужны эти звездочки – ниже расскажу.

1.5. Создаем дополнительные TV-параметры

В последних версиях MODx tv-параметры названы просто Дополнительными полями, но привычка – сильная вещь, поэтому – «tv-параметры».

Наши элементы (книги) будут обладать дополнительными свойствами:
– Список жанров
– Формат (электронная, бумажная или аудиокнига)
– Обложка (картинка)
– Язык
– Цена
– Год издания
– Число просмотров (как показатель популярности)

Для этих свойств создаем TV-параметры:



В последней строке мы выбираем в «Возможные значения» те самые жанры из папки с id=5 (это id нашей папки «Жанры»). Таким образом, когда мы добавим новые жанры, они автоматически отобразятся в возможных значениях этого TV-параметра.

Также не забываем поставить галочку у шаблона «Шаблон элемента» на вкладке «Доступно для шаблонов».

1.6. Создаем страницы элементов

Для них сделаем отдельную папку (ресурс-контейнер) «Каталог», чтобы страницы элементов не «путались под ногами». Важно: у ресурса «Каталог» выбираем шаблон «Пустой шаблон».

В папке «Каталог» создаем ресурсы по авторам (для товаров это были бы категории). И уже в папке каждого автора создаем страницы для конкретных книг. Для них ставим «Шаблон элемента».



Заполняем информацию по каждой книге:
– Название
– Псевдоним
– Аннотация
– Содержимое (там я указала отрывок книги)
– Дополнительные поля





Дополнительное поле «Число просмотров» заполнять не нужно – его будет перезаписывать плагин Hits, когда число посещений для данного ресурса будет увеличиваться.

Шаг 2. Ajax-подгрузка контента

Итак, подготовка закончена, у нас есть необходимые плагины и ресурсы, список которых надо выводить на сайте.

Для начала настроим просто вывод списка ресурсов. Сделаем это в ресурсе «Каталог» (который у нас на «Пустом шаблоне» – пусть это вас не смущает).

Для вывода списка используем getResources:
[[getResources?
&limit=`8`
&parents=`2`
&hideContainers=`1`
&tpl=`tplList`
&includeTVs=`1` 
]]

Здесь мы задаем вывод 8 ресурсов из папки «Каталог» (id=2), исключая контейнеры (то есть ресурсы с авторами), по шаблону из чанка tplList, и при это указываем, что нам понадобятся и значения tv-параметров ресурсов.

Чанк tplList пока содержит только название ресурса со ссылкой на его страницу:
<p><a href="/[[~[[+id]]]]" target="_blank">[[+pagetitle]]</a></p>

Сортировку не задаю, ее добавим позже.

Итак, открываем http://catalog.26th.ru/catalog/ и видим список ресурсов:



Но нам понадобятся не только 8 первых ресурсов, но и все остальные тоже.

Поэтому используем плагин getPage, чтобы получить список всех доступных ресурсов, разбитый на подстраницы:
[[!getPage? 
&elementClass=`modSnippet` 
&element=`getResources`
&limit=`8`
&parents=`2`
&hideContainers=`1`
&tpl=`tplList`
&includeTVs=`1` 
]] 
[[+page.nav]]

Его параметры совпадают с параметрами getResources, ибо getPage не делает выборку, он лишь разбивает на подстраницы то, что выдает getResources. Поэтому заменяем только начало и вызов делаем некешируемым.

Плейсхолдер page.nav отвечает за вывод навигации.

Обновяем наш список и видим, что появились ссылки на подстраницы:



Собственно в ссылках на подстраницы лишь добавлен параметр page:
http://catalog.26th.ru/catalog/?page=2

Таким образом, у нас есть список ресурсов, и именно его мы будем запрашивать ajax-ом. Так как в результате нам будет нужен только сам список, а не навигация, то строчку
[[+page.nav]]
убираем.

Итого в коде ресурса «Каталог» имеем:
[[!getPage? 
&elementClass=`modSnippet` 
&element=`getResources`
&limit=`8`
&parents=`2`
&hideContainers=`1`
&tpl=`tplList`
&includeTVs=`1` 
]]

Теперь открываем редактирование главной страницы. Для нее выставляем «Шаблон каталога».
И пишем такой код:
<div class="page-header">
<h1>Каталог книг<small>(на MODx Revolution)</small></h1>
<p>Пример каталога элементов на getResources с Ajax-подгрузкой</p>
</div>
<div class="row">
<div class="col-md-9">
<div class="sort">Здесь будет сортировка</div>
<div id="catalog"></div>
<button class="btn btn-block btn-warning" id="more" data-show="0" style="display:none;">Еще</button>
</div>
<div class="col-md-3">
<div class="filter">Здесь будет фильтр</div>
</div>
</div>

На данном шаге важно – мы создали блок #catalog, в который будем подгружать наш список, и кнопку «Еще» (правда, сразу же ее скрыли).

Теперь открываем файл http://catalog.26th.ru/js/catalog.js и в нем прописываем функции для подгрузки списка ресурсов:
// адрес нашего списка ресурсов (где идет вывод getResources)
var uri = '/catalog/';

$(document).ready(function() {
// загружаем начальный блок
loadCatalog(0, 1);

// клик на кнопку "Еще"
$('#more').on('click', function() {
var showPage = $(this).data('show');
var preloadPage = parseInt(showPage) + 1;
loadCatalog(showPage, preloadPage);
}); 
});

function loadCatalog(showPage, preloadPage) {
// скрываем кнопку "Еще"
$('#more').hide();

// показываем блок с ранее загруженным контентом и прокручиваем к нему
if (showPage != 0) {
$('#page' + showPage).show('slow');
$('html,body').animate({ scrollTop: $('#page' + showPage).offset().top - 100 }, 1000);
}

// создаем блок под новую загрузку
$('#catalog').append('<div id="page' + preloadPage + '"></div>');

uri = uri + '?page=' + preloadPage;
// загружаем ajax-ом контент следующей страницы, но не показываем его
$.ajax({
url: uri,
cache: false,
success: function(html) { 
if (html != '') {
$('#more').data('show', preloadPage);
$('#more').show();
$('#page' + preloadPage).hide();
$('#page' + preloadPage).html(html);
if (preloadPage == 1) loadCatalog(1, 2);
}
}
});
}

Изначально я написала скрипт, который запрашивал содержимое очередной страницы (page=1, page=2, ...) и тут же выводил ее. Но когда доступные элементы заканчивались, получалось, что кнопка «Еще» показывается, а ресурсов уже нет, и ничего не подгружается.

Поэтому я решила сделать предварительную загрузку (preload) — чтобы и мне заранее можно было узнать, а есть ли еще элементы, и пользователям вроде как польза – меньше ждать загрузки контента. Вот так невольно и улучшаем юзабилити ))

Поэтому скрипт стал работать таким образом:

При загрузке страницы запрашивается содержимое с адреса http://catalog.26th.ru/catalog/, вставляется в блок
<div id="page1">
и остается скрытым, затем тут же запрашивается содержимое http://catalog.26th.ru/catalog/?page=2, вставляется в блок
<div id="page2">
и остается скрытым, а блок
<div id="page1">
тем временем показывается. То есть при первой загрузке страницы мы дважды обращаемся ajax-ом к странице http://catalog.26th.ru/catalog/

У кнопки «Еще» в атрибут data-show записываем номер последнего загруженного блока (то есть именно его мы должны показать по клику на кнопке).

Далее, когда мы жмем «Еще»:
– Скрываем эту кнопку
– Показываем блок с номером из data-show этой кнопки
– Затем подгружаем следующую подстраницу с http://catalog.26th.ru/catalog/
Важно (почему собственно и не показываем сразу этот подгруженный контент): сначала смотрим, а вернулось ли нам хоть что-нибудь. Ибо если элементы кончились, то мы получим пустой ответ. А если мы получили пустой ответ, то в этом случае кнопку «Еще» оставляем скрытой – потому что она более не нужна, ведь подгружать нечего.

Всё, теперь можем открыть нашу главную страницу и посмотреть, как подгружается по 8 строчек с названиями наших элементов.

Теперь настроим чанк tplList, чтобы карточки элементов были похожи собственно на карточки. Окончательный вид у него такой:
[[+idx:mod=`4`:is=`1`:then=`<div class="row">`]]
<div class="col-md-3 list-item text-center">
<p class="list-item-image">
<img src="/[[+tv.image]]" alt="" class="img-thumbnail">
<span class="label label-primary label-lang">[[+tv.lang]]</span>
[[showFormats?content=`[[+tv.format]]`]]
</p>
<h5><a href="/[[~[[+id]]]]" target="_blank">[[+pagetitle]]</a></h5>
<p class="grey"><small>[[getAuthor?id=`[[+parent]]`]], [[+tv.year]] г.</small></p>
<p><small>[[showCategories?content=`[[+tv.category]]`]]</small></p>
<p class="bg-info list-item-price">[[+tv.price]] руб.</p>
<p class="grey"><small>Просмотров: [[+tv.hits]]</small></p>
</div>
[[+idx:mod=`4`:is=`0`:then=`</div>`]]

Пойдем по порядку:

[[+idx]]
– плейсхолдер, который содержит порядковый номер выводимого элемента. Т.к. я использую Bootstrap, то у меня выводится по 4 элемента в строке – и мне нужно добавлять разрывы строк в сплошной список. Поэтому перед 1-ым, 5-ым, 9-ым и т.д. элементом я добавляю
<div class="row">
, а после каждого 4-го, 8-го, 12-го и т.д. добавляю закрывающий
</div>
.

Далее
[[+tv.image]], [[+tv.lang]], [[+tv.year]], [[+tv.price]], [[+tv.hits]]
– выводим значения соответствующих tv-параметров.

Еще остаются параметры Формат, Жанры и Автор. Если мы просто напишем
[[+tv.format]]
, то получим, например, electro||audio, что нам, конечно, не подходит.

Поэтому пишем сниппет
[[showFormats?content=`[[+tv.format]]`]]
для распарсивания значений в человеческий вид. Его код:
<?php
$formats = explode('||', $content);
$arrFormats = array(
'electro' => 'электр.',
'casual' => 'бум.',
'audio' => 'аудио'
);
foreach($formats as $format) {
$out .= '<span class="label label-success">' . $arrFormats[$format] . '</span>';
}
if ($out != '') echo '<span class="formats">' . $out . '</span>';

Аналогично создаем сниппет
[[showCategories?content=`[[+tv.category]]`]]
для вывода списка жанров:
<?php
$out = '';
$categories = explode('||', $content);
foreach($categories as $categ) {
$query = $modx->newQuery('modResource', array('parent'=>5, 'alias'=>$categ));
$query->sortby('pagetitle','ASC');
$resources = $modx->getCollection('modResource', $query);
foreach($resources as $resource) {
if ($resource->published == true && $resource->deleted != true && $resource->hidemenu != true) $out .= $resource->pagetitle . ', ';
}
}
return trim($out, ', ');

И простенький сниппет
[[getAuthor?id=`[[+parent]]`]]
для вывода имени автора (это название родительской категории):
<?php
if ((int)$id > 0) {
$resource = $modx->getObject('modResource', $id);
return $resource->pagetitle;
}

Обратите внимание, в хедере подключены дополнительные стили для каталога:
http://catalog.26th.ru/css/catalog.css

В итоге получаем такие карточки:



Вот только просмотры пока везде по нулям – давайте исправим. Для этого займемся шаблоном «Шаблон элемента», вставляем в него код:
<!DOCTYPE html>
<html>
<head>
[[$header]]
</head>
<body>
<div class="container">
<div class="page-header">
<h1><a href="/">Каталогкниг</a><small>(на MODx Revolution)</small></h1>
<p>Пример каталога элементов на getResources с Ajax-подгрузкой</p>
</div>
<div class="row" style="margin-bottom:30px;">
<div class="col-md-3 item-left">
<p style="margin-top:25px;"><img src="/[[*image]]" alt="" class="img-thumbnail"></p>
<span class="label label-primary label-lang">[[*lang]]</span>
[[showFormats?content=`[[*format]]`]]
<p class="bg-info list-item-price">[[*price]] руб.</p>
<p class="grey"><small>Просмотров: [[!Hits? &punch=`[[*id]]` &hit_keys=`[[*id]]` &tpl=`hitsID`]]</small></p>
</div>
<div class="col-md-9">
<h2>[[*pagetitle]]</h2>
<p>[[getAuthor?id=`[[*parent]]`]], [[*year]] г.</p>
<p class="grey">[[showCategories?content=`[[*category]]`]]</p>
<p style="margin-top:20px;"><b>Аннотация:</b></p>
<p>[[*introtext]]</p>
<p style="margin-top:20px;"><b>Отрывок:</b></p>
[[*content]]
</div>
</div>
</div>
[[$footer]]
</body>
</html>

В нем используем
[[*image]]
и т.д. для вывода наших дополнительных полей (и те же вспомогательные сниппеты, что были в чанке tplList).

Отличием является вызов сниппета
[[!Hits? &punch=`[[*id]]` &hit_keys=`[[*id]]` &tpl=`hitsID`]]

Именно он:
– Прибавляет +1 к числу просмотров текущего ресурса (для этого передаем id в &punch)
– Записывает число просмотров в tv-параметр hits (соответствующие изменения в код плагина мы вносили выше)
– И выводит число просмотров (в &hit_keys указываем, просмотры какого ресурса нам нужны), используя при этом чанк hitsID, который содержит только одну строчку (собственно, число просмотров):
[[+hit_count]]

Итого на отдельной странице, например, на http://catalog.26th.ru/catalog/u-nesbe/syin/ получаем такой контент:



Число просмотров – слева под картинкой.

Теперь при каждом посещении страницы книги (при каждой новой сессии) у ресурса увеличивается счетчик просмотров.

В списке тоже видим это число просмотров:



Итак, радуемся – каталог с динамической подгрузкой контента (без перезагрузки страницы) готов!

Шаг 3. Сортировка

Для добавления сортировки (а далее и фильтра) будем использовать get-параметры.

Поэтому строчка uri = uri + '?page=' + preloadPage; в нашем js-скрипте перестанет работать – ибо она будет «затирать» все параметры сортировки или фильтра.

Чтобы этого не происходило, добавим в код функцию для добавления (замены, удаления) get-параметра в URI и будем использовать ее:
function setAttr(prmName,val) {
var res = '';
var d = uri.split("?"); 
var base = d[0];
var query = d[1];
if(query) {
var params = query.split("&"); 
for(var i = 0; i < params.length; i++) { 
var keyval = params[i].split("="); 
if(keyval[0] != prmName) { 
res += params[i] + '&';
}
}
}
if (val != '') res += prmName + '=' + val;
return base + '?' + res;
}

Таким образом, строчка с uri принимает вид:
uri = setAttr('page', preloadPage);

Теперь мы можем запрашивать вторую, третью и прочие страницы, оставляя неизменными параметры сортировки.

Далее добавляем кнопки для сортировки на Главную страницу (вместо строчки «Здесь будет сортировка»):
<div class="btn-group" role="group">
<button data-sort="hits" data-sortdir="desc" type="button" class="btn btn-xs btn-default active">По популярности<span class="glyphicon glyphicon-arrow-down"></span></button>
<button data-sort="hits" data-sortdir="asc" type="button" class="btn btn-xs btn-default"><span class="glyphicon glyphicon-arrow-up"></span></button>
</div>
<div class="btn-group" role="group">
<button data-sort="price" data-sortdir="desc" type="button" class="btn btn-xs btn-default">По цене<span class="glyphicon glyphicon-arrow-down"></span></button>
<button data-sort="price" data-sortdir="asc" type="button" class="btn btn-xs btn-default"><span class="glyphicon glyphicon-arrow-up"></span></button>
</div>
<div class="btn-group" role="group">
<button data-sort="year" data-sortdir="desc" type="button" class="btn btn-xs btn-default">По году издания<span class="glyphicon glyphicon-arrow-down"></span></button>
<button data-sort="year" data-sortdir="asc" type="button" class="btn btn-xs btn-default"><span class="glyphicon glyphicon-arrow-up"></span></button>
</div>
<div class="btn-group" role="group">
<button data-sort="title" data-sortdir="desc" type="button" class="btn btn-xs btn-default">По названию<span class="glyphicon glyphicon-arrow-down"></span></button>
<button data-sort="title" data-sortdir="asc" type="button" class="btn btn-xs btn-default"><span class="glyphicon glyphicon-arrow-up"></span></button>
</div>

Добавили 4 вида сортировки – по популярности, по цене, по году издания, по названию. Каждая сортировка доступна по убыванию и возрастанию. По умолчанию будем сортировать по популярности по убыванию.



Чтобы передавать информацию о параметре сортировки и направлению, используем атрибуты data-sort и data-sortdir.

Теперь по клику на той или иной кнопке нам надо:
– Выделить ее активной
– Сформировать нужный адрес, например, /catalog/?sortby=price&sortdir=asc, чтобы получить нужный список ресурсов
– Очистить блок
<div id="catalog">

– Подгрузить в него контент с нового адреса (с параметрами сортировки)

Добавляем в /js/catalog.js (в $(document).ready(function(){):
$('.sort button').on('click', function() {
$('.sort button').removeClass('active');
$(this).addClass('active');
uri = setAttr('sortby', $(this).data('sort'));
uri = setAttr('sortdir', $(this).data('sortdir'));
$('#catalog').html('');
loadCatalog(0, 1);
return false;
});

Но пока наш каталог на http://catalog.26th.ru/catalog/ не умеет обрабатывать параметры sortby и sortdir – так давайте исправим это.

В getResources за сортировку отвечают параметры:
&sortby – сортировка по какому-либо полю ресурса (кроме tv-параметров), например, сортировка по дате публикации: &sortby=`{«publishedon»:«DESC»}`
&sortbyTV – сортировка по tv-параметру
&sortdirTV – направление сортировки по tv-параметру (desc|asc)
&sortbyTVType – тип сортировки (string|integer)

В &sortbyTVType сразу прописываем `integer`, ибо все tv-параметры, по которым мы сортируем – числа (год, цена, число просмотров).

В &sortdirTV нам нужно записывать ASC или DESC в зависимости от get-параметра sortdir. Напишем небольшой сниппет
[[!sortbyTV]]
, который будет считывать get-параметр и подставлять нужное значение:
<?php
$sort_direction = (isset($_GET['sortdir']) && $_GET['sortdir'] == 'asc') ? 'ASC' : 'DESC';
return $sort_direction;

В &sortbyTV нам нужно указывать необходимый tv-параметр. Вот, кстати, зачем понадобилось записывать число просмотров отдельно в tv-параметр – чтобы можно было использовать его в &sortbyTV. Только здесь нужно помнить, что если сортировка идет по названию, то здесь должно быть пусто. Делаем еще один сниппет
[[!sortbyTV]]
:
<?php
$sort_name = isset($_GET['sortby']) ? strip_tags($_GET['sortby']) : 'hits';
if ($sort_name != 'title') return $sort_name;
return '';

Если сортировка идет по названию, мы должны указать это (и направление сортировки) в параметре &sortby – здесь воспользуемся сниппетом
[[!sort]]
:
<?php
$sort_name = isset($_GET['sortby']) ? strip_tags($_GET['sortby']) : 'hits';
$sort_direction = (isset($_GET['sortdir']) && $_GET['sortdir'] == 'asc') ? 'ASC' : 'DESC';
if ($sort_name == 'title') return '"pagetitle":"' . $sort_direction . '",';
return '';

Таким образом, код ресурса «Каталог» становится таким:
[[!getPage? 
&elementClass=`modSnippet` 
&element=`getResources`
&tpl=`tplList`
&limit=`8`
&includeTVs=`1` 
&parents=`2`
&hideContainers=`1`
&sortby=`{[[!sort]]"publishedon":"DESC"}` 
&sortbyTV=`[[!sortbyTV]]` 
&sortdirTV=`[[!sortdirTV]]` 
&sortbyTVType=`integer` 
]]

Заметьте, в параметре &sortby добавлено «publishedon»:«DESC» на тот случай, если, например, сортировка идет по числу просмотров – чтобы элементы с одинаковым числом просмотров сортировались по дате добавления.

И снова небольшая порция радости – сортировка готова.

Шаг 4. Фильтр

Добавление фильтра делаем аналогично сортировке.

Добавляем html-код для фильтра в «Главную страницу» вместо строчки «Здесь будет фильтр»:
<p><b>Фильтр</b></p>
<div class="filter-group btn-group" role="group">
<button data-filter="lang" data-value="ru" type="button" class="btn btn-sm btn-default">RU</button>
<button data-filter="lang" data-value="en" type="button" class="btn btn-sm btn-default">EN</button>
</div>
<div class="filter-group btn-group" role="group">
<button data-filter="format" data-value="electro" type="button" class="btn btn-sm btn-default">Электронные</button>
<button data-filter="format" data-value="casual" type="button" class="btn btn-sm btn-default">Бумажные</button>
<button data-filter="format" data-value="audio" type="button" class="btn btn-sm btn-default">Аудио</button>
</div>
<div class="filter-group btn-group-vertical" role="group">
[[filterCategory]]
</div>

Здесь сниппет
[[filterCategory]]
выводит кнопки для Жанров, его код:
<?php
$out = '';
$query = $modx->newQuery('modResource', array('parent'=>5));
$query->sortby('pagetitle','ASC');
$resources = $modx->getCollection('modResource', $query);
foreach($resources as $resource) {
if ($resource->published == true && $resource->deleted != true && $resource->hidemenu != true) $out .= '<button data-filter="category" data-value="' . trim($resource->alias, "*") . '" type="button" class="btn btn-sm btn-default"><span class="glyphicon glyphicon-unchecked"></span> ' . $resource->pagetitle . '</button> ';
}
return $out;

И добавляем обработку клика по кнопкам фильтра в catalog.js (после обработки кликов по кнопкам сортировки):
$('.filter .filter-group button').on('click', function() {
var filter_group = $(this).parent('.filter-group');
var active = $(this).hasClass('active');
var filter = $(this).data('filter');
var value = '';
if (filter == 'category') { // категорийможетбытьвыбранонесколько (checkbox)
$(this).toggleClass('active');
var categories = '';
$('.filter button[data-filter="category"].active').each(function() {
categories += $(this).data('value') + '|';
});
value = categories.substr(0, categories.length - 1);
}
else { // остальные фильтры (язык и формат) - только один вариант (radiobutton)
filter_group.find('button').removeClass('active');
if (!active) {
$(this).addClass('active');
value = $(this).data('value');
}
}
uri = setAttr(filter, value);
$('#catalog').html('');
loadCatalog(0, 1);
return false;
});

В нем учитывается, что Язык и Формат можно выбрать только какой-то один. А вот жанров можно отметить несколько – тогда будут выводиться те книги, у которых в списке жанров есть все выбранные жанры.

Таким образом, кликая по кнопкам фильтра, мы формируем адреса вида (выбранные жанры записываем в параметр category через вертикальную черту):
/catalog/?lang=ru&format=electro&category=child
/catalog/?lang =ru&category=child|fantastic

И теперь осталось опять же научить страницу catalog.26th.ru/catalog/ разбираться с этими новыми параметрами.

За фильтрацию по tv-параметрам в getResources отвечает параметр &tvFilters. Например, для строки:
/catalog/?lang=ru&category=child|fantastic

мы должны указать в этом параметре:
&tvFilters=`lang==ru,category==%*child*%,category==%*fantastic*%`

перечисление через запятую означает условие И, % соответствуют поиску по LIKE%...%

Теперь немного о звездочках. Что было бы, если бы мы не добавили их в alias страниц жанров.

Допустим, есть два жанра с похожими alias: Классика (classic) и Неоклассика (neoclassic).

И есть две книги:
– Книга_1 относится к жанрам Классика и Детективы, поэтому у нее tv-параметр category=classic||detectiv (разделитель || мы задали при добавлении tv-параметра)
– Книга_2 относится к жанру Неоклассика, поэтому у нее tv-параметр category=neoclassic

Если мы захотим выбрать только те ресурсы, у которых среди жанров есть Классика, то напишем &tvFilters=`category==%classic%`. И под такой фильтр попадут и Книга_1, и Книга_2 (т.к. поиск идет просто по строке) – а нам нужна только Книга_1.

Если же в alias жанров добавить звездочки, то получится, что у Книги_1 category=*classic*||*detectiv*, а у Книги_2 category=*neoclassic*

Поэтому при фильтре по &tvFilters=`category==%*classic*%` мы получим только Книгу_ 1.

Далее пишем сниппет
[[!filter]]
для формирования &tvFilters из get-параметров:
<?php
$out = array();
$lang = isset($_GET['lang']) ? strip_tags($_GET['lang']) : '';
$format = isset($_GET['format']) ? strip_tags($_GET['format']) : '';
$category = isset($_GET['category']) ? strip_tags($_GET['category']) : '';
$categories = explode('|', $category);
if ($lang != '') $out[] = 'lang==' . $lang;
if ($format != '') $out[] = 'format==%' . $format . '%';
if ($category != '') {
foreach ($categories as $categ) {
$out[] = 'category==%*' . $categ . '*%';
}
}
return implode(',', $out);

Теперь код ресурса «Каталог» такой:
[[!getPage? 
&elementClass=`modSnippet` 
&element=`getResources`
&tpl=`tplList`
&limit=`8`
&includeTVs=`1` 
&parents=`2`
&hideContainers=`1`
&sortby=`{[[!sort]]"publishedon":"DESC"}` 
&sortbyTV=`[[!sortbyTV]]` 
&sortdirTV=`[[!sortdirTV]]` 
&sortbyTVType=`integer` 
&tvFilters=`[[!filter]]`
]]

Облегченно выдыхаем – фильтр настроен!

Заключение

Итоговый результат – на http://catalog.26th.ru/
Скрипт каталога – http://catalog.26th.ru/js/catalog.js
Дополнительные стили для каталога – http://catalog.26th.ru/css/catalog.css

Единственное, что не сделала – если юзер «натыкает» сортировку и фильтры, а затем из полученного списка перейдет на какую-нибудь страницу книги – то вся его выбранная сортировка пропадет, поэтому когда он нажмет «Назад» в браузере – то снова попадет на список по умолчанию.

Получается, надо бы записывать в сессию еще и настройки сортировки и фильтра (и количество подгруженных страниц). В дальнейшем, думаю, сделаю. Пока же добавила target="_blank" для ссылок на отдельные страницы ))

Если есть вопросы, замечания, уточнения – пишите в комментариях к статье. Критика приветствуется, но конструктивная.

А всем кто дочитал статью подарок — бесплатная книга по юзабилити «Как избежать обыденных ошибок». Получить книгу можно здесь.

Автор: Дарья Севостьянова, руководитель отдела разработки Сервиса 1PS.RU