mFilter2 своими руками
Введение
Следует понимать, что mFilter2 из коробки работает на все бабки:
- window.history с четкой обратной связью
- на опыте настраивается за 5 минут (не считая рутины)
- быстро работает
Но можно все перенести в браузер.
Минусы очевидны - тысячи товаров висящие в оперативной памяти и ожидающие потенциальной фильтрации - это бэд, так низзя.
Но если у вас товаров сотенка-другая или вы адепт Offline first и умеете готовить PouchDB или Dexie (локальные базы данных), то даже этой проблемы можно избежать.
Из плюсов - скорость и json. Товары приходят в json, схема данных фильтров тоже. Как бы рисуй-фильтруй - не хочу. Не хочу -)
Можно добавить анимацию фильтрации товаров, чтобы плитки так чух-чух и менялись местами (mixitUp).
Но у всего есть цена, конечно.
Мой частный случай
Мне локальная база не нужна, т.к. в категории товаров не много. Поэтому я поставил виджет отслеживания сколько сжирает js оперативки. И красивый зеленый график мне ни о чем не говорит =)) В режиме инкогнито и на телефоне типа 9 Мб занято. А в обычном режиме на десктопе с работающими всякими расширениями браузера до 150-200 бывает добегает. Но иногда до 11-13 сбрасывается. Короч, странно.
Бэкенд
MODx отдает json, содержащий список товаров, список фильтров и список seo-страниц. Последний список необязательный и состоит из url, title, текстового контента, и значений полей фильтра. Это можно сделать чтобы при фильтрации подменять h1, текст, url. А также при отрисовке для чекбоксов добавить прямую ссылку на страницу. И если бот сподобится фильтр просканировать, что получится передача веса по ссылке. Иначе стоит уповать лишь на sitemap.xml
С небольшими изменениями полученный объект я кладу в стейт от redux. В списке фильтров каждому из них я добавляю поле values с пустым массивом. В него-то мы и будем складывать значения, когда пользовать начнет тыкать по фильтру.
Как движок понимает, что надо отдать json с фильтрами?
Во-первых, я создал ресурс "api" с пустым шаблоном, где fenom мне помогает слушать $.post запросы. И дергает соответствующие сниппеты.
MODX смотрит, что у ресурса есть тв-поле со списком фильтров через запятую и отдавая разметку в шаблоне подготавливает пустой контейнер, куда мы чуть позже будем врисовывать фильтры, запрошенные отдельным запросом ajax
Список фильтров в админке я сделал с помощью migx. Прикрепил поле на главную страницу. В настройках этой tv есть название фильтра, тип (чекбокс, цена, размеры), единица изменения (например Ватты или класс водостойкости) и место расположения единицы изменения - префикс или постфикс. Это нужно, чтобы при отрисовке рядом с чекбоксом была не просто цифра, а еще и юнит, типа ◘ 600 Вт.
Обращаясь к странице за ее товарами через ajax, мы читаем тв-поле с фильтрами, бьем по запятой. Если это не "price" и не "size", то значит опции товара. Запустив сниппет "msProducts" и добавив в запрос опции, мы получаем список товаров с опциями.
Список фильтров и их подробности возьмем из migx главной страницы.
Список seo-страниц вытащим через pdoResources, фильтруя их по шаблону.
Фронтэнд
Получив json от сервера, используя его соотвествующее поле мы рисуем фильтры. Добавляем к ним слушатели-диспетчеры.
Диспатчить они будут в редюсер и в экшн-дата будут передавать название фильтра и значение. Сама фильтрация будет происходить путем накопления в массиве filters фильтрующих функций:
let res = filters.reduce((data, current_filter_function) => {
return data.filter(current_filter_function)
}, state.data)
А потом через reduce мы бегаем по этому массиву и поочередно применяем фильтрующие фукции к списку товаров. Т.о. мы корректно фильтруем с динамическим количеством фильтров.
Итак, элементы фильтра (чекбоксы, инпуты) будут слушать стейт. Если там в поле values появится значение или оно опустеет, то им нужно будет соотвествующим образом отреагировать.
Также помимо диспатичинга в statе, они будут смело пушить window.history.pushState.
И здесь мы работаем с URLSearchParams, методами has(), set(), toString(). Проверяем кол-во параметров и т.д. Потом уже пушим в window.history. Читать значения фильтров из subscribe нельзя, потому что будет неявная циклическая ошибка, когда мы будем слушать popstate. Поэтому только так - диспатчить одно и тоже и в стейт и в window.history
popstate
При использовании стрелок браузера мы перемещаемся по истории браузера. Обычно страница при этом перезагружается, но если мы в историю пушим объект и url, то страница не перезагрузится, а просто сообщит о том, что она могла бы. И это событие мы слушаем через window.addEventListener("popstate", ... ). В поле event.state мы увидим объект состояния, который сами туда и записали ранее.
Поэтому на такое событие мы должны отреагировать и обновить state приложения согласно этому объекту состояния. А на это событие в свою очередь отреагируют фильтры и чекнут чекбоксы или введут значения в инпуты. А также список товаров отфильтруется "сам". Таким образом кликая по стрелкам мы будем включать/выключать чекбоксы и фильтровать товары. Прям как в оригинальном mFilter2.
on page load
У нас может сложиться ситуация, что пользователь обновит страницу или вернется на нее со страницы товара и у него будут в адресной строке браузера параметры - значения наших фильтров. Эту ситуацию надо тоже оследить и сделать 2 вещи: если есть query параметры, то window.history.pushState. Потом с помощью URLSearchParams определить каким образом обновить объект filters в стейте. Таким образом при загрузке страницы мы получим сразу отфильтрованные товары согласно get-параметрам в url.
Фильтр размеров
Желая фильтровать по длине-ширине-высоте и каждое такое поле как 2 инпута "от-до", мы, во-первых, получаем довольно сложный объект в самом стейте, типа:
[
{ "deep": [ 2, null ] },
{ "width": [ 6, null ] },
]
А, во-вторых, его приходится JSON.stringify чтобы хранить как параметр в window.history.
msProducts и json
Известно, что сниппеты MODx отдают просто строку. Далеко не всегда работает параметр return => json. Поэтому строку приходится парсить в объект. И мешается trailing comma. Ниже есть регулярка, которая зарешает:
$output = $modx->runSnippet('msProducts', $array);
$output = preg_replace('/\,(?!\s*?[\{\[\"\'\w]")/, '', $output);
$output = "[".$output."]";
Писать-то словами такую логику долго. А уж писать код и тестить еще дольше. Но получается красиво - фильтры по длине, ширине, высоте отдельными полями "от" - "до". Тоже самое с ценой. И чекбоксы с опциями товара.