Pull to refresh

Vue — рекомендации при работе с формами

Reading time6 min
Views7.8K

Наверно все работали с формами и понимают как это сложно. В свое время я смотрел разные решения и одним из лучших был Vuetify. Сейчас решений стало больше, но все они однотипны (я не буду брать во внимание форм генераторы, тк они должны быть на чем то основаны). В чем то это связано с ограничением самого Vue и его философией. Но для меня все таки странно, что время идет, а прогресса нет. Странно что люди вокруг пытаются меня убедить что это нормально.

Небольшая оговорка. Я не фанатик Vuetify и стараюсь не использовать его совсем, так как в моих проектах он вызывает больше проблем в будущем, чем их решает в начале. Но мне приходиться работать в проектах где он есть и от него никуда не деться (его невозможно , просто взять и выпилить).

Обертки

Начнем с того, что мало кто создает обертки. Действительно зачем когда это и так выглядит нормально.

<v-autocomplete
  auto-select-first
  chips
  clearable
  deletable-chips
  dense
  filled
  multiple
  rounded
  small-chips
  solo
  solo-inverted
></v-autocomplete>

Я конечно утрирую, но обычно 4-5 свойства присутствует всегда и это копипаститься по всем элементам формы. Создайте 5-6 оберток в начале и дополняйте их по мере необходимости (дальше их можно просто копипастить из проекта в проект). Если вам не хочется этого делать - то переопределите стили (они все равно глобальные).

На такое замечание мне обычно отвечают, что в этом нет ничего страшного и это не как не мешает пониманию/написанию кода и если что мы можем просто сделать глобальный реплейс или поправить через стили. Ну ок ....

Создание своих элементов формы

При рассмотрении библиотек, я смотрю на то, как можно сделать свой компонент, который будет интегрирован с Vuetify или другой системой. Увы в документации Vuetify я не видел примеров как создать свое кастомное поле (иногда это бывает очень полезным), но есть другие источники, которые об этом рассказывают.

В рамках Vuetify я не создавал кастомные поля (только помогал), но делал это в рамках другого решения. Для многих достаточно записать данные в input и сделать обертку, но если сложность задачи усложняется (надо общаться объектами), то обертки может быть не достаточно.

Для многих это не критично, но стоит понимать, что такая ситуация может возникнуть.

Выносите формы в отдельный компонент

В основном все пишут в один файл. Рассмотрим на примере страницы с переключателем "хотите заполнить форму Ф1 или Ф2". Вся эта логика будет написана в один файл (логика переключения, логика формы Ф1 и логика формы Ф2). Файл начинает распухать, а логика становиться совсем не прозрачной. Вынеся формы в отдельные файлы, вы пишете чуть больше кода, но код становиться гораздо прозрачней. В дальнейшем некоторый функционал уйдет в общий mixin, который вы будете подключать ко всем своим формам.

В итоге ваш компонент будет работать с данными, а вы писать простой код. И это стоит делать даже если у вас 1 простая форма (это дело привычки). Логике должно быть все равно, как были получены данные и корректно ли они прошли валидацию, какие там были анимации и интерфейсы.

Одна форма для редактирования и добавления.

Начнем с того что форма, должна иметь возможность получить данные, а при их отсутствие использовать свои дефолтные (бывают случаи когда делают 2 практически одинаковые формы, только одна имеет дефолтные значения, а вторая обязательно должна принять эти значения). Если формы добавления и редактирования не отличаются или отличаются не значительно (добавлены/скрыты какие то поля), то не стоит создавать преждевременные клоны. С другой стороны, если вы не понимаете что будет дальше или поддержка универсальной формы начинает превращаться в сплошные if, то стоит разделить.

Да да это очень очевидные вещи! Но многие про это забывают. Хотя разделение/склеивание форм занимает не больше получаса времени.

Отсутствие Submit

В Vuetify отсутствует метод Submit (есть reset/validate). Этот метод нужен для отдачи чистых/сконвертированных данных. Обычно все это делается перед отправкой самих данных, но все таки за это стоило бы отвечать форме. Напомню про mixin для формы, часть логики можно поместить туда

Бесполезное DTO

Старайтесь избежать лишней конвертации данных. Это значит что если вам бек присылает full_name и ожидает full_name, то не надо в форме использовать поле fullName (так как вы к этому привыкли). Это становиться очень накладно, когда у вас более 10 полей. Для 10 полей - вам придется написать 2 функции по 10 строк, отступы между функциями, название функций, вызовы этих функций, обработку ошибок (ошибка в поле full_name => ошибка в поле fullName). Итого порядка +50 строк. Даже если вам не нравиться 2-3 названия, вам все равно придется пройти все шаги (будет ток чуть меньше кода).

Если у вас кривой бек или другие небесные силы, которые используют где то full_name, а где то fullName, тогда сочувствую (да да, бывает и такое). Постарайтесь делать конвертацию в 1 сторону (если это возможно) и наверно стоит ориентироваться на отправляемые данные, те при получении мы конвертируем в структуру которую надо будет отправить.

Формы для разных сущностей не должны пересекаться

Обычно в начале, все формы однотипны, а может даже похожи и хочется использовать одну и туже форму. К примеру есть форма новости и форма акции (и в самом начале они одинаковы). Но это обманчивое ощущение. Лучше сразу сделать 2 разные формы и не вздрагивать когда в новость добавиться 1 поле, потом другое...

Копипаста? да именно так. Не забывайте, что формы в начале очень простые и у вас не будет миллион копий (максимум 5, а обычно это 2-3 копии).

Что взять

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

Рассмотрим простую задачу создания и редактирования списка клиентов. Соответственно будут файлы "PageClientAdd.vue", "PageClientEdit.vue" и "ClientEditForm.vue".

PageClientAdd.vue
<template>
  <div class="container">
    <h1 class="page-title">Добавить пользователя</h1>
    <ClientEditForm ref="clientEditForm" />
    <button @click="save">Сохранить</button>
  </div>
</template>
import ClientEditForm from "./ClientEditForm";

export default {
  name: "PageClientAdd",
  components: {
    ClientEditForm,
  },
  methods: {
    async save() {
      let formData = this.$refs.clientEditForm.formSubmitGetData();
      if(!formData) { return; }
      // Тут логика (в оригинале ее чуть больше)
      RequestManager.Client.addClient(formData).then( (res) => {
        this.$router.push({name: this.$routeName.CLIENT_LIST });
      });
    }
  }
};
PageClientEdit.vue
<template>
  <!-- тут упростим -->
  <div class="container" v-if="client">
    <h1 class="page-title">Редактировать пользователя</h1>
    <ClientEditForm 
       ref="clientEditForm"
       :formData="client"
    />
    <button @click="save">Сохранить</button>
  </div>
</template>
import ClientEditForm from "./ClientEditForm";

export default {
  name: "PageClientEdit",
  props: { clientId: String },
  data() {
    return {
      client: false
    }
  },
  components: {
    ClientEditForm,
  },
  methods: {
    async save() {
      let formData = this.$refs.clientEditForm.formSubmitGetData();
      if(!formData) { return; }
      
      RequestManager.Client.updateClientById({
        id     : this.clientId,
        client : formData
      }).then( (res) => {
        this.$router.push({name: this.$routeName.CLIENT_LIST });
      });
    }
  },
  mounted() {
    RequestManager.Client.getClientById({
      id: this.clientId
    }).then((client) => {
      this.client = client
    });
};
ClientEditForm.vue
<template>
  <form>
    <fieldset>
      <legend>Данные для входа</legend>
      <FvePhone
        label="Номер телефона"
        name="mobile"
        required
        v-model="form.mobile"
      />
      <FveEmail
        label="E-mail"
        name="email"
        v-model="form.email"
      />
    </fieldset>

    <fieldset>
      <legend>Личные данные</legend>
      <FveFileImageCropperPreview
        label=""
        name="avatar"
        v-model="form.avatar"
      />
      <FveText
        label="ФИО"
        name="name"
        required
        v-model="form.fio"
      />
      <FveDatePicker
        label="Дата рождения"
        name="birthday"
        v-model="form.birthday"
      />
      <FveTextarea
        label="О себе"
        name="about"
        v-model="form.about"
      />
    </fieldset>
  </form>
</template>
// import полей будет опущен (будем считать что они глобальные)
import FveFormMixin from "FveFormMixin";

export default {
  mixins: [
    FveFormMixin
  ],
  // components: { FveText, FveEmail, FvePhone, ... },
  methods: {
    formSchema() {
      return {
        mobile       : { type: String, default: () => { return ''; } },
        email        : { type: String, default: () => { return ''; } },
        // это кастомное поле которое общается классом FileClass
        avatar       : { type: FileClass, default: () => { return null; } },
        fio          : { type: String, default: () => { return ''; } },
        birthday     : { type: String, default: () => { return ''; } },
        about        : { type: String, default: () => { return ''; } },
      };
    }
  },
};

Это мой не идеальный идеал. Возможно кто то не поверит, что это вообще работает. Кто то скажет что FveFormMixin - заточен под эту форму. От себя скажу, что у меня так работают любые формы и да там происходит отправка изображения на сервер. Изначально было FveFileImagePreview, но потом его заменили на FveFileImageCropperPreview (добавили кроп изображения), те компоненты формы могут быть взаимозаменяемыми.

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

Tags:
Hubs:
Total votes 7: ↑7 and ↓0+7
Comments16

Articles