Админка магазина на vue.js. Урок 2. Фильтры и сортировки

декабрь 20 , 2018
Метки:
Предыдущая статья Следующая статья Демо Исходники

В первом уроке админки на 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

Что еще почитать по теме

Предыдущая статья Следующая статья Демо Исходники
Метки:
Заходите в группу в контакте - https://vk.com/webdevkin
Анонсы статей, обсуждения интернет-магазинов, vue, фронтенда, php, гита.
Истории из жизни айти и обсуждение кода.
Как Вам статья? Оцените!