Наверно все работали с формами и понимают как это сложно. В свое время я смотрел разные решения и одним из лучших был 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 я не создавал кастомные поля (только помогал), но делал это в рамках другого решения. Для многих достаточно записать данные в input и сделать обертку, но если сложность задачи усложняется (надо общаться объектами), то обертки может быть не достаточно.
Для многих это не критично, но стоит понимать, что такая ситуация может возникнуть.
В итоге ваш компонент будет работать с данными, а вы писать простой код. И это стоит делать даже если у вас 1 простая форма (это дело привычки). Логике должно быть все равно, как были получены данные и корректно ли они прошли валидацию, какие там были анимации и интерфейсы.
Да да это очень очевидные вещи! Но многие про это забывают. Хотя разделение/склеивание форм занимает не больше получаса времени.
Если у вас кривой бек или другие небесные силы, которые используют где то full_name, а где то fullName, тогда сочувствую (да да, бывает и такое). Постарайтесь делать конвертацию в 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 (добавили кроп изображения), те компоненты формы могут быть взаимозаменяемыми.
Возможно кто то добавит еще пару дельных советов, я же постарался перечислить основные ошибки и дать рекомендации как такого не допустить.
Источник статьи: https://habr.com/ru/post/569524/
Небольшая оговорка. Я не фанатик 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 (добавили кроп изображения), те компоненты формы могут быть взаимозаменяемыми.
Возможно кто то добавит еще пару дельных советов, я же постарался перечислить основные ошибки и дать рекомендации как такого не допустить.
Источник статьи: https://habr.com/ru/post/569524/