Introduction
Building dynamic forms can be complex. In this blog post, we'll cover a simple yet powerful way of creating dynamic forms in Angular. We'll walk through the process of creating a form schema, converting it to Angular's ReactiveForms controls, and finally building and using the form in our Angular component.
Creating the Dynamic Form
To begin, we need to create a schema object that defines the form controls, validators, and other metadata. We can model and save this schema to a backend of our choice and load it through an API call. Here's an example of a form schema:
formSchema: FormSchemaItem[] = [
{
key: 'name',
name: 'Full Name',
value: '',
type: 'text',
validators: [
{
type: FormSchemaValidatorTypes.REQUIRED,
},
{
type: FormSchemaValidatorTypes.MAX_LENGTH,
argument: 25,
},
{
type: FormSchemaValidatorTypes.MIN_LENGTH,
argument: 3,
},
],
},
{
key: 'age',
name: 'Age',
value: '',
type: 'number',
validators: [
{
type: FormSchemaValidatorTypes.REQUIRED,
},
{
type: FormSchemaValidatorTypes.MAX,
argument: 60,
},
{
type: FormSchemaValidatorTypes.MIN,
argument: 18,
},
],
},
{
key: 'email',
name: 'Email',
value: '',
type: 'email',
validators: [
{
type: FormSchemaValidatorTypes.REQUIRED,
},
{
type: FormSchemaValidatorTypes.EMAIL,
},
],
},
{
key: 'phoneNumber',
name: 'Phone Number',
value: '',
type: 'number',
validators: [
{
type: FormSchemaValidatorTypes.REQUIRED,
},
{
type: FormSchemaValidatorTypes.PATTERN,
argument: /\d/,
},
],
},
{
key: 'address',
name: 'Full Address',
value: '',
type: 'text',
validators: [
{
type: FormSchemaValidatorTypes.PATTERN,
argument: /[a-z]/,
},
{
type: FormSchemaValidatorTypes.MAX_LENGTH,
argument: 100,
},
],
},
{
key: 'date',
name: 'Date',
value: '',
type: 'date',
validators: [],
},
];
Next, we define the types used in the schema, including form item definitions and validators. These types help in maintaining a structured and consistent form schema.
import { FormControl } from '@angular/forms';
export type FormSchemaItemAndControl = {
item: FormSchemaItem;
control: FormControl;
};
export type FormSchemaItem = {
key: string;
name: string;
value: any;
type: 'text' | 'number' | 'tel' | 'email' | 'date';
validators: FormSchemaValidator[];
};
export type FormSchemaValidator = {
type: FormSchemaValidatorTypes;
argument?: any;
};
export enum FormSchemaValidatorTypes {
REQUIRED = 'required',
MAX = 'max',
MIN = 'min',
MAX_LENGTH = 'maxLength',
MIN_LENGTH = 'minLength',
EMAIL = 'email',
PATTERN = 'pattern',
}
Converting Schema to Form Controls
We can now create a function that converts our custom schema to Angular's ReactiveForms controls. This function called createFormControls
maps each item in the schema to a corresponding form control using Angular's FormBuilder. We also load the validators specified in the schema. Here's the code snippet:
createFormControls(schema: FormSchemaItem[]): FormSchemaItemAndControl[] {
const formSchemaWithControls = schema.map((item) => {
return {
item: item,
control: this.fb.control(
item.value,
this.loadValidators(item.validators)
),
};
});
return formSchemaWithControls;
}
Additionally, we have a loadValidators
function that maps the custom validators to Angular's built-in validators.
loadValidators(validators: FormSchemaValidator[]): any {
const validations = validators.map((element) => {
if (element.argument) {
return Validators[element.type](element.argument);
}
return Validators[element.type];
});
return validations;
}
Finally, we define the createFormGroup
function that creates a form group for all the controls. This function adds each control to the form group using the control's key from the schema.
createFormGroup(
formSchemaWithControls: FormSchemaItemAndControl[]
): UntypedFormGroup {
const form = this.fb.group({});
formSchemaWithControls.forEach((item) => {
form.addControl(item.item.key, item.control);
});
return form;
}
Using the form service
In our component.ts file, we can now build and use the dynamic form by injecting the form service and creating the form. Inside the createForm
method, we call createFormControls
to populate the formSchemaWithControls
array with the converted form controls. Then, we use createFormGroup
to create the form group for our dynamic form.
formSchemaWithControls: FormSchemaItemAndControl[] = [];
formGroup!: UntypedFormGroup;
constructor(private formService: FormService) {}
ngOnInit(): void {
this.createForm();
}
createForm(): void {
this.formSchemaWithControls = this.formService.createFormControls(
this.formService.formSchema
);
this.formGroup = this.formService.createFormGroup(
this.formSchemaWithControls
);
}
submit() {
console.log(this.formGroup.value);
}
In the HTML template, we can dynamically generate the form by looping through the controls and binding them to the respective input fields. We also display any validation errors associated with each control.
<div class="container">
<form
class="form"
*ngIf="formSchemaWithControls"
(ngSubmit)="submit()"
[formGroup]="formGroup"
>
<div class="form-item" *ngFor="let element of formSchemaWithControls">
<input
[type]="element.item.type"
[placeholder]="element.item.name"
[formControl]="element.control"
/>
<div
class="form-error"
*ngIf="
element.control.errors &&
(element.control.dirty || element.control.touched)
"
>
<p *ngFor="let error of element.control.errors | keyvalue">
{{ element.item.name + " " + error.key }}
</p>
</div>
</div>
<button [disabled]="formGroup.invalid" type="submit">Submit</button>
</form>
</div>
Conclusion
With the implementation of dynamic forms in Angular using the provided techniques, we have successfully created a form that adapts to changing requirements. The power of Angular's ReactiveForms, combined with a well-defined form schema, allows us to build robust and user-friendly dynamic forms. By following the steps outlined in this blog post, you can enhance the form-building experience in your Angular applications and create dynamic forms with ease.
And that's it! We now have a fully functional dynamic form based on the schema, complete with validations. Feel free to experiment with different schema configurations and expand upon this foundation to suit your specific needs.
You can find the source code in this GitHub Repository.