13 января 2023 г.

mFilter2 своими руками

Введение

Следует понимать, что mFilter2 из коробки работает на все бабки:

Но можно все перенести в браузер.

Минусы очевидны - тысячи товаров висящие в оперативной памяти и ожидающие потенциальной фильтрации - это бэд, так низзя.

Но если у вас товаров сотенка-другая или вы адепт 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."]";

Писать-то словами такую логику долго. А уж писать код и тестить еще дольше. Но получается красиво - фильтры по длине, ширине, высоте отдельными полями "от" - "до". Тоже самое с ценой. И чекбоксы с опциями товара.