Админка магазина на vue.js. Урок 10. Добавляем и удаляем бренды

август 1 , 2019
Метки:
Предыдущая статья Следующая статья Исходники

Продолжаем разбираться с vue.js. Мы на первом же уроке научились считывать и отображать данные из базы, пора разобраться, как ими управлять. Сегодня мы будем работать с брендами и посмотрим, как их добавлять и удалять. REST API под это давно написан, сегодня исключительно фронтенд и vue.

Почему мы начинаем с брендов, а не с самого интересного, с товаров? Бренды и категории это самые простые сущности нашей админки. Обе состоят состоят из двух полей, id и название title. Поэтому разобраться с ними проще, чем сразу с товарами. Давайте сначала сделаем самое простое, а до товаров доберемся уже подготовленными. Сегодня реализуем добавление и удаление брендов, а редактированием займемся чуть позже. Для категорий код напишем по аналогии, а потом приступим к вещам поинтереснее, к товарам.

Итак, начнем.


Выводим список брендов

Вспомним, что сейчас на вкладке брендов просто выводится json через тег pre.

Давайте заменим это на привычную таблицу. Откроем компонент AppBrands.vue. Вместо старого кода с тегом pre

    {{ brands }}

выведем простую таблицу из двух колонок: id и Бренд

    
Id Бренд

Заметим, что для вывода каждого бренда мы используем отдельный компонент BrandsItem. Создадим его чуть позже, а сейчас давайте сразу импортируем его в теге script и добавим в раздел components. Получится так

    import BrandsItem from './BrandsItem';

    export default {
        components: {
            BrandsItem
        },
        computed: {
            brands () {
                return this.$store.state.brands.all;
            }
        }
    }

Дальше создадим сам компонент BrandsItem. Он очень простой, всего лишь вывод id и title

    
    
    

Смотрим, что получилось

Отлично, можно переходить к добавлению брендов


Добавляем бренды

Как это реализовать в интерфейсе? Варианты есть разные, но у нас простейший случай, когда для добавления нужно ввести только название бренда. Поэтому предлагаю обойтись модальным окном. Перед таблицей-списком брендов поставим кнопку Добавить, а при нажатии на нее выведем окошко с одним инпутом - название бренда. И кнопки Добавить/Отмена в окне. Годится.

Напоминаю, что мы подключили css-фреймворк minicss, поэтому изобретать окошко сами не будем. Возьмем код из документации фреймворка - https://minicss.org/docs#modal-dialogs

    
    
    
    

Modal

This is a modal dialog!

label - это кнопка, при клике на которую появляется модалка, секция div - это сама модалка с кнопкой-крестиком закрытия, заголовком h3 и p class="section", куда мы и добавим все, что нам нужно. input type="checkbox" непонятно зачем. Он скрыт и используется, как флаг видимости модалки, но да ладно. Вот будем писать свои окошки, сделаем, как хотим, а пока просто скопируем код.

Кнопку Добавить бренд и модалку мы вынесем в отдельный компонент BrandsNew.vue. А в AppBrands будем его просто использовать. Добавим его перед таблицей брендов, сразу после заголовка h1 Бренды

    ...
    
    ...

Не забываем его импортировать и добавить в секцию components

    import BrandsNew from './BrandsNew';
    
    components: {
        ...
        BrandsNew
    }

Теперь создаем компонент BrandsNew. Сначала шаблон

    

Написано много, но половина кода - это копипаста из примера выше. В чем разница? К label добавлены role="button" и class="tertiary", чтобы метка стала зеленой кнопкой. К инпуту-чекбоксу добавлен v-model="visible". Это нужно, чтобы управлять видимостью модалки руками. Если бы у нас было информационное окошко, то хватило бы и дефолтного кода фреймворка, но у нас в модалке какая-никакая логика, удобнее завести отдельное поле visible.

В p class="section" нужное содержимое: текстовый инпут с v-model="newBrand" и две кнопки, Добавить и Отмена. На кнопки навешаны события, соответственно, addBrand и closeModal.

Дальше интереснее, пишем javascript часть компонента

    export default {
        name: "BrandsNew",
        data () {
            return {
                visible: false,
                newBrand: ''
            }
        },
        methods: {
            addBrand () {
                this.$store.dispatch('brands/addBrand', this.newBrand);
                this.newBrand = '';
                this.closeModal();
            },
            closeModal () {
                this.visible = false;
            }
        }
    }

Разбираем. Имя BrandsNew стандартно, в data 2 поля, видимость visible и newBrand, мы уже знаем, для чего они. И два метода. addBrand вызывает действие action brands/addBrand, очищает поле инпута и закрывает модалку. А closeModal - это метод закрытия модалки, который выставляет свойство visible в false. Все, в логике работы здесь больше ничего не требуется.

Добавим немного стилей, чтобы выглядело симпатичнее. Это секция style компонента

    

Получилось вот так

С компонентом закончили, осталось отправить запрос на сервер и добавить новый бренд в хранилище store. Открываем store/modules/brands.js и пишем код для действия addBrand

    addBrand (context, newBrand) {
        const data = 'title=' + newBrand;

        axios
            .post('/admin/api/v1/brands', data)
            .then(response => {
                context.commit('ADD_BRAND', response.data)
            });
    }

Это очень похоже на получение брендов getBrands, только вместо get запрос post и в теле запроса нужно передать заголовок title. И дальше мы будем все запросы прогонять по такой же схеме. То есть отправляем axios get/post/delete/put, если нужно, добавляем данные в data, дожидаемся ответа от сервера и вызываем нужную мутацию через context.commit

Давайте добавим код мутации - это последнее, чтобы добавление брендов нормально заработало.

    ADD_BRAND (state, brand) {
        state.all.push(brand);
    }

Совсем просто, всего лишь добавляем объект бренда в store в поле all. А объект brand берем из ответа сервера при вызове axios.post('/admin/api/v1/brands', data). Там вернется новый id и указанный нами заголовок бренда, благо что мы это реализовали в уроке REST API для админки магазина.

Теперь можно пробовать и сколько угодно создавать новые бренды. Работает. Пора приступать и к удалению, здесь-то все должно быть совершенно аналогично.

Но есть одна тонкость.


Кроссдоменные запросы DELETE

Я уже писал статьи и по кроссдоменным запросам, и по REST сервису на нативном php. Думал, что уже разобрался в этой теме. Ага. Не совсем.

Напомню, сейчас у нас npm run dev запускает админку на localhost:9000, а апишные запросы на бекенд отправляются на отдельный хост. У меня полный путь такой - http://w-shop.lc/admin/api/v1/. Чтобы разрешить кроссдоменные запросы, мы давно поставили заголовок прямо в admin/api/v1/index.php

    header('Access-Control-Allow-Origin: *');

Это нужно только для режима разработки, в продакшене это ни к чему. Так вот, это прекрасно работает с get и post-запросами и напрочь отказывается с delete. При попытке отправить с localhost:9000 запрос на удаление бренда delete http://w-shop.lc/admin/api/v1/brands/5 я получал в ответ 400 код с таким json-ом

    {
        "code": "invalid_parameters",
        "message": "invalid parameters"
    } 

Этот json мы сделали сами еще в 3 уроке, чтобы пропускать только нужные GET, POST, PUT, DELETE запросы с правильными параметрами. Но сейчас на бекенд приходил запрос не DELETE, а OPTIONS. OPTIONS под список разрешенных методов не попадал и код brands.php отдавал 400.

Хитрость в том, что запрос OPTIONS вообще не должен доходить до этого кода. Задача OPTIONS в том, чтобы проверить, что запросу разрешен доступ до нашего бекенда. А это мы разрешили установкой того самого заголовка Access-Control-Allow-Origin: *

Я так и не разобрался, почему этих проблем не было с get и post-запросами. Возможно, это связано с нашей ручной реализацией REST API. Ребят, кто шарит, подскажите в коментах, в чем может быть дело.

Короче, нужно было разбираться с проблемой. Сначала я хотел разрешить кроссдоменные запросы на уровне nginx, но подробные инструкции почему-то не отработали. Выручил тот же php. Идея фикса в том, чтобы прокидывать заголовок Access-Control-Allow-Methods: GET, POST, PUT, DELETE, PATCH, OPTIONS, но не всегда, а если дергается метод OPTIONS. В коде index.php это выглядит так

    header('Access-Control-Allow-Origin: *');
    if ($_SERVER['REQUEST_METHOD'] === 'OPTIONS') {
        header("Access-Control-Allow-Methods: GET, POST, PUT, DELETE, PATCH, OPTIONS");
        exit(0);
    }

После этого все стало работать замечательно и наконец можно было вернуться к фронтенду.


Реализуем удаление брендов

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

Для этого сначала нужно добавить новую колонку в таблицу брендов. В AppBrands.vue добавляем третий th, пустой, чтобы получилось так

    Id
    Бренд
    

P.S. Боже, как меня бесит этот убогий парсер кода, который сжирает теги!!!!! А ставить новый прям накладно, полсайта придется перелопатить ((
Благо что в исходниках все равно есть нормальный код

Ладно, идем дальше. В компоненте BrandsItem.vue добавим ячейку с кнопкой Удалить и навесим обработчик по ее клику. Вот полный шаблон

    

Осталось реализовать метод remove. Это в разделе methods того же компонента

    methods: {
        remove (id) {
            this.$store.dispatch('brands/removeBrand', id);
        }
    }

Как видим, все происходит по одной схеме - вызывается действие action с нужным параметром.

Теперь идем в хранилище и реализуем action removeBrand. Файл store/modules/brands.js. Сначала добавим действие в разделе actions

    removeBrand (context, brandId) {
        axios
            .delete('/admin/api/v1/brands/' + brandId)
            .then(response => {
                context.commit('REMOVE_BRAND', response.data.id)
            });
    }

И мутацию в mutations

    REMOVE_BRAND (state, brandId) {
        state.all = state.all.filter(brand => brand.id !== brandId);
    }

Это фильтр, который возвращает ту же коллекцию брендов, но без удаленного id. Теперь все, удаление брендов работает.


Что в следующем уроке?

До сего дня мы писали запросы на сервер и обработку ответов так, будто у нас все и всегда будет хорошо. В жизни так не бывает. То бекенд недоступен, то пользователи вводят странные данные, а иногда мы банально ошибаемся в строке запроса. Все же люди.

В серверном коде у нас уже предусматривается обработка таких случаев. Мы выбрасываем 400 ошибку с нужными кодами, когда не найден роутер или, например, пользователь пытается добавить уже существующий бренд. Обычно я не рассматривал такие случаи, чтобы не усложнять код. Но в уроках админки решил сделать исключение. Может, это окажется интересно или полезно. Поэтому следующий урок будет посвящен обработке ошибок на клиенте и сервере. Как их перехватывать, обрабатывать и показывать пользователям.

До встречи.

Все уроки админки на vue.js

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