circuit

How to Build Scalable Form Factory By Vue 3 — Form Builder Pattern




Photo on  freepik

Photo on freepik

Form almost appears in every application, but making form is taking a lot of effort such as error handling, reactivity, validation… and we will repeat the whole process when we have a new feature with form inside. To solve this problem, I will introduce a different way to build a form factory component. We need to declare a blueprint — a piece of information to tell what kind of component and some extra metadata need to render for that form. This solution is very completed and I think it will cover most of the case we have for a different type of form.

First, I will show some example that I use my FormBuild Component. Sorry for my ugly CSS as I only focus on functionality on this topic. For example, there are other specific considerations for PDF form design best practices.

Live example can be found at: http://form-builder-vue3.s3-website-ap-southeast-2.amazonaws.com/

Example 1: Simple login form

Example 2: Dynamic address form

The default field for all country will be buildingName, streetName but each country will display a different field. For example, Australia will have a postal code but not in Istanbul.

It means other fields will be correlated with each other, and it is not limited to the field visible or not, also it can be validation, props, component type… or other attributes you want.

Example 3: Dynamic document selector

Each country will have a different list of document, choose each document will display a different collection of field.

Passport is the default for all country

Australia has extra Medicare

NewZealand has an extra Driver Licence

After selecting the document type, it shows the collection of fields for that document type.

Alright, I think all the example above is complexed enough. Now we start to build the FormBuilder. This component will read the data from the blueprint and build the form we want.

Blueprint:

Because we want our form to be flexible, dynamic and correlated to each other so we will write a blueprint in javascript, better is Typescript as it is very helpful for type suggestion.

The blueprint will have a type of IBlueprint<typeof FormData>:

  • Path: is the object path of the field from formData object pass to the form builder.

For example: you pass a form data to form builder:

formData = {form: {passport: {idNumber: null}}

So if you want to create a form field to map again the idNumber, the key will be form.passport.idNumber.

Props inside path will be:

  • Value: it is formData , we pass it to blueprint that allow each field can dynamic change again the formData.
  • fieldValue: field value is the value for specific field from formData. It similar to _.get(fieldPath, formData)
  • component(): is a function to return component used for the field, it will be your custom Vue component will take value as a prop and $emit event input on input change.
  • props(): is a function to return props that will be passed to the component we define above. It will automatically pass the fieldValue to the component, but if you want to modify the props pass to the component, you can add value: customValue into the object.
  • visibility(): is a function to return the field is visible or not.
  • validation(): is a function to return the constraints for the field you want to validate. I use validate js so it needs to follow the format of that library. For example, if it is an email, it should return {email: true}
  • required(): is a function return the field is required or not.
  • width(): is a function to return width for the field.

Note: all the property of the blueprint can dynamic access to the formData , so we can make this field behave differently base on the formData . For example:

idNumber: {
 visibility(){
   return this.value.country === 'AUS'
 }
}

Here is the example of complex address form:

Country is a dropdown and the value has to have a length of 3. State and postal code only display when the country is Australia.

Form Builder

Now we know what is the blueprint look like, we will start building the FormBuilder:

  1. Template:

It will be the list of dynamic components from childNodes which we will convert from the blueprint.

childNodes will have interface:

export type IChildNode = {
	component: Component;
	visibility: boolean;
	width: string;
	props: {
		value?: string | null;
		options?: IOption[];
		error: string[] | boolean;
		[key: string]: any;
	};
};
  1. Convert blueprint to childNodes:

ChildNodes is a getter that transforms the blueprint(be pass as a prop from the parent) to an array of IChildNode. Because it is a getter so every time props are changing, the whole form field will be re-render base on new props.
Also, we need to give the blueprint the context, in another word, to make the blueprint can access the formData. Beside that, we can pass other utils function or value we want to pass to the blueprint that allows blueprint can do like this.helperFunction(), this.uppercaseValue.

And don’t forget to filter out the field that has visibility false.

  1. Handle event:

We are passing the field path to all the dynamic component as a props, so when the component emit the event(such as VInput, VDropdown, VRadio), it needs to come with the fieldPath, that let us know what field is changing.

Important note: for all the custom input component, you need to take a props name: string , value: any and emit event input with payload $emit('input', {name, value})

With the helper of lodash, we update the value of formData base on the fieldPath and new value. Then we emit the formData to parent.

Note: for UX thing, what field is updating, we should clear the error for that specific field that let user know the error is re-calculate.

  1. Error handling:

As we already define all the constraints for all the fields the blueprint, now we can gather everything into single constraints and use validatejs to validate the formData.

Then when we have all the error, we can emit it to the parent.

How to use FormBuilder

In my example, I use composition API to bundle my logic for every form. Basically, for every formBuilder, we need to pass:

props: {
 blueprint: Blueprint;
 error: IError;
 value: FormData;
 validateOnChange?: Boolean
}

With the form validation, we have 2 options:

  • Validate on demand from wrapper, like you want to trigger the validation when use click the button Submit, but the Submit button stay outside the formBuilder. We can archive it by using ref: this.ref.validate().
  • Validate on input change: validation will be trigger in every single input change, just need to pass the props validateOnChange: true.

Please check my repo to see my example and give a star if you like it: https://github.com/Tony1106/form-builder-vue3




Continue Learning