Angular Dynamic Forms And Custom Validators Complete Guide
Understanding the Core Concepts of Angular Dynamic Forms and Custom Validators
Angular Dynamic Forms and Custom Validators
Dynamic Forms in Angular
Dynamic forms in Angular refer to the creation of forms at runtime based on data models or external configurations. Unlike static forms which are hard-coded in the component template, dynamic forms allow developers to build forms dynamically by adding or removing form fields, changing form validation rules, and more, based on user interactions or predefined business logic.
Benefits of Dynamic Forms
- Flexibility: Ability to modify forms dynamically to adapt to various user scenarios or data-driven requirements.
- Reusability: Form components can be reused across different parts of the application.
- Maintainability: Easier to maintain and update forms as they are not hard-coded in templates.
Implementation Steps
Define Configuration: Start by defining the form configuration which includes the form fields, their types, default values, and validation rules. This configuration can be static or fetched from an API.
const formConfig = [ { name: 'firstName', type: 'text', required: true, label: 'First Name' }, { name: 'lastName', type: 'text', required: true, label: 'Last Name' }, { name: 'email', type: 'email', required: true, label: 'Email' }, { name: 'age', type: 'number', min: 18, label: 'Age' }, ];
Create Dynamic Form Group: Use Angular's
FormBuilder
to create aFormGroup
based on the form configuration.import { FormBuilder, FormGroup, Validators } from '@angular/forms'; constructor(private fb: FormBuilder) {} ngOnInit() { this.dynamicForm = this.fb.group({}); formConfig.forEach((field) => { const control = this.fb.control( field.default ? field.default : '', field.required ? [Validators.required, ...this.getCustomValidators(field)] : [ ...this.getCustomValidators(field)] ); this.dynamicForm.addControl(field.name, control); }); }
Render Form in Template: Create a loop in your template to iterate over the form controls and render the form fields.
<form [formGroup]="dynamicForm"> <div *ngFor="let field of formConfig"> <label [for]="field.name">{{ field.label }}</label> <input *ngIf="field.type === 'text' || field.type === 'email'" [type]="field.type" [formControlName]="field.name" id="{{ field.name }}"> <input *ngIf="field.type === 'number'" [type]="field.type" [formControlName]="field.name" id="{{ field.name }}" [min]="field.min"> <small *ngIf="dynamicForm.get(field.name)?.invalid && (dynamicForm.get(field.name)?.dirty || dynamicForm.get(field.name)?.touched)"> {{ getValidationMessage(field) }} </small> </div> <button [disabled]="dynamicForm.invalid">Submit</button> </form>
Custom Validators in Angular
Custom validators provide a means to add specific validation logic that is not covered by Angular’s built-in validators like required
, minLength
, maxLength
, and email
. This feature is invaluable when dealing with unique validation criteria specific to an application.
Benefits of Custom Validators
- Specificity: Custom validators allow developers to implement exact validation rules tailored to business needs.
- Reusability: Validator logic can be reused across multiple form controls.
- Simplicity: Encapsulating validation logic in custom validators helps in keeping form templates clean and focused.
Implementation Steps
Create a Custom Validator Function:
Custom validators are functions that take a
FormControl
as an argument and return anobject
with a validation key if the validation fails, ornull
if it passes.import { AbstractControl, ValidationErrors, ValidatorFn } from '@angular/forms'; export function emailPatternValidator(): ValidatorFn { return (control: AbstractControl): ValidationErrors | null => { const emailPattern = /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/; const value = control.value; if (value && !emailPattern.test(value)) { return { emailPattern: true }; } return null; }; }
Apply the Custom Validator to a FormControl: Apply the custom validator to a form control using the
Validators.compose
method if combining with other validators.ngOnInit() { this.dynamicForm = this.fb.group({ email: ['', Validators.compose([ Validators.required, emailPatternValidator() ])] }); }
Display Validation Messages: Update the template to display validation messages based on the custom validation errors.
Online Code run
Step-by-Step Guide: How to Implement Angular Dynamic Forms and Custom Validators
Step 1: Set Up Your Angular Project
Install Angular CLI (if you haven't already):
npm install -g @angular/cli
Create a new Angular project:
ng new dynamic-forms-example
Navigate to the project directory:
cd dynamic-forms-example
Serve the project to ensure everything is set up correctly:
ng serve
Navigate to
http://localhost:4200/
in your web browser to see your application running.
Step 2: Create a Dynamic Form
Generate a new component for our dynamic form:
ng generate component dynamic-form
Install ReactiveFormsModule: The
ReactiveFormsModule
in Angular contains the necessary services and parts to build forms in a reactive fashion. Open yourapp.module.ts
file and import it:import { BrowserModule } from '@angular/platform-browser'; import { NgModule } from '@angular/core'; import { ReactiveFormsModule } from '@angular/forms'; import { AppComponent } from './app.component'; import { DynamicFormComponent } from './dynamic-form/dynamic-form.component'; @NgModule({ declarations: [ AppComponent, DynamicFormComponent ], imports: [ BrowserModule, ReactiveFormsModule ], providers: [], bootstrap: [AppComponent] }) export class AppModule { }
Set up the DynamicFormComponent: Open
dynamic-form.component.ts
and set up a form to dynamically add form controls.import { Component } from '@angular/core'; import { FormArray, FormBuilder, FormGroup, Validators } from '@angular/forms'; @Component({ selector: 'app-dynamic-form', templateUrl: './dynamic-form.component.html', styleUrls: ['./dynamic-form.component.css'] }) export class DynamicFormComponent { form: FormGroup; constructor(private fb: FormBuilder) { this.form = this.fb.group({ fields: this.fb.array([this.createField()]) }); } get fields() { return this.form.get('fields') as FormArray; } createField(): FormGroup { return this.fb.group({ type: ['', Validators.required], value: [''] }); } addField() { this.fields.push(this.createField()); } removeField(index: number) { this.fields.removeAt(index); } onSubmit() { console.log('Form Submitted', this.form.value); } }
Set up the template for the DynamicFormComponent: Open
dynamic-form.component.html
and add the necessary HTML to display and manipulate the form.<form [formGroup]="form" (ngSubmit)="onSubmit()"> <div formArrayName="fields"> <div *ngFor="let field of fields.controls; let i = index" [formGroupName]="i"> <label> Field Type: <select formControlName="type"> <option disabled>Select a type</option> <option>Text</option> <option>Email</option> </select> </label> <label> Value: <input formControlName="value" /> </label> <button type="button" (click)="removeField(i)">Remove Field</button> </div> </div> <button type="button" (click)="addField()">Add Field</button> <button type="submit" [disabled]="form.invalid">Submit</button> </form>
Step 3: Create a Custom Validator
Generate a new service for your custom validator:
ng generate service custom-validator
Implement the custom validator logic: Open
custom-validator.service.ts
and create a method for a custom validator.import { Injectable } from '@angular/core'; import { AbstractControl, ValidationErrors, ValidatorFn } from '@angular/forms'; @Injectable({ providedIn: 'root' }) export class CustomValidatorService { constructor() { } // Custom validator to validate email confirmation emailConfirmation(controlNameToMatch: string): ValidatorFn { return (control: AbstractControl): ValidationErrors | null => { if (control.parent && control.value !== control.parent.get(controlNameToMatch)?.value) { return { 'emailConfirmation': true }; } return null; }; } }
Use the custom validator in your DynamicFormComponent: Inject the
CustomValidatorService
and use it in your form setup.import { Component } from '@angular/core'; import { FormArray, FormBuilder, FormGroup, Validators } from '@angular/forms'; import { CustomValidatorService } from '../custom-validator.service'; @Component({ selector: 'app-dynamic-form', templateUrl: './dynamic-form.component.html', styleUrls: ['./dynamic-form.component.css'] }) export class DynamicFormComponent { form: FormGroup; constructor(private fb: FormBuilder, private customValidator: CustomValidatorService) { this.form = this.fb.group({ fields: this.fb.array([this.createField()]), email: ['', [Validators.required, Validators.email]], confirmEmail: ['', [Validators.required, this.customValidator.emailConfirmation('email')]] }); } get fields() { return this.form.get('fields') as FormArray; } get email() { return this.form.get('email'); } get confirmEmail() { return this.form.get('confirmEmail'); } createField(): FormGroup { return this.fb.group({ type: ['', Validators.required], value: [''] }); } addField() { this.fields.push(this.createField()); } removeField(index: number) { this.fields.removeAt(index); } onSubmit() { console.log('Form Submitted', this.form.value); } }
Update the template to include the email and confirm email fields: Open
dynamic-form.component.html
and add the email and confirm email fields.<form [formGroup]="form" (ngSubmit)="onSubmit()"> <div formArrayName="fields"> <div *ngFor="let field of fields.controls; let i = index" [formGroupName]="i"> <label> Field Type: <select formControlName="type"> <option disabled>Select a type</option> <option>Text</option> <option>Email</option> </select> </label> <label> Value: <input formControlName="value" /> </label> <button type="button" (click)="removeField(i)">Remove Field</button> </div> </div> <button type="button" (click)="addField()">Add Field</button> <label> Email: <input formControlName="email" /> <div *ngIf="email?.touched && email?.invalid"> <div *ngIf="email?.errors?.required">Email is required.</div> <div *ngIf="email?.errors?.email">Invalid email format.</div> </div> </label> <label> Confirm Email: <input formControlName="confirmEmail" /> <div *ngIf="confirmEmail?.touched && confirmEmail?.invalid"> <div *ngIf="confirmEmail?.errors?.required">Confirmation email is required.</div> <div *ngIf="confirmEmail?.errors?.emailConfirmation">Emails do not match.</div> </div> </label> <button type="submit" [disabled]="form.invalid">Submit</button> </form>
Step 4: Execute the Application
Serve your application:
ng serve
Navigate to the application: Visit
http://localhost:4200/
in your browser to see the dynamic form in action, and test the custom validators.
Top 10 Interview Questions & Answers on Angular Dynamic Forms and Custom Validators
Top 10 Questions and Answers on Angular Dynamic Forms and Custom Validators
1. What are Dynamic Forms in Angular?
2. How do you create a Dynamic Form in Angular using Reactive Forms?
To create a dynamic form using Reactive Forms, first make sure to import ReactiveFormsModule
in your module. Next, you can define your form structure dynamically in the component class by creating FormGroup
instances. Here's a simplified example:
import { Component, OnInit } from '@angular/core';
import { FormBuilder, FormGroup } from '@angular/forms';
@Component({
selector: 'app-dynamic-form',
templateUrl: './dynamic-form.component.html',
styleUrls: ['./dynamic-form.component.css']
})
export class DynamicFormComponent implements OnInit {
form: FormGroup;
formData = [
{ type: 'text', label: 'First Name', name: 'firstName' },
{ type: 'email', label: 'Email', name: 'email' },
];
constructor(private fb: FormBuilder) {}
ngOnInit() {
this.form = this.fb.group({});
this.addFormControl();
}
addFormControl() {
this.formData.forEach(item => {
this.form.addControl(item.name, this.fb.control(''));
});
}
}
In the template, you would loop through form.controls
to render form controls dynamically.
3. What are Custom Validators in Angular?
Custom validators allow you to add your own validation logic to form controls that goes beyond Angular's built-in validators. They can be synchronous or asynchronous and help ensure that the form data meets specific business requirements.
4. How do you create a Custom Synchronous Validator in Angular?
To create a custom synchronous validator, you define a function that takes a FormControl
as an argument and returns an object representing an error state if the validation fails. Here's how you can create a custom validator to check if the input contains only alphabetic characters:
import { AbstractControl, ValidationErrors } from '@angular/forms';
export function alphabeticValidator(control: AbstractControl): ValidationErrors | null {
const value = control.value;
const regex = /^[a-zA-Z]+$/;
return regex.test(value) ? null : {'invalidAlphabetic': true};
}
Then apply it to a form control:
this.form.addControl('firstName', this.fb.control('', [alphabeticValidator]));
5. How do you use Custom Async Validators in Angular?
Asynchronous validators are used when validation needs to involve an external source, such as checking if a username is in use by querying a backend service. Here's how to create and use an async validator:
import { AbstractControl, AsyncValidator, ValidationErrors } from '@angular/forms';
import { Observable, of } from 'rxjs';
import { map, catchError } from 'rxjs/operators';
import { Injectable } from '@angular/core';
import { UserService } from '../user.service';
@Injectable({
providedIn: 'root'
})
export class UniqueUsernameValidator implements AsyncValidator {
constructor(private userService: UserService) {}
validate(control: AbstractControl): Observable<ValidationErrors | null> {
return this.userService.checkUsername(control.value).pipe(
map(isAvailable => (isAvailable ? {'uniqueUsername': true} : null)),
catchError(() => of(null))
);
}
}
Apply the async validator to a form control:
form: FormGroup;
constructor(private fb: FormBuilder, private uniqueUsernameValidator: UniqueUsernameValidator) {}
ngOnInit() {
this.form = this.fb.group({
username: ['', this.uniqueUsernameValidator.validate.bind(this.uniqueUsernameValidator)],
});
}
6. How can you add multiple validators to a single form control in Angular?
You can add a combination of both synchronous and asynchronous validators to a form control by passing them as an array to the FormControl
constructor:
this.form.addControl('email', this.fb.control('', [
Validators.required,
Validators.email,
this.uniqueEmailValidator.validate.bind(this.uniqueEmailValidator)
]));
7. What is the best practice for creating dynamic controls and validators?
- Modularize Code: Break down the code into smaller, reusable components.
- Separate Data from UI: Store the form definition separately from the component’s logic.
- Use Custom Validators: Create and use custom validators for complex validation rules.
- Test Extensively: Thoroughly test your dynamic forms to ensure all possible scenarios are addressed.
8. How do you handle form submission of a Dynamic Form?
When handling form submission, ensure that form controls are properly validated and that the data is formatted correctly for the server or downstream processes. You can use the form.valid
property to check if the form is valid before submission.
onSubmit() {
if (this.form.valid) {
const formData = this.form.value;
// submit formData
} else {
console.log('Form is invalid');
}
}
9. How can you dynamically update form controls and validators based on user input?
Use the valueChanges
observable of a FormControl
or FormGroup
to listen for changes and adjust the form accordingly. For example, you might add or remove controls or validators based on the value of a field.
this.form.get('country').valueChanges.subscribe(country => {
if (country === 'USA') {
this.form.addControl('zipCode', this.fb.control('', [Validators.required, zipCodeValidator]));
} else {
this.form.removeControl('zipCode');
}
});
10. How do you handle errors in a dynamic form?
Angular provides mechanisms to track errors on form controls. Use getError
on controls to conditionally display error messages in the template. For example:
getErrorMessage(errorType: string) {
const messages = {
required: 'This field is required',
email: 'Invalid email address',
uniqueUsername: 'Username is already taken'
};
return messages[errorType];
}
In the template:
Login to post a comment.