Forms
Systems usually are composed of several pages that revolve around actions performed by filling out forms. These forms can appear in modals or take up the entire page.
Due to the number of forms the project uses, a system was created to automate several aspects of form creation, like:
- Field validations;
- Form validations;
- Buttons;
- Titles;
- Labels;
- Field types;
- Payloads.
INFO
90% of the pages in projects will contain some type of form.
Components
The form system was divided into several components, ranging from fields to a form template. The hierarchy is:
AFormHeader: Called on the form page, usually in full-page forms.FormTemplate: Called on the form page.MFormFooter: Called on the form page, when not using a custom footer.OFieldset: Called within theFormTemplate, used to render each individual field and apply validations.Labels and Fields: Each field is a separate component with its label rendered inOFieldset. Validations and other details are handled in higher-level components.
Usage
New Fields
To implement a new field, create the new component in:
src
│
└───modules
│
└───System
│
└───Form
│
└───componentsThen add it as a new case in composables > useForm, types.ts and constants.ts.
Forms
Once all the fields are created, the form will be implemented as a new view. The form consists of at least 4 files:
- The page / component;
- A helper called
getSanitizedPayload.helper; - A composable called
useFields; - The service(s).
All files must be placed inside the same folder, except for the services, which must be in their own separate folder at the root of the module folder.
The Page / Component
The page will consist of a template calling AFormHeader, TemplateForm, and MFormFooter. In the script, the function to submit the form will always have a similar structure, which includes error validation, payload creation, and calling the service:
<script setup lang="ts">
function onSendForm() {
const hasErrors = fieldsValidation(fieldsSchema.value, currentSchemaModels.value);
if (hasErrors) return (formError.value = true);
formError.value = false;
loading.value = true;
const editableFields = getSanitizedPayloadHelper(currentSchemaModels.value);
const nonEditableFields = {
shelter_id: path.to.this.payload.field
};
const payload = {
...editableFields,
...nonEditableFields,
};
const TOAST_SUCCESS = {
title: 'components.forms.toast.title',
type: TOAST_TYPES.SUCCESS,
};
const TOAST_ERROR = {
message: 'errors.generics.unknownIssue.message',
title: 'errors.generics.unknownIssue.title',
type: TOAST_TYPES.ERROR,
};
FormService(payload)
.then(() => notify(TOAST_SUCCESS))
.catch(() => notify(TOAST_ERROR))
.finally(() => (loading.value = false));
}
</script>useFields
The composable for the form used to fill the form fields, pass its settings, edit labels, options, field types and validations. It will always follow the same format:
import { computed, ref } from 'vue';
import type { FieldInitialSettings, FieldSchema, RawField } from '@FormModule';
import type { GenericField } from '@Types';
import { useForm } from '@FormModule';
export default function () {
const { getDetailsSchema } = useForm();
const fieldsSettings: FieldInitialSettings = {};
const rawFields: RawField[] = [
{
label: '',
name: '',
required: true / false,
type: '',
value: '',
},
];
const currentSchemaModels = ref<Set<GenericField>>(new Set());
const fieldsSchema = ref<FieldSchema[]>([]);
const formError = ref();
function setForm() {
fieldsSchema.value = getDetailsSchema(rawFields.value, fieldsSettings.value);
}
return {
currentSchemaModels,
fieldsSchema,
formError,
rawFields,
setForm,
};
}Depending on the necessity, fieldSettings and rawFields can be refs or computed properties, as some fields can get their values dynamically. In some cases, the form can be pre-filled, in which case we can use a switch case to populate the setForm function:
function setForm(sourceData: any) {
const updatedFieldSchema = getDetailsSchema(rawFields.value, fieldsSettings);
if (!sourceData) return (fieldsSchema.value = updatedFieldSchema);
formError.value = null;
wasFormSubmitted.value = false;
updatedFieldSchema.forEach((field) => {
const match = sourceData[field.name];
switch (field.name) {
default: {
field.value: match;
field.model: match;
}
}
});
fieldsSchema.value = updatedFieldSchema;
}getSanitizedPayload.helper
The useFields file handles the creation of the form and the receipt of the data that populates the form fields. However, sometimes the backend expects different data for certain fields—such as the field name, the value format, or a group of fields inside a specific object. To ensure the payload is sent in the format that the backend expects, we use getSanitizedPayload.helper to sanitize the payload.
A common use case of this helper is:
import type { GenericField } from '@Types';
import type { FieldPayload } from '@FormModule';
import { formatDateToString } from '@Helpers';
import { payloadSanitizerHelper } from '@FormModule';
import moment from 'moment';
function getSanitizedFieldPerName({ name, value }: FieldPayload) {
switch (name) {
case 'exam_date': {
return {
exam_date: moment(value, 'MM/DD/YYYY').toISOString(),
};
}
case 'time_until_followup': {
return {
time_until_followup: value?.duration?.toString(),
followup_unit: value?.duration_unit,
};
}
}
}
export default function (fields: Set<GenericField>) {
let payload: { [key: string]: any } = {};
const fieldNamesToBeSanitezed = ['exam_date', 'time_until_followup'];
fields.forEach((field: GenericField) => {
const NOT_PAYLOAD_FIELDS = ['header', 'seperator', 'paragraph'];
if (NOT_PAYLOAD_FIELDS.includes(field.name)) return;
if (!field.value) return;
const entry = payloadSanitizerHelper(field, fieldNamesToBeSanitezed, getSanitizedFieldPerName);
payload = { ...payload, ...entry };
});
return payload;
}Error Messages
The FormTemplate component accepts a prop called formError. This prop can be a string or a boolean, allowing it to display either a custom error message or the default one. If a custom error message is used, the string can be the i18n path without the t, as the formError prop already handles it within its template.
The error message is displayed by AFormError, typically located in the form footer.
The Form Footer
An important step in implementing a form is using the MFormFooter. This is not only essential for handling validations but also for incorporating the AFormError component.
The AFormError component is teleported from the FormTemplate to any component where the form is used via MFormFooter. This makes it versatile enough to be placed anywhere, such as in a modal or a full-page form.
However, if you decide to use a custom footer for your form and cannot use MFormFooter, you must directly include the AFormError component in your template or pass an empty formError prop to the FormTemplate like this:
<FormTemplate formError="" ... />This step is mandatory when creating a form to prevent teleportation errors.
TL;DR
- When implementing a form, use
MFormFooter. - If you use a custom footer, include
AFormErrorin your template. - To hide the error message (
AFormError), pass an empty string as the formError prop in FormTemplate.
TIP
There are snippets created for several components related to the forms to facilitate their creation.