Introduction
Continuing from Part 1: Create Configuration Based Dynamic Forms in Angular, where we explored dynamic forms in Angular via a configuration-driven approach, this article advances further. Angular's versatility for building forms is undeniable, yet default validation messages sometimes lack. Here, we delve into custom validation errors, in order to provide more meaningful and user-centric feedback in Angular forms, enhancing the clarity and relevance of error messages, leading to a better user experience. By the end of this article, you'll be able to:
- Implement a dedicated error component for streamlined error handling in dynamic forms.
- Extend the capabilities of the
DynamicFormFieldModel
interface with custom validators. - Achieve dynamic data binding using template references and the
@ViewChildren
decorator. - Enhance the user experience of your Angular forms with more meaningful error messages and improved interactivity.
Code Example
Find a live code example on StackBlitz here. Feel free to explore the code and experiment with dynamic forms yourself!
Let's dive in! 🚀
Adding Custom Validation Errors
To begin, we'll empower the DynamicFormFieldModel
by introducing the ValidatorError
interface. This interface allows us to define custom validation errors, providing more meaningful feedback to users. We use the ErrorAssosiation
enum to associate each custom error with a specific validation scenario, making it easy to identify the error type triggered by each validation. The errorMessage
field within the ValidatorError
interface serves as the custom error message for each validation error. Furthermore, the errorAssosiation
field within this interface serves as a critical linkage between the error object and a specific reactive form error identifier. This linkage ensures precise error identification, facilitating a sophisticated error handling mechanism, and fostering effective communication with users.
// ValidatorError interface and ErrorAssosiation enum
export interface ValidatorError {
validator: ValidatorFn;
errorMessage?: string;
errorAssosiation: ErrorAssosiation;
}
export enum ErrorAssosiation {
// Native Validation Error Identifiers
REQUIRED = 'required',
MINLENGTH = 'minlength',
MAXLENGTH = 'maxlength',
EMAIL = 'email',
MIN = 'min',
MAX = 'max',
PATTERN = 'pattern',
REQUIREDTRUE = 'requiredtrue',
NULLVALIDATOR = 'nullvalidator',
// Custom Validation Error Identifier
UPPERCASE = 'uppercaseLetter',
}
Enhancing the Interface for Custom Validators
With the ValidatorError
interface in place, we upgrade the DynamicFormFieldModel
interface by replacing the traditional validators
field with this new interface. This enhancement enables us to attach custom validation errors to form fields, resulting in more relevant and user-focused error messages. Now, each validator in the configuration can have a corresponding error message, greatly improving the overall user experience.
// Enhanced DynamicFormFieldModel with ValidatorError interface
export interface DynamicFormFieldModel {
id: string;
label: string;
type: DynamicFormFieldModelType;
selectMenuOptions?: SelectMenuOption[];
value?: string | SelectMenuOption[]; // default value of the dynamic field
validators?: ValidatorError[];
}
// Example configuration with custom validation errors
...
{
id: 'favoriteMovies',
label: 'Favorite Movies',
type: 'select',
selectMenuOptions: [],
validators: [
{
validator: Validators.required,
errorMessage: 'Field is required',
errorAssosiation: ErrorAssosiation.REQUIRED,
},
{
validator: Validators.minLength(3),
errorMessage: 'You need to select at least 3 movies',
errorAssosiation: ErrorAssosiation.MINLENGTH,
},
],
},
...
Creating a Separate Error Component
While our enhancements have significantly improved the form's flexibility, we encounter an issue with code repetition in the template when displaying error messages. To address this, we introduce a dedicated component: ErrorMessageComponent
. This component handles the display of error messages below each dynamic form field, ensuring a cleaner and more organized user interface. By utilizing the ValidatorError
associations, we can present the correct custom error message for each validation error.
<!-- error-message.component.html -->
<span *ngIf="form.get(formItem.id)!.dirty">{{ getValidationErrorMessage(formItem) }}</span>
// error-message.component.ts
@Input() formItem!: DynamicFormFieldModel;
form!: FormGroup;
constructor(private rootFormGroup: FormGroupDirective) {
this.form = this.rootFormGroup.control;
}
getValidationErrorMessage(field: DynamicFormFieldModel): string {
const control = this.form.get(field.id);
const errorKey = Object.keys(control!.errors || {})[0];
// Here we assosiate the form control error with the custom error
const customError = field.validators?.find(
(validatorError) => validatorError.errorAssosiation === errorKey
);
return customError?.errorMessage || '';
}
Achieving Dynamic Data Binding
In the final section, we explore the dynamic data binding capabilities of our enhanced dynamic forms. To accomplish this, we use template references (#) to identify each dynamic form field. We leverage the @ViewChildren
decorator to access these fields, and in the ngAfterViewInit
lifecycle hook, we fetch data from a service. We demonstrate this with an example of asynchronously fetching a user's favorite movies and passing the data to the relevant dynamic form field. This dynamic data binding technique enhances the form's adaptability and user interaction.
// ngAfterViewInit in the component
@ViewChildren('dynamicFormField')
dynamicFormFieldContainers!: QueryList<ElementRef>;
ngAfterViewInit() {
const favoriteMoviesId = 'favoriteMovies';
const favoriteMovies = this.getFieldContainer(favoriteMoviesId) as any;
this.mainService.getFavoriteMovies().subscribe((movies) => {
favoriteMovies['formItem']['selectMenuOptions'] = movies;
});
}
getFieldContainer(id: string): ElementRef {
return this.dynamicFormFieldContainers.filter((d: any) => d['formItem']['id'] === id)[0];
}
// Service for fetching movie data
@Injectable({ providedIn: 'root' })
export class MainService {
constructor() {}
getFavoriteMovies(): Observable<SelectMenuOption[]> {
return of([
{
label: 'Star Wars',
value: 'e7a9c049-1a85-4c5a-b755-1b0a5c4394a3',
},
{
label: 'Lord of The Rings',
value: 'd624c916-d6f7-487f-8b16-87a034de3e34',
},
{
label: 'Shutter Island',
value: 'cf7cb566-6d2b-4b6b-96a3-ae9fba2a47af',
},
{
label: 'The Godfather',
value: 'b19b3edc-32da-4684-a10f-6e019df00d9e',
},
]);
}
}
Summary and Next Steps
In this article, we've covered essential aspects of creating powerful dynamic forms in Angular. We added custom validation errors, enhanced the DynamicFormFieldModel
interface, introduced a separate error component for improved error handling, and achieved dynamic data binding. These enhancements significantly enhance the user experience and form flexibility.
Stay tuned for the next article, where we'll take our dynamic forms to the next level. We'll delve deeper into advanced form handling techniques and the tools to create even more dynamic and interactive Angular forms. Exciting developments are on the horizon, so keep an eye out for the upcoming part! 🚀
Top comments (0)