Админка магазина на vue.js. Урок 2. Фильтры и сортировки
В первом уроке админки на vue мы вывели на страницу товары и реализовали поиск. Сегодня прикрутим к списку товаров несколько фильтров и сортировок. Это будут фильтры по категориям, брендам, ценам и сортировки по рейтингу и ценам. Главная цель статьи - показать, что на vue делать такие вещи быстро и удобно.
Немного es6
Сначала займемся небольшими правками. Мы уже использовали возможности es6, когда писали многострочный шаблон компонента ProductItem. А сейчас сделаем чуть больше. Вспомним код получения отфильтрованных товаров
filteredProducts: function() { var that = this; return this.products.filter(function(product) { return product.good.toLowerCase().indexOf(that.inputSearch.toLowerCase()) !== -1; }); }
Давайте используем стрелочные функции, тем самым сократив запись и убрав некрасивый var that = this;
filteredProducts: function() { return this.products.filter(product => { return product.good.toLowerCase().indexOf(this.inputSearch.toLowerCase()) !== -1; }); }
Так выглядит короче и симпатичнее. В этом уроке мы будем часто использовать колбэки. Без стрелочных функций, у которых нет своего контекста, var that пришлось бы использовать часто. А так код будет читабельнее.
Фильтры по категориям
Когда мы делали фильтры в интернет-магазине, то поленились реализовать отдачу категорий с бекенда. Не хочется во фронтовый урок добавлять куски php-кода, поэтому сделаем так. Забьем список из трех категорий руками (а в следующих уроках это исправим). Это будет коллекция categories. Плюс по аналогии с inputSearch нам нужно поле, которое хранит выбранную категорию - selectCategory. Дополним объект data таким образом
data: { products: [], categories: [ { id: 1, category: 'Ноутбуки' }, { id: 2, category: 'Смартфоны' }, { id: 3, category: 'Видеокарты' } ], inputSearch: '', selectCategory: 0 }
Айдишники и названия категорий взяты из базы данных. Если вы ее еще не развернули, самое время это сделать. Здесь описана структура базы и там же приложены исходники.
Данные готовы, теперь нужно отобразить выпадающий список категорий на странице. В index.html между заголовком h1 и инпутом поиска напишем так
Если Вы разбирали предыдущий урок, то сюрпризов не увидите. Html-овский select связывается с полем selectCategory через атрибут v-model. Дальше в option-ах отображается список категорий. Сначала будет дефолтное значение 0 - все категории. А дальше в цикле выводим те категории, что записали чуть раньше в data.categories.
Если сейчас обновите страницу, то увидите, что селект со списком категорий появился, но при выборе ничего не происходит. Чтобы заработала фильтрация, нужно немного дописать filteredProducts. У нас было так
filteredProducts: function() { return this.products.filter(product => { return product.good.toLowerCase().indexOf(this.inputSearch.toLowerCase()) !== -1; }); }
А станет так, с учетом фильтра по категории
filteredProducts: function() { return this.products // Фильтруем по категории .filter(product => { return this.selectCategory == 0 || product.category_id == this.selectCategory; }) // Фильтруем по полю поиска .filter(product => { return this.inputSearch == '' || product.good.toLowerCase().indexOf(this.inputSearch.toLowerCase()) !== -1; }); }
Сначала смотрим, если this.selectCategory == 0, то сразу возвращается true, то есть под фильтр попадают все товары. Если же категория выбрана (не равна нулю), то отбираем только те товары, у которых совпадает category_id.
Вот теперь фильтрация работает, можно смотреть. А дальше мы займемся брендами.
Фильтры по брендам
Для брендов фильтрация происходит почти так же, как и в категориях. Отличие только в первоначальном наборе данных. Категории мы забивали руками, но бренды можно получить с сервера.
В первом уроке мы получали список товаров запросом https://shop.webdevkin.ru/scripts/catalog.php. В уроках по фильтрам для интернет-магазина мы добавили get-параметр, который позволял вытаскивать в этом запросе еще и список брендов. Это параметр needs_data=brands.
Посмотрите запрос https://shop.webdevkin.ru/scripts/catalog.php?needs_data=brands и убедитесь, что кроме товаров появился и массив брендов. Нам это очень удобно, так как не нужно забивать список брендов руками.
Сначала в коде в get-запросе axios добавим нужный параметр. Получится
axios.get('/scripts/catalog.php?needs_data=brands')
Теперь заводим новые поля в data: brands: [] и selectBrand = 0, по аналогии с категориями. Дальше нужно полученный с сервера массив брендов передать в поле brands. Это делается в axios.then после получения товаров. Добавим одну строчку this.brands = response.data.data.brands, чтобы весь mounted получился такой
mounted: function() { axios .get('/scripts/catalog.php?needs_data=brands') .then(response => { this.products = response.data.data.goods; this.brands = response.data.data.brands; }); },
Затем выводим список брендов в html, сразу после списка категорий
Все ровно то же самое, как и в категориях. Осталось учесть бренды при фильтрации в filteredProducts
filteredProducts: function() { return this.products // Фильтруем по категории .filter(product => { return this.selectCategory == 0 || product.category_id == this.selectCategory; }) // Фильтруем по брендам .filter(product => { return this.selectBrand == 0 || product.brand == this.selectBrand; }) // Фильтруем по полю поиска .filter(product => { return this.inputSearch == '' || product.good.toLowerCase().indexOf(this.inputSearch.toLowerCase()) !== -1; }); }
Вот. Чувствуете, даже становится скучно. Несколько строчек кода и готов новый фильтр. Но пора взбодриться, следующий фильтр будет интереснее и кода чуть больше.
Фильтры по ценам
Идея такая: в интерфейсе заведем 2 инпута с типом number, в которые запишем минимальную и максимальную цену товаров. Для этого нам понадобятся 2 поля - minPrice: 0 и maxPrice: 0. Добавим их в data. Нули - это просто значения по умолчанию, чуть позже мы их обновим. Для обновления нам нужно уметь вычислять эти цены по имеющимся товарам. Чтобы было удобнее работать с такими вычислениями, добавим в проект библиотеку lodash. В index.html после подключения axios добавим https://cdnjs.cloudflare.com/ajax/libs/lodash.js/4.17.11/lodash.min.js
Напишем функции получения минимальной и максимальной цены. Это делается в новом разделе methods
data: {...}, computed: {...}, mounted: {...}, methods: { getMinPrice: function() { return Number(_.minBy(this.products, 'price').price); }, getMaxPrice: function() { return Number(_.maxBy(this.products, 'price').price); } }
lodash-евские minBy и maxBy удобнее рукопашной реализации. Осталось применить эти функции и записать в minPrice и maxPrice вместо нулей реальные значения. В axios.then добавим 2 строки
this.minPrice = this.getMinPrice(); this.maxPrice = this.getMaxPrice();
В index.html после селекта брендов пишем
Как и договаривались, 2 инпута с type=number. Обратите внимание, value не указываем. Когда связываем инпут с полем из data через v-model, атрибут value игнорируется. Vue сам разберется, как обновлять значение инпута. Нам важно помнить только, что в случае с v-model value инпута всегда равен какому-то полю из data.
В примере у v-model есть модификатор .number. Он означает, что значение minPrice и maxPrice будут числовыми. Подстраховка.
Так, инпуты вывели, первоначальные значения подготовили, осталось учесть цены в фильтрации. Это несложно, добавим в filteredProducts еще один блок
filteredProducts: function() { return this.products // Фильтруем по категории .filter(product => { return this.selectCategory == 0 || product.category_id == this.selectCategory; }) // Фильтруем по брендам .filter(product => { return this.selectBrand == 0 || product.brand == this.selectBrand; }) // По ценам .filter(product => { return Number(product.price) >= this.minPrice && Number(product.price) <= this.maxPrice; }) // Фильтруем по полю поиска .filter(product => { return this.inputSearch == '' || product.good.toLowerCase().indexOf(this.inputSearch.toLowerCase()) !== -1; }); }
Все, с фильтрами мы закончили. Можно теперь фильтровать сколько угодно, пробуя разные комбинации. И переходить к сортировкам.
Сортировки по id, ценам и рейтингу
Сортировки мы сделаем немного иначе. Реализуем их не по отдельности, а все разом.
Вот такие 4 сортировки:
- - по порядку, то есть по возрастанию id
- - по популярности, то есть по убыванию рейтинга
- - по возрастанию цены
- - по убыванию цены
Давайте подумаем, как это сделать обобщенно, а не рассматривать каждое поле по отдельности. Почти любая сортировка делается по двум критериям: название поля и направление (возрастание/убывание). Например, чтобы отсортировать товары по возрастанию цены, нужно знать поле price и направление asc. А сортировка по популярности означает поле rating и направление desc. По порядку будет так: good_id и asc. А по убыванию цены: price и desc.
Давайте договоримся сделать сортировки для этих четырех типов. Кроме полей и направлений нужны еще человеческие названия, которые мы покажем в селекте выбора сортировки. У меня получился такой массив параметров сортировок sortRules
data: { ... sortRules: [ { key: 'good_id:asc', title: 'По порядку' }, { key: 'rating:desc', title: 'По рейтингу' }, { key: 'price:asc', title: 'По цене, сначала дешевые' }, { key: 'price:desc', title: 'По цене, сначала дорогие' } ], ... }
Я объединил в key и поле, и направление. Это для того, чтобы было удобно положить key в значения select-а в html. А уж разбить строку по двоеточию и понять, где какое поле и направление, мы сможем.
Осталось реализовать. sortRules в data уже есть, добавим еще selectSort: 'good_id:asc' - по умолчанию будет сортировка по порядку. Теперь идем в index.html и после inputSearch пишем привычный select
Возвращаемся в app.js. Прежде чем добавлять сортировку в filteredProducts, подумаем вот о чем. Нам не хочется каждый раз вспоминать, где у нас ключ сортировки, а где направление. Поэтому добавим 2 вычисляемых поля, которые будут парсить selectSort и возвращать нужное
sortKey: function() { return this.selectSort.split(':')[0]; }, sortDir: function() { return this.selectSort.split(':')[1]; }
sortKey - это то, что до двоеточия, а sortDir - после. Все согласно конфигу sortRules. И вот теперь можно править filteredProducts. У меня получилось так
filteredProducts: function() { // Фильтруем товары var filtered = this.products // По категории .filter(product => { return this.selectCategory == 0 || product.category_id == this.selectCategory; }) // По брендам .filter(product => { return this.selectBrand == 0 || product.brand == this.selectBrand; }) // По ценам .filter(product => { return Number(product.price) >= this.minPrice && Number(product.price) <= this.maxPrice; }) // По полю поиска .filter(product => { return this.inputSearch == '' || product.good.toLowerCase().indexOf(this.inputSearch.toLowerCase()) !== -1; }); // Сортируем var sorted = _.sortBy(filtered, product => { return Number(product[this.sortKey]); }); // При необходимости сортируем в обратном направлении if (this.sortDir === 'desc') { sorted = sorted.reverse(); } return sorted; }
Изменения такие: сначала вынесли в переменную filtered все, что до этого сразу возвращали в return. Нам нужно это промежуточное состояние, потому что дальше эти данные еще сортировать. Первый этап сортировки такой
// Сортируем var sorted = _.sortBy(filtered, product => { return Number(product[this.sortKey]); });
Обратите внимание на Number(...). Здесь мы сделали себе поблажку - сортируемые поля у нас только числовые. Если Вам нужны еще и строки, то придется проверять на тип данных или задавать этот тип в конфиге sortRules.
В sorted у нас попал массив, отсортированный по возрастанию. Если же выбрано обратное направление, то отработает такой код
// При необходимости сортируем в обратном направлении if (this.sortDir === 'desc') { sorted = sorted.reverse(); }
То есть просто переворачиваем массив.
С сортировкой все. Давайте добавим еще один небольшой штрих - кнопку сброса всех фильтров и сортировок. А заодно посмотрим, как работать с методами на vue.
Сброс фильтров и сортировок
Добавим в index.html после всех фильтров кнопку
Даже не зная vue, легко догадаться, что при клике на кнопку сработает событие clear. Не нужно лезть в js-код и выискивать навешивание событий - все видно по разметке.
Осталось реализовать метод clear. В нем мы просто сбрасываем все фильтры и сортировки в дефолтные
methods: { ... clear: function() { this.inputSearch = ''; this.selectCategory = 0; this.selectBrand = 0; this.minPrice = this.getMinPrice(); this.maxPrice = this.getMaxPrice(); this.selectSort = 'good_id:asc'; } }
То есть вернули все обратно. Больше ничего не нужно, vue увидит изменения и отфильтрует-отсортирует данные заново.
На этом все, второй урок завершен. Я вспоминаю, как мы фильтровали и сортировали товары в серии уроков по самому магазину. Было сложнее. Да, мы гоняли запросы на бекенд, там собирали sql, выполняли запросы и возвращали данные обратно клиенту. Но даже фронтовая часть была тяжелее. С vue мы пишем гораздо меньше кода. И больше думаем о данных, а не о кнопочках и селектах.
Вы можете заметить, что плохо тащить все данные на клиент - нужно получать их с бекенда порциями. Я тоже считаю, что не нужно это делать для всех пользователей - в реальном магазине. А вот в админке вполне допустимо. Если у Вас в магазине пара тысяч товаров, то Вы не заметите разницы. Разве только в том, что данные, хранящиеся на клиенте, фильтруются и сортируются моментально, в отличие от отправки запросов на сервер.
А если у вас серьезный магазин с десятком тысяч товаров, то скорее всего он на Битриксе или чем-то подобном. Впрочем, и 10 тысяч товаров переварить несложно. В следующих уроках, когда будем работать с категориями, мы чуть изменим схему - будем загружать товары на клиент при смене категории. Таким образом большие объемы данных сократятся таким нехитрым способом. Но это будет потом, а пока я заканчиваю и начинаю работать над следующим уроком.
Нужные ссылки:
Демо второго урока
Исходники урока
Все уроки админки на vue.js
- Урок 1. Список товаров
- Урок 2. Фильтры и сортировки
- Урок 3. Новое REST API на чистом PHP
- Урок 4. Правим клиентский код под новое REST API и находим багу
- Урок 5. Разбиваем приложение на компоненты
- Урок 6. Инструмент vue-cli и vue-компоненты
- Урок 7. Flux и Vuex - общие вопросы
- Урок 8. Vuex на практике
- Урок 9. Перерабатываем фильтры
- Урок 10. Добавляем и удаляем бренды
- Урок 11. Обрабатываем ошибки на клиенте и сервере
- Урок 12. Редактируем бренды
- Урок 13. Роутинг
- Урок 14. Карточка товара
Что еще почитать по теме
Истории из жизни айти и обсуждение кода.