DEV Community

Cover image for Part 2: Custom Validation, Enhanced Interface, Error Handling, and Dynamic Data Binding
Loukas Kotas
Loukas Kotas

Posted on

Part 2: Custom Validation, Enhanced Interface, Error Handling, and Dynamic Data Binding

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',
}
Enter fullscreen mode Exit fullscreen mode

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,
        },
      ],
    },
...
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode
// 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 || '';
  }
Enter fullscreen mode Exit fullscreen mode

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];
}
Enter fullscreen mode Exit fullscreen mode
// 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',
      },
    ]);
  }
}
Enter fullscreen mode Exit fullscreen mode

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)