Админка магазина на vue.js. Урок 10. Добавляем и удаляем бренды
Продолжаем разбираться с 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
{{ brand.id }} {{ brand.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 в том, чтобы проверить, что запросу разрешен доступ до нашего бекенда.
А это мы разрешили установкой того самого заголовка
Я так и не разобрался, почему этих проблем не было с get и post-запросами. Возможно, это связано с нашей ручной реализацией REST API. Ребят, кто шарит, подскажите в коментах, в чем может быть дело.
Короче, нужно было разбираться с проблемой. Сначала я хотел разрешить кроссдоменные запросы на уровне nginx, но подробные инструкции почему-то не отработали.
Выручил тот же 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 добавим ячейку с кнопкой Удалить и навесим обработчик по ее клику. Вот полный шаблон
{{ brand.id }} {{ brand.title }}
Осталось реализовать метод 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
- Урок 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. Карточка товара
Истории из жизни айти и обсуждение кода.