PageBlocks стал ещё мощнее — теперь с пагинацией, сортировкой и фильтрацией

PageBlocks получил мощный апдейт — теперь вы можете реализовать пагинацию, сортировку и фильтрацию ваших данных с нуля буквально за пару строк кода. Всё работает как через обычные GET-параметры (?page=2), так и через человекочитаемые URL (например, /page-2), что идеально для SEO и красоты.


ДЕМО

Пагинация


Основные настройки:
'showPagination' => 1,
'pageVar' => 'page',
'pageLink' => '?{pageVar}={page}',

  • showPagination — включает или отключает пагинацию (1 — включена, 0 — выключена).
  • pageVar —имя GET-параметра, обозначающего текущую страницу (по умолчанию page).
  • pageLink — шаблон для генерации ссылок. Может быть классическим ?page=2 или красивым URL, вроде /page-2. По умолчанию ?{pageVar}={page}

Пример через GET-параметры:
{'!pbResources' | snippet: [
    'fieldName' => 'products',
    'tpl' => '@FILE chunks/product.tpl',
    'toPlaceholder' => 'pls.products', // Результат записываем в плейсхолдер
    'limit' => 12,
    'showPagination' => 1,
    'pageLink' => '?page={page}' // Стандартное значение можно опустить
]}

Вывод данных на странице

1. Отображение общего количества элементов
<span id="pb-total">
    {'pls.total' | placeholder}
</span>
Дополнительные параметры:
  • targetTotal — изменяет ID HTML-элемента
  • plsTotal — задает имя плейсхолдера

2. Контейнер для вывода товаров
<div id="pb-items">
    {'pls.products' | placeholder}
</div>
Кастомизация
  • targetItems — меняет ID контейнера

3. Блок пагинации
<nav id="pb-pagination" aria-label="Page navigation">
    {'pls.pagination' | placeholder}
</nav>
Настройки:
  • targetPagination — изменяет ID навигации
  • plsPagination — задает имя плейсхолдера

Реализация через ЧПУ (чистые URL)

'pageLink' => '/page-{page}'

Что для этого нужно:

1. Настройка роутинга
Добавьте в core/App/routes/web.php:
Route::get('/page-{page}', 'ProductController@index');

2. Создание контроллера:
Разместите в core/APP/Http/Controllers/ProductController.php:
class ProductController extends Controller
{
    public function index(int $page)
    {
        // Указываем ресурс, где отображаются продукты
        $this->modx->resource = $this->modx->getObject(\modResource::class, 1);
        
        return view('file:templates/base', [
            'page' => $page, // передаём номер страницы в шаблон, если нужен
        ]);
    }
}
Теперь ваш URL /page-2 будет отдавать вторую страницу товаров.

Кнопка «Загрузить еще»


Базовая реализация:
<div class="d-flex js-center">
    {'pls.loadmore'|placeholder}
</div>

Настройки кнопки:
'plsLoadmore' => 'pls.loadmore',
'clsLoadmore' => 'btn btn-lg btn-outline-secondary w-100',
'tplLoadmore' => '@INLINE <button type="button" class="{$classes}" data-pbloadmore>{"pb.btn.loadmore" | lexicon}</button>'
  • plsLoadmore — имя плейсхолдера для кнопки;
  • clsLoadmore — CSS-классы для стилизации;
  • tplLoadmore — шаблон кнопки.

Бесконечная прокрутка (infinite scroll)


Для включения бесконечного скролла достаточно двух параметров:
'infiniteScroll' => 1,
'infiniteOffsetScroll' => 200, // сколько пикселей от низа до подгрузки

Сортировка


Базовые параметры сортировки:
'sortVar' => 'sort',
'sortLink' => '?{sortVar}={sort}',
  • sortVar — имя переменной, отвечающей за текущую сортировку. По умолчанию — sort.
  • sortLink — шаблон для формирования ссылок сортировки. Может быть в формате GET (?sort=...) или «чистым» URL (/sort-...).
Пример HTML-разметки
<div class="form-floating ms-auto" data-pbsort>
    <select name="sort" class="form-select" aria-label="Sorting">
        <option value="id-asc" selected>Oldest first</option>
        <option value="id-desc">Newest first</option>
        <option value="price-asc">From cheap to expensive</option>
        <option value="price-desc">From expensive to cheap</option>
        <option value="rating-asc">Rating asc</option>
        <option value="rating-desc">Rating desc</option>
    </select>
    <label for="floatingSelect">Sort products</label>
</div>

Кастомизация значений сортировки
Для более понятных URL можно использовать псевдонимы через параметр sortNames:
'sortNames' => 'expensive==price-desc,cheap==price-asc'
при такой настройки в URL будет добавлен параметр ?sort=expensive, вместо ?sort=price-desc

Обновленная разметка с псевдонимами:
<div class="form-floating ms-auto" data-pbsort>
    <select name="sort" class="form-select" aria-label="Sorting">
        <option value="id-asc" selected>Oldest first</option>
        <option value="id-desc">Newest first</option>
        <option value="cheap">From cheap to expensive</option>
        <option value="expensive">From expensive to cheap</option>
        <option value="rating-asc">Rating asc</option>
        <option value="rating-desc">Rating desc</option>
    </select>
    <label for="floatingSelect">Sort products</label>
</div>

Пример через «чистый» URL

'sortLink' => '/{sortVar}={sort}',

1. Настройка маршрута:
Route::get('/sort-{sortName}', 'ProductController@sort');

2. Добавляем метод sort для контроллера ProductController
public function sort(string $sortName)
    {
        $this->modx->resource = $this->modx->getObject(\modResource::class, 1);
        [$sortby, $sortdir] = $this->getSort($sortName);

        return view('file:templates/base', [
            'sortby' => $sortby,
            'sortdir' => $sortdir,
            'sortName' => $sortName
        ]);
    }

    public function getSort(string $sortName): array
    {
        [$sortby, $sortdir] = explode('-', $sortName);
        switch ($sortName) {
            case 'cheap':
                $sortby = 'price';
                $sortdir = 'asc';
                break;
            case 'expensive':
                $sortby = 'price';
                $sortdir = 'desc';
                break;
        }

        return [$sortby, $sortdir];
    }

Комбинирование пагинации и сортировки в ЧПУ

Для работы комбинированных URL вида /page-2/sort-cheap:
'pageLink' => '/page-{page}',
'sortLink' => '/sort-{sort}',

Обновленный маршрут:
Route::get('/page-{page}/{sort?}', 'ProductController@index');
Такой маршрут обрабатывает оба варианта: /page-6 и /page-6/sort-cheap

Фильтрация


Для вывода фильтров используем сниппет pbFilters.

Базовый пример использования:
{'!pbFilters' | snippet: [
    'fieldName' => 'products',
    'filterFields' => 'price,category,brand,size,color',

    // price — range input
    'tpl.item.price' => '@FILE chunks/filters/price.tpl',

    // brand — select
    'tpl.wrapper.brand' => '@FILE chunks/filters/select.tpl',
    'tpl.item.brand' => '@INLINE <option value="{$value}">{$name}</option>',

    // size — radio buttons
    'tpl.item.size' => '@FILE chunks/filters/radio.tpl',
]}

Обязательно нужно добавить параметр filterFields в сниппет, который выводит сами продукты — чтобы PageBlocks знал, какие фильтры учитывать при запросах:
'filterFields' => 'price,category,brand,size,color',

Параметры сниппета, который выводит данные, для фильтрации URL
'filterLink' => '?{name}={value}', // фильтр в виде ?price=100&brand=nike
  • filterLink — шаблон для формирования ссылок фильтров.

Фильтры в «чистом» URL


Чтобы фильтры выглядели как красивые сегменты URL, можно сделать так:
'filterLink' => '/{name}-{value}',
'filterSeparator' => ';',

Тогда URL будет выглядеть так:
/filter/category-10002;brand-nike/

1. Добавляем маршрут и обработку
Регистрируем маршрут, который поймает фильтры:
Route::get('/filter/{filters}', 'ProductController@filter');

2. В контроллере реализуем метод:
public function filter(string $filters)
{
    $this->modx->resource = $this->modx->getObject(\modResource::class, 1);

    return view('file:templates/base');
}

Фильтры + пагинация + сортировка

Чтобы объединить фильтрацию с пагинацией и сортировкой:
/filter/category-10002;brand-nike/page-2/sort-cheap

Нужно в настройках сниппета указать:
'filterLink' => '/{name}-{value}',
'pageLink' => '/page-{page}',
'sortLink' => '/sort-{sort}',

И обновить маршруты так:
Route::get('/filter/{filters}/{page?}', 'ProductController@filter'); // фильтры с пагинацией
Route::get('/filter/{filters}/{page?}/sort-{sortName}', 'ProductController@filter'); // фильтры + пагинация + сортировка
Метод filter в контроллере при этом можно оставить без изменений

JavaScript API


На фронте доступна глобальная JavaScript-переменная pbPagination, которая позволяет программно управлять пагинацией, сортировкой и фильтрацией без перезагрузки страницы.

pbPagination.next() // загружает следующую страницу
pbPagination.next('beforeend')  // подгружает следующую страницу (аналог кнопки "загрузить еще")
pbPagination.prev() // загружает предыдущую страницу
pbPagination.loadPage(2) // загружает необходимую страницу
pbPagination.sort('price', 'desc') // обновляет сортировку
pbPagination.sort('cheap') // если заполнен параметр sortNames
pbPagination.filter('brand', 'adidas') // добавляем фильтр
pbPagination.filter('brand', '') // удаляет фильтр brand
pbPagination.filter('category', [10002,10003]) // добавляет несколько значений для фильтра

События

Все запросы в PageBlocks выполняются через класс pbFetch, который генерирует на каждом этапе события. Это удобно для кастомизации поведения без необходимости переписывать логику.
События инициируются через CustomEvent с префиксом pb: и могут быть отменяемыми (в случае pb:before).

Доступные события

pb:before
  • method — метод запроса (`GET`, `POST`)
  • url — адрес запроса
  • target — селектор DOM-элемента, куда вставлять ответ
  • options — все переданные опции (`headers`, `body`, `form`, `expect`, и т.п.)
Пример
document.addEventListener('pb:before', (e) => {
    console.log('Запрос собирается отправиться:', e.detail.url);

    // Пример отмены запроса
    if (e.detail.url.includes('admin')) {
        e.preventDefault(); // Запрос не отправится
    }
});

pb:progress:start
Перед отправкой запроса, если `showProgress = true`
  • url — адрес запроса
Пример:
document.addEventListener('pb:progress:start', (e) => {
    console.log('Индикатор загрузки включён для:', e.detail.url);
});

pb:progress:end
после получения ответа или ошибки, если `showProgress = true`
  • url — адрес запроса
Пример:
document.addEventListener('pb:progress:end', (e) => {
    console.log('Индикатор загрузки выключен:', e.detail.url);
});

pb:success
при успешном ответе (`response.ok === true`)
  • method
  • url
  • target
  • response — объект Response
Пример
document.addEventListener('pb:success', (e) => {
    console.log('Успешный ответ:', e.detail.response);
});

pb:after
после успешной вставки данных в DOM (если `expect === 'html'`)
  • method
  • url
  • target
  • response — объект Response
  • data — HTML-строка или JSON-ответ
Пример
document.addEventListener('pb:after', (e) => {
    console.log('Ответ вставлен в DOM:', e.detail.target);
});

pb:error
при ошибке HTTP (например, 404, 500)
  • method
  • url
  • target
  • status
  • statusText
Пример
document.addEventListener('pb:error', (e) => {
    alert(`Ошибка ${e.detail.status}: ${e.detail.statusText}`);
});


pb:fail
при исключении или сетевой ошибке
  • method
  • url
  • target
  • error — объект ошибки
Пример
document.addEventListener('pb:fail', (e) => {
    console.error('Ошибка запроса:', e.detail.error);
});

pb:abort
если запрос был отменён вручную или другим запросом с тем же `cancelKey`
  • method
  • url
  • target
Пример
document.addEventListener('pb:abort', (e) => {
    console.warn('Запрос отменён:', e.detail.url);
});

Производительность


По умолчанию все значения в PageBlocks хранятся в JSON-формате (в поле values) — это удобно и гибко. Но есть нюанс: фильтрация по JSON-полям не использует индексы, и при большом количестве данных это может заметно замедлить выборку.

Чтобы ускорить фильтрацию, можно воспользоваться компонентом ExtraFields и расширить таблицу нужными полями с индексами.

Варианты улучшения
  • ENUM — хорошо подходит для одиночных значений, например brand.
  • SET — можно использовать для множественного выбора, но: поиск по SET не использует индекс.
  • Связанная таблица — самое производительное и гибкое решение для множественных связей. В PageBlocks есть поддержка таких полей через relationship.

Допустим, товар может находиться в нескольких категориях — это множественное значение. Значит, поле category нужно реализовать как связь many-to-many.

1. Создаём список категорий
Можно сделать это как обычную таблицу, но лучше создать их как ресурсы — вдруг в будущем категории станут полноценными страницами. (Если вы используете контроллеры, то это не имеет значения.)
Tab::make('Categories')
    ->position(1)
    ->fields([
        Field::make('Categories') // name = categories
            ->type('table')
            ->resource()
    ])

2.Добавляем поле category в наши продукты
Field::make('Category')
    ->label('Categories')
    ->type('relationship')
    ->relationType('many_to_many')
    ->primaryTable(\pbResource::class) // наша текущая таблица
    ->relatedTable(\pbResource::class) // таблица категорий
    ->fieldName('categories') // имя поля для получения только категорий
    ->displayField('pagetitle')
    ->valueField('id'),

Больше ничего делать не нужно.
PageBlocks автоматически определит, что это поле типа relationship, сделает JOIN с нужной таблицей и применит условия фильтрации напрямую.

Поддержка


MODX 2 / 3
PHP ^7.4

Заключение


PageBlocks предлагает мощный и гибкий подход к пагинации, фильтрации и сортировке — без лишнего кода, с красивым URL и полной свободой кастомизации.

Всё работает «из коробки» — но при этом легко расширяется:
можно подключать контроллеры, ускорять выборки через ExtraFields, строить связи через `relationship` и контролировать всё с помощью Laravel-подобных маршрутов.

Если вы ещё не используете PageBlocks — сейчас самое время попробовать.
Aleksandr Huz
05 июня 2025, 16:59
modx.pro
1 103
+15
Поблагодарить автора Отправить деньги

Комментарии: 27

Артур Шевченко
05 июня 2025, 20:56
+2
А ещё говорят я компоненты не для новичков пишу))) Вот где хардкор, хотя очень интересно, надеюсь в этом году будет время попробовать.

Есть в планах написать документацию?
    Aleksandr Huz
    06 июня 2025, 11:15
    0
    Документация есть — https://pageblocks.boshnik.com/
    Основная часть материалов уже доступна, остальное скоро будет добавлено.
    Алексей Шумаев
    06 июня 2025, 10:13
    0
    Поясните мне, кто может: что такое pbShop? Тут упоминается, но информации в гугле и заметках автора нет.
    Только ИИ сообщает нечто интересное: disk.yandex.ru/i/sZyQIu2xi7YBXw, хотя сам не знает, где вообще взять pbShop :-)
      Aleksandr Huz
      06 июня 2025, 11:17
      0
      pbShop — интернет-магазин, разрабатываемый на основе PageBlocks
        Артур Шевченко
        06 июня 2025, 20:11
        0
        А что значит на основе Pageblocks? Без minishop'а?
          Aleksandr Huz
          06 июня 2025, 21:03
          0
          да, только Pageblocks
            Алексей Шумаев
            09 июня 2025, 09:11
            0
            Где можно посмотреть информацию? Откуда-то ИИ гугла взял )
              Aleksandr Huz
              09 июня 2025, 09:58
              +1
              Он только разрабатывается. Документации нет.
                Алексей Шумаев
                10 июня 2025, 09:09
                0
                Вероятно ИИ из будущего: disk.yandex.ru/i/BEDdd2ZG9uWunQ
                  Aleksandr Huz
                  10 июня 2025, 11:57
                  0
                  у меня яндекс не работает (даже через впн), я не вижу скрины
                    Sergey (Sentinel)
                    13 июня 2025, 14:34
                    0
                    По моему когда-то такое название было у Evolution, отсюда и история в поисковике
                      Ivan K.
                      13 июня 2025, 14:47
                      0
                      Все проще, ИИ просто выдумывает. Он чтобы дать ответ придумывает, то чего не было.
            Алексей Шумаев
            26 июня 2025, 10:10
            +1
            Магазин в составе единой экосистемы pb очень интересен, особенно для modx3. Нет ли каких-то сроков, когда можно будет «пощупать»? Или пока хотя бы планируемый функционал.
              Aleksandr Huz
              26 июня 2025, 11:01
              +1
              Нет ли каких-то сроков, когда можно будет «пощупать»?
              в планах до 15.07, если все будет ок.

              Или пока хотя бы планируемый функционал.
              Пока, что базовый функционал.
          Miša Bulic
          06 июня 2025, 10:53
          +2
          Крутое обновление, наконец то фильтр на Modx 3 будет.
          Pageblocks растёт такими темпами, что скоро ему и Modx не нужен будет.
            W.H.I.T.E
            06 июня 2025, 12:31
            0
            Если будет интеграция или своя реализация seofilter будет действительно круто!
              Aleksandr Huz
              06 июня 2025, 13:25
              +3
              Не пользовался seofilter, бегло посмотрел, и вот что могу сказать:
              Создаете контроллер для страницы и там уже в зависимости от URL прописываете нужные метатеги и текст.

              Чего нет:
              — статистики, которую тоже можно самому реализовать в контроллере
              — хлебные крошки — не тестировал.

              Если у вас есть реальный проект/пример — с удовольствием напишу подробную статью, как это реализовать через PageBlocks
              Павел Романов
              09 июня 2025, 15:30
              0
              Сорри за оффтоп, вопрос по FastPaginate.
              Он присутствует в репозитории, прекрасно работает, но поддомен с примерами и документацией в настоящее время недоступен.
              Это временная история или компонент более не поддерживается?

              PS кстати, по ExtraFields та же история.
                Aleksandr Huz
                09 июня 2025, 16:32
                0
                FastPaginate действительно больше не будет поддерживаться!

                extrafields.boshnik.com/ — все работает, документация скоро будет обновлена
              Марат
              09 июня 2025, 19:37
              0
              Basket это не та корзина, та — cart.
                Андрей Шевяков
                11 июня 2025, 16:32
                0
                А pbFilters умеет работать с товарами miniShop2 и свойствами товара, которые созданы через msFieldsManager?
                  Aleksandr Huz
                  12 июня 2025, 10:58
                  0
                  Компонент не будет работать с miniShop2 и даже с TV параметрами.
                    Ivan K.
                    12 июня 2025, 14:31
                    +1
                    Очень хороший у вас компонент получился. Еще не пробовал, но думаю скоро попробую его в деле.
                    Очень шустро смотрю фильтрует товары, мне прям очень нравится, скажите не тестировали на большем количестве параметров? Скорость не сильно падает?
                    Я так понимаю pbShop еще в разработке, не подскажите, когда примерно будет готова корзина и оформление заказов? Так сказать, минимальный набор для запуска нового интернет-магазина. А то miniShop3 не понятно, когда появится и еще более призрачно появления компонента для фильтрации товаров для MODX3.
                      Aleksandr Huz
                      12 июня 2025, 14:52
                      +3
                      Спасибо.
                      Скажите не тестировали на большем количестве параметров? Скорость не сильно падает?
                      Пробовал на VPS с базой из 100 000 товаров — работает быстро. Больше всего на скорость влияет подсчет кол-ва, но я его вынес в отдельный запрос. Поэтому, при условии оптимизации базы и наличия индексов, должно хорошо работать и с миллионом.

                      Я так понимаю pbShop еще в разработке
                      в планах до 15.07, если все будет ок.
                        Артем
                        26 августа 2025, 15:32
                        0
                        в планах до 15.07, если все будет ок.
                        есть какие-то подвижки?
                Авторизуйтесь или зарегистрируйтесь, чтобы оставлять комментарии.
                27