Advanced Angular Forms: Dynamic Fields & Custom Validators
Tags: webdev, angular, javascript, typescript
Published: 2024-06-26 16:19:05

## Introduction

Forms are a fundamental part of web applications, allowing users to input and submit data. Form handling in Angular is robust and flexible using reactive forms. More complex and dynamic forms of applications require advanced techniques to handle them. In this article, we'll delve into advanced Angular form handling, focusing on dynamic forms and custom validators. We'll use Angular 18.0.3 for this and leverage the new features and improvements. This guide will help developers of any [Angular Development](https://wirefuture.com/blog/angular-development) Company to improve their form handling capabilities.

## Setting Up the Angular Project

First, let's set up a new Angular project. Ensure you have Angular CLI installed, then create a new project:

```
ng new advanced-angular-forms
cd advanced-angular-forms
ng add @angular/forms
```
This setup provides the basic structure and dependencies needed for our application. The Angular CLI facilitates a smooth development experience by scaffolding the project structure and managing dependencies.

## Creating Dynamic Forms

[Dynamic forms](https://docs.angular.lat/guide/dynamic-form) are forms whose structure can change at runtime. This is useful when form fields need to be added or removed based on user interactions or data retrieved from a server. These forms provide flexibility and adaptability, crucial for applications with variable data input requirements.

### Step 1: Define the Form Model

Start by defining a form model using FormGroup and FormControl in your component. Create a new component for our form:

```
ng generate component dynamic-form
```

In dynamic-form.component.ts, define the form model:

```
import { Component, OnInit } from '@angular/core';
import { FormBuilder, FormGroup, FormArray, Validators } from '@angular/forms';

@Component({
  selector: 'app-dynamic-form',
  templateUrl: './dynamic-form.component.html',
  styleUrls: ['./dynamic-form.component.css']
})
export class DynamicFormComponent implements OnInit {
  dynamicForm: FormGroup;

  constructor(private fb: FormBuilder) {
    this.dynamicForm = this.fb.group({
      name: ['', Validators.required],
      items: this.fb.array([])
    });
  }

  ngOnInit(): void { }

  get items(): FormArray {
    return this.dynamicForm.get('items') as FormArray;
  }

  addItem(): void {
    this.items.push(this.fb.control('', Validators.required));
  }

  removeItem(index: number): void {
    this.items.removeAt(index);
  }

  onSubmit(): void {
    console.log(this.dynamicForm.value);
  }
}

```

In this code, we use FormBuilder to simplify the creation of our form model. The form consists of a name control and an items form array. The items array will hold dynamically added form controls.

### Step 2: Build the Template

In dynamic-form.component.html, create a form template that allows adding and removing form controls dynamically:

```
<form [formGroup]="dynamicForm" (ngSubmit)="onSubmit()">
  <div>
    <label for="name">Name</label>
    <input id="name" formControlName="name">
  </div>
  
  <div formArrayName="items">
    <div *ngFor="let item of items.controls; let i = index">
      <label for="item-{{ i }}">Item {{ i + 1 }}</label>
      <input [id]="'item-' + i" [formControlName]="i">
      <button type="button" (click)="removeItem(i)">Remove</button>
    </div>
  </div>
  
  <button type="button" (click)="addItem()">Add Item</button>
  <button type="submit" [disabled]="!dynamicForm.valid">Submit</button>
</form>

```

This template uses Angular's form directives to bind the form model to the view. The *ngFor directive iterates over the items array, creating form controls dynamically. The formArrayName directive binds the form array to the template, ensuring synchronization between the model and view.

## Implementing Custom Validators

Custom validators are necessary for complex validation logic that goes beyond Angular's built-in validators. Validators ensure that the data entered by users meets specific criteria, enhancing data integrity and user experience. Let's implement a custom validator that ensures no duplicate items are added to the dynamic form.

### Step 1: Create the Validator Function

In dynamic-form.component.ts, add a custom validator function:

```
import { AbstractControl, ValidationErrors, ValidatorFn } from '@angular/forms';

export function uniqueItemsValidator(): ValidatorFn {
  return (formArray: AbstractControl): ValidationErrors | null => {
    const items = formArray.value;
    const uniqueItems = new Set(items);
    return items.length !== uniqueItems.size ? { duplicateItems: true } : null;
  };
}

```

This function checks for duplicate items by comparing the length of the items array with the length of a Set created from the items array. If they differ, it means there are duplicates, and the validator returns an error object.

### Step 2: Apply the Validator to the Form Array

Update the form group definition to include the custom validator:

```
this.dynamicForm = this.fb.group({
  name: ['', Validators.required],
  items: this.fb.array([], uniqueItemsValidator())
});

```
Here, we attach the uniqueItemsValidator to the items form array, ensuring that our custom validation logic is applied whenever the form array changes.

### Step 3: Display Validation Errors

Update the template to display validation errors:

```
<div formArrayName="items">
  <div *ngFor="let item of items.controls; let i = index">
    <label for="item-{{ i }}">Item {{ i + 1 }}</label>
    <input [id]="'item-' + i" [formControlName]="i">
    <button type="button" (click)="removeItem(i)">Remove</button>
  </div>
  <div *ngIf="items.hasError('duplicateItems')">
    Duplicate items are not allowed.
  </div>
</div>

```

This ensures that users receive immediate feedback when they attempt to add duplicate items, enhancing the form's usability and data integrity.

## Performance Considerations

When dealing with dynamic forms, performance can be a concern, especially if forms are large or complex. Here are some tips to keep performance in check:

**Debounce Input Changes:** Use debounceTime with valueChanges to reduce the frequency of form updates. This can prevent performance degradation in forms with many controls.

```
this.dynamicForm.valueChanges.pipe(
  debounceTime(300)
).subscribe(value => {
  console.log(value);
});
```

**OnPush Change Detection:** Use ChangeDetectionStrategy.OnPush to reduce unnecessary change detection cycles. This strategy tells Angular to check the component's view only when its input properties change.

```
@Component({
  selector: 'app-dynamic-form',
  templateUrl: './dynamic-form.component.html',
  styleUrls: ['./dynamic-form.component.css'],
  changeDetection: ChangeDetectionStrategy.OnPush
})
export class DynamicFormComponent { /*...*/ }

```

**Lazy Loading Form Controls:** Load form controls only when necessary, such as when a user interacts with a particular section of the form. This can significantly reduce initial load times and improve performance.

## Conclusion

Advanced form scenarios in Angular require creating dynamic forms and using custom validators to validate data integrity. Using Angular's reactive forms, we build flexible, robust forms that respond to changing requirements. Taking performance into account, these techniques can be used to produce fast and friendly forms in any Angular app.

This article demonstrated how to create dynamic forms and custom validators, from which more complex form-based features can be built in Angular. Exploring these concepts will help you to fully utilize Angular's form management capabilities. As you continue to create applications, these advanced form handling methods will prove invaluable for producing user-friendly, responsive, and dynamic forms.