Компоненты админки: vesp-modal

Следующий важный компонент админки - модальные окна для создания и редактирования моделей.

Vesp-modal расширяет компонент из BootstrapVue, принимает всего его параметры и добавляет отправку формы с данными.

Редактируемые данные передаются через v-model, и если в них указан первичный ключ (по умолчанию поле id, но можно настроить и другой), то форма отправится методом PATCH (редактирование), а если нет, то PUT (создание).

Содержимое формы вставляется в слот #form-fields, а сами формы я предлагаю хранить отдельно от модальных окон, для удобства. Давайте разберём форму работы с пользователями.

Создание модели

Модальные окна в админке находятся в отдельных маршрутах в директории pages, это даёт возможность переходить сразу по прямым ссылкам на создание и редактирование чего-либо.

Эти маршруты являются доченими по отношению к таблице с моделями, поэтому в шаблоне с таблицей обязательно должен быть тег <nuxt-child /> - именно в него и будет вставлена страница с модалкой.

Так и получается, что список пользователей находится по адресу admin/users, а создание нового пользоветеля admin/users/create.

<template>
  <!--Окну обязательно нужено передать данные и адрес для отправки-->
  <vesp-modal v-model="record" :url="url" :title="$t('models.user.title_one')">
    <!--Вставляем поля формы в соответствующий слот компонента-->
    <template #form-fields>
      <!--И синхронизируем данные между формой и модалкой-->
      <form-user v-model="record" />
    </template>
  </vesp-modal>
</template>
<script>
// Чтобы не дублировать адрес, импортируем его из страницы с таблицей
// Вдруг потом нужено будет его поменять? Проще это сделать в 1 месте
import {url} from '../users'
// Импортируем компонент с полями формы
import FormUser from '@/components/forms/user'

// Экспортируем url снова, для окна редактирования
export {url}
// Основной экспорт
export default {
  components: {FormUser},
  data() {
    return {
      // передаём url в template
      url,
      // и выставляем данные формы по умолчанию
      record: {
        fullname: '',
        password: '',
        active: true,
      },
    }
  },
}
</script>

Вся логика отправки формы находится в модальном окне, поэтому компонент <form-user> содержит только поля формы, и ничего более. Ни тега form, ни индикации загрузки - ничего.

Иначе пришлось бы дублировать функционал отправки в каждой форме, а моей задачей было наоборот, всё упростить.

Смотрим <form-user> в сокращённом виде:

<template>
  <div>
    <!--Я использую компоненты BootstrapVue, но вообще можно псиать обычные-->
    <!--div, input и прочие теги-->
    <b-row>
      <b-col md="6">
        <!--Названия полей мультиязычные по умолчанию-->
        <b-form-group :label="$t('models.user.username')">
          <!--Поля ввода редактируют присланные данные-->
          <b-form-input v-model.trim="record.username" required autofocus />
        </b-form-group>
      </b-col>
      <b-col md="6">
        ...
      </b-col>
    </b-row>
    <b-form-group :label="$t('models.user.fullname')">
      <b-form-input v-model.trim="record.fullname" required />
    </b-form-group>
   ...
  </div>
</template>
<script>
export default {
  name: 'FormUser',
  // Компонент принимает только один параметр - value
  props: {
    value: {
      type: Object,
      required: true,
    },
  },
  // И с помощью этого value мы создаём новый вычисляемый параметр
  computed: {
    // В эту логику сразу трудно въехать, смотрите ниже
    record: {
      get() {
        return this.value
      },
      set(newValue) {
        this.$emit('input', newValue)
      },
    },
  },
  ...
}
</script>

Следите за руками:

  • Данные в модальном окне передаются через v-model, внутри компонент такие данные доступны как value. Это у Vue так придумано, нужно просто запомнить
  • v-model всегда означает, что передаваемые данные должны быть реактивными, то есть когда они меняются внутри компонента, мы это видим снаружи.

Как это делается в Vue? Мы получаем данные в this.value и с их помощью создаём новое вычисляемое свойство record, у которого есть 2 метода: get и set.

Get срабатывает при обращении к полям record, а set - при изменении. Так что, напрямую данные менять нельзя, и это связано с реактивностью работы.

Вместо изменения напрямую, функция set отправляет событие об изменении, которое принимает родительский компонент, меняет данные у себя передаёт уже изменённые данные обратно через v-model.

Честно скажу, я вникал в эту логику примерно неделю, пока не привык и не понял как с этим работает. Это реально сложная парадигма, и если пока не получается понять - то просто заучите, что при передаче данных через v-model в компонент, он не меняет их сам, он только оповещает родительский компонт об изменениях, чтоб тот их изменил у себя.

Именно эта логика гарантирует синхронность данных, когда в модалке v-model используется 2 раза: для самого окна, и для формы. Потому что при изменении чего-то в форме, событие об этом улетает в модалку, та обновляет данные и новые данные выставляются обратно в обоих местах.

Более подробно можно почитать в документации Vue, а мы можем пока отправить форму на сервер. Для этого прописан специальный метод в vesp-modal:

async submit() {
  try {
    // Передаём данные в хук beforeSubmit - он должен проверить их перед отправкой
    // Перед этим убираем реактивность из данных, путём конвертации объекта в 
    // JSON и обратно
    const record = this.beforeSubmit(JSON.parse(JSON.stringify(this.record)))
    if (record) {
      // Если в ответ вместо массива пришла строка - это ошибка проверки, выводим её
      if (typeof record === 'string') {
        this.$toast.error(record)
      } else {
        // Ошибки нет, выставляем флаг загрузки
        this.loading = true
        // Определяем метод отправки: PATCH или PUT
        const method = record.id ? this.actionEdit : this.actionCreate
        const {data} = await this.$axios[method](this.url, record)
        // Даём событие об успешной отправке, на него можно подписаться
        this.$emit('after-submit', data)

        // И даём еще одно, глобальное событие - на него по умолчанию 
        // подписывается таблица с редактируемыми данными
        // Это даёт нам обновление таблицы моделей при работе с ними в модалке
        if (this.updateKey) {
          this.$root.$emit(`app::${this.updateKey}::update`, data)
        }
        // Закрываем окно
        this.hide()
      }
    }
  } catch (err) {
  } finally {
    this.loading = false
  }
},

При закрытии окна срабатывает метод, который возвращает нас на предыдущий адрес, с таблицей моделей. Как видите, всё максимально автоматизировано.

Редактирование модели

Редактирование пользователей находится по адресу admin/users/edit/idюзера и расширяет страницу создания модели.

У него вообще нет тега <template> (хотя его можно и добавить), потому что мы никак не меняем внешний вид компонента. Эта та же формы создания модели, с одним отличием - мы должны ей подсунуть уже созданную ранее модель.

<script>
// Загружаем страницу создания юзера и адрес для запросов из предыдущей модалки
import Create, {url} from '../create'

export default {
  // Указываем, что наш компонент раширяет родительский
  // То есть, он наследует всё, что прописано в UserCreate
  extends: Create,
  // Страница работает только если прислан id юзера
  validate({params}) {
    return /^\d+$/.test(params.id)
  },
  // Эта особенная функция загружает данные до создания компонента
  // и является особенностью Nuxt, как и метод validate
  async asyncData({app, params, error}) {
    try {
      // Получаем данные методом GET из контроллера
      const {data: record} = await app.$axios.get(url + '/' + params.id)
      // Возвращаем массив с данными, которые будут выставлены компоненту
      // Родительский компонент выставлял переменную record, её мы и заменяем
      return {record}
    } catch (e) {
      // Если данные не получаются - ошибка
      error({statusCode: e.statusCode, message: e.data})
    }
  },
}
</script>

Вот так просто мы унаследовали родительский компонент и добавили в него получение начальных данных модели.

Функция asyncData обращается в API и заменяет родительский массив record, указанный в UserCreate по умолчанию:

record: {
  fullname: '',
  password: '',
  active: true,
},

Таким образом форма видит начальные данные, в которых есть id модели и метод форма будет отправляться на этот id методом PATCH. Вместо создания новой модели будет отредактирована существующая.

Заключение

Эта логика выкристаллизовалась сама собой во время разработки многих админок на Vesp. С каждым разом я хотел писать всё меньше кода, поэтому определял общие моменты в работе и выносил их в родительский @vesp/frontend.

Именно таким образом и появились vesp-table и vesp-modal, которые закрывают 99% потребностей в управлении обычными моделями.

Думаю, на следующием уроке мы пробежимся по остальным компонентам админки, они гораздо проще и в основном добавляют какие-то типы ввода, которых нет в BootstrapVue, навроде выбора цвета или ввода телефонных номеров.

← Предыдущая заметка
Компоненты админки: vesp-table
Следующая заметка →
Остальные компоненты админки
Комментарии (8)
bezumkinВасилий Наумкин
14.06.2022 19:09

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

bezumkinВасилий Наумкин
17.02.2023 15:12

Создание маршрута и окошка - да, просто скопировать edit.

Только после получения данных товара в asyncData надо убрать из него id, чтобы при сохранении был создан новый, а не изменён старый.

bezumkinВасилий Наумкин
19.02.2023 03:29

Нет, это проверка параметра маршрута на то, что в параметре id какое-то число.

validate({params}) {
  return /^\d+$/.test(params.id)
},

Потому что, если на страницу передали не id, то запрос на сервер можно и не делать.

bezumkin
Василий Наумкин
09.04.2024 01:45
Ошибка 500 Это не похоже на ошибку Nginx, это скорее всего ошибка PHP - надо смотреть его логи. Во...
futuris
Futuris
04.04.2024 05:56
Я просто немного запутался. Когда в абзаце &quot;Vesp/Core&quot; ты пишешь про &quot;новый trait Fil...
bezumkin
Василий Наумкин
20.03.2024 18:21
Volledig!
Андрей
14.03.2024 10:47
Василий! Как всегда очень круто! Моё почтение!
russelgal
russel gal
09.03.2024 17:17
А этот стоило написать хотя бы затем, чтобы получить комментарий от юзера, который ничего не писал ...
inetlover
Александр Наумов
27.01.2024 00:06
Василий, спасибо! Извини, тупанул.
bezumkin
Василий Наумкин
22.01.2024 04:43
Давай-давай!
bezumkin
Василий Наумкин
24.12.2023 11:26
Спасибо!
bezumkin
Василий Наумкин
27.11.2023 02:43
Ура!
bezumkin
Василий Наумкин
25.11.2023 08:30
Vesp тянет 2 зависимости: vesp-frontent для фронта и vesp-core для бэкенда. Их можно обновлять, но э...