Lesson 06-Angular Form Handling

Angular Form Basics

Angular provides two approaches to handle user input through forms: Reactive Forms and Template-Driven Forms. Both capture user input events from the view, validate input, create form models, modify data models, and provide ways to track changes.

Template-Driven Forms

Template-driven forms are the most straightforward form type in Angular, defined directly in the HTML template using directives. They are ideal for rapid prototyping and simple forms.

Basic Structure

Template-driven forms use the ngForm directive to define the form and the ngModel directive for two-way data binding.

<!-- app.component.html -->
<form #f="ngForm" (ngSubmit)="onSubmit(f)">
  <input type="text" name="username" ngModel>
  <button type="submit">Submit</button>
</form>

Form Controls

The ngModel directive creates form controls and enables two-way data binding.

<input type="text" name="email" ngModel>

Validation

Template-driven forms support built-in validation directives such as required, minlength, and maxlength.

<input type="text" name="password" ngModel required minlength="8">

Submission Handling

On form submission, access the ngForm instance to retrieve form data.

// app.component.ts
import { Component } from '@angular/core';

@Component({
  selector: 'app-root',
  templateUrl: './app.component.html'
})
export class AppComponent {
  onSubmit(form: any) {
    console.log(form.value);
  }
}

Reactive Forms

Reactive forms offer more powerful data binding and validation capabilities, making them suitable for complex form logic and dynamic form structures.

Creating a Form Model

Reactive forms start by creating a form model in the component class.

// app.component.ts
import { Component } from '@angular/core';
import { FormBuilder, FormGroup, Validators } from '@angular/forms';

@Component({
  selector: 'app-root',
  templateUrl: './app.component.html'
})
export class AppComponent {
  formGroup: FormGroup;

  constructor(private fb: FormBuilder) {
    this.formGroup = this.fb.group({
      username: ['', Validators.required],
      password: ['', [Validators.required, Validators.minLength(8)]]
    });
  }
}

Using the Form Model

Bind the form model to the form in the template using the formGroup directive.

<!-- app.component.html -->
<form [formGroup]="formGroup" (ngSubmit)="onSubmit()">
  <input type="text" formControlName="username">
  <input type="password" formControlName="password">
  <button type="submit">Submit</button>
</form>

Validation

Reactive forms provide a rich set of validators that can be combined.

this.formGroup = this.fb.group({
  email: ['', [Validators.required, Validators.email]],
  password: ['', [Validators.required, Validators.minLength(8)]]
});

Submission Handling

Access form data directly from the FormGroup instance upon submission.

onSubmit() {
  if (this.formGroup.valid) {
    console.log(this.formGroup.value);
  }
}

Comparison and Selection

  • Template-Driven Forms: Best for rapid development and simple forms, easy to understand and implement.
  • Reactive Forms: Offer more powerful features, ideal for complex form logic and dynamic structures, but have a steeper learning curve.

Best Practices

  • Separate Form Logic: Isolate form-related logic (e.g., validation rules, data handling) from UI logic to improve readability and maintainability.
  • Reuse Form Controls: Use Angular’s FormControl and FormGroup classes to create reusable form controls.
  • State Management: Leverage Angular’s change detection to avoid unnecessary form updates, improving performance.
  • Error Handling: Display clear validation errors with user-friendly feedback.

Reactive Forms

Reactive forms are a powerful mechanism in Angular for creating and managing forms declaratively, particularly suited for complex and dynamic form structures.

Importing Required Modules and Interfaces

Ensure the ReactiveFormsModule is imported in your Angular module.

// app.module.ts
import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';
import { ReactiveFormsModule } from '@angular/forms';

import { AppComponent } from './app.component';

@NgModule({
  declarations: [
    AppComponent
  ],
  imports: [
    BrowserModule,
    ReactiveFormsModule
  ],
  providers: [],
  bootstrap: [AppComponent]
})
export class AppModule { }

Adding Basic Form Controls

Create a simple form control using FormControl.

// app.component.ts
import { Component } from '@angular/core';
import { FormControl } from '@angular/forms';

@Component({
  selector: 'app-root',
  templateUrl: './app.component.html'
})
export class AppComponent {
  username = new FormControl('');
}

Bind the control in the template using the formControlName directive.

<!-- app.component.html -->
<input type="text" [formControl]="username">

Using the FormBuilder Service

The FormBuilder service provides a concise way to create form controls and groups.

// app.component.ts
import { Component } from '@angular/core';
import { FormBuilder, FormGroup } from '@angular/forms';

@Component({
  selector: 'app-root',
  templateUrl: './app.component.html'
})
export class AppComponent {
  formGroup: FormGroup;

  constructor(private fb: FormBuilder) {
    this.formGroup = this.fb.group({
      username: ['']
    });
  }
}

Grouping Form Controls

Use FormGroup to combine multiple FormControl instances into a cohesive form.

// app.component.ts
this.formGroup = this.fb.group({
  personalInfo: this.fb.group({
    firstName: [''],
    lastName: ['']
  }),
  contactInfo: this.fb.group({
    email: [''],
    phone: ['']
  })
});

Access these groups in the template using the formGroupName directive.

<!-- app.component.html -->
<form [formGroup]="formGroup">
  <div formGroupName="personalInfo">
    <input type="text" formControlName="firstName">
    <input type="text" formControlName="lastName">
  </div>
  <div formGroupName="contactInfo">
    <input type="text" formControlName="email">
    <input type="text" formControlName="phone">
  </div>
</form>

Validating Form Input

Reactive forms offer a comprehensive set of validators for easy rule application.

// app.component.ts
this.formGroup = this.fb.group({
  email: ['', [Validators.required, Validators.email]],
  password: ['', [Validators.required, Validators.minLength(8)]]
});

Display validation errors in the template using ngIf and the valid property of FormControl.

<!-- app.component.html -->
<div *ngIf="formGroup.get('email').invalid && formGroup.get('email').touched">
  Email is required and must be valid.
</div>

Creating Dynamic Forms

Dynamic forms allow adding or removing controls at runtime, ideal for forms with unpredictable structures.

// app.component.ts
this.formGroup = this.fb.group({
  questions: this.fb.array([])
});

addQuestion() {
  const control = this.fb.control('');
  (this.formGroup.get('questions') as FormArray).push(control);
}

removeQuestion(index: number) {
  (this.formGroup.get('questions') as FormArray).removeAt(index);
}

Iterate over the FormArray in the template using *ngFor.

<!-- app.component.html -->
<form [formGroup]="formGroup">
  <div formArrayName="questions">
    <div *ngFor="let questionCtrl of formGroup.get('questions').controls; let i = index">
      <input type="text" [formControl]="questionCtrl">
      <button (click)="removeQuestion(i)">Remove</button>
    </div>
  </div>
  <button (click)="addQuestion()">Add Question</button>
</form>

Submitting Forms

Check the valid property of FormGroup to determine if the form can be submitted.

// app.component.ts
onSubmit() {
  if (this.formGroup.valid) {
    console.log(this.formGroup.value);
  }
}

Template-Driven Forms

Importing Required Modules and Directives

Ensure the FormsModule is imported in your Angular module, as it is required for template-driven forms.

// app.module.ts
import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';
import { FormsModule } from '@angular/forms';

import { AppComponent } from './app.component';

@NgModule({
  declarations: [
    AppComponent
  ],
  imports: [
    BrowserModule,
    FormsModule
  ],
  providers: [],
  bootstrap: [AppComponent]
})
export class AppModule { }

Creating Basic Form Controls

Use the ngModel directive to create form controls with two-way data binding.

<!-- app.component.html -->
<form #form="ngForm" (ngSubmit)="onSubmit(form)">
  <label for="username">Username:</label>
  <input type="text" id="username" name="username" [(ngModel)]="username">
  <br>
  <label for="password">Password:</label>
  <input type="password" id="password" name="password" [(ngModel)]="password">
  <br>
  <button type="submit">Submit</button>
</form>

Define corresponding variables in the component class:

// app.component.ts
import { Component } from '@angular/core';

@Component({
  selector: 'app-root',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.css']
})
export class AppComponent {
  username = '';
  password = '';

  onSubmit(form: any) {
    console.log('Form submitted:', form.value);
  }
}

Form Validation

Template-driven forms support built-in validation directives like required, minlength, and maxlength.

<!-- app.component.html -->
<input type="text" name="username" [(ngModel)]="username" required>
<input type="password" name="password" [(ngModel)]="password" required minlength="8">

Display validation errors using ngIf with the submitted property of ngForm and the valid property of ngModel.

<div *ngIf="form.submitted && form.controls.username.errors?.['required']">
  Username is required.
</div>

Form Groups

Use ngForm and ngModelGroup directives to create form groups, organizing and validating related controls.

<!-- app.component.html -->
<form #form="ngForm" (ngSubmit)="onSubmit(form)">
  <div ngModelGroup="personalInfo">
    <label for="firstName">First Name:</label>
    <input type="text" id="firstName" name="firstName" [(ngModel)]="personalInfo.firstName">
    <br>
    <label for="lastName">Last Name:</label>
    <input type="text" id="lastName" name="lastName" [(ngModel)]="personalInfo.lastName">
  </div>
  <!-- Additional form controls -->
  <button type="submit">Submit</button>
</form>

Define the corresponding object in the component class:

// app.component.ts
export class AppComponent {
  personalInfo = { firstName: '', lastName: '' };
}

Custom Validators

Create custom validators to meet specific business requirements.

// custom.validator.ts
import { NG_VALIDATORS, Validator, AbstractControl, ValidationErrors } from '@angular/forms';

export function customValidator(control: AbstractControl): ValidationErrors | null {
  if (control.value === 'secret') {
    return { invalidCustom: true };
  }
  return null;
}

export class CustomValidatorDirective implements Validator {
  validate(control: AbstractControl): ValidationErrors | null {
    return customValidator(control);
  }
}

Register the custom validator in the module:

// app.module.ts
@NgModule({
  imports: [FormsModule],
  declarations: [CustomValidatorDirective],
  providers: [{ provide: NG_VALIDATORS, useExisting: CustomValidatorDirective, multi: true }]
})
export class AppModule { }

Use the custom validator in the template:

<input type="text" name="secretField" [(ngModel)]="secretField" appCustomValidator>

Dynamic Form Controls

Template-driven forms support dynamically adding and removing controls using event listeners and conditional rendering.

<!-- app.component.html -->
<button (click)="addInput()">Add Input</button>
<div *ngFor="let input of inputs; let i = index">
  <input type="text" name="input{{i}}" [(ngModel)]="inputs[i]">
  <button (click)="removeInput(i)">Remove</button>
</div>

Define the array and methods in the component class:

// app.component.ts
export class AppComponent {
  inputs: string[] = [];

  addInput() {
    this.inputs.push('');
  }

  removeInput(index: number) {
    this.inputs.splice(index, 1);
  }
}

Form Reset

Reset the form to its initial state when needed.

// app.component.ts
resetForm() {
  this.form.reset();
}

Call the reset method in the template:

<button (click)="resetForm()">Reset</button>

Form Disabling

Use the [disabled] attribute to disable form controls, preventing user input.

<input type="text" name="username" [(ngModel)]="username" [disabled]="isDisabled">

Control the isDisabled variable in the component:

// app.component.ts
isDisabled = false;

Form Styling

Use CSS and Angular’s ngClass directive to style forms based on their state.

<input type="text" name="username" [(ngModel)]="username" [ngClass]="{ 'is-invalid': form.submitted && form.controls.username.errors?.['required'] }">

Form Input Validation

Reactive Form Validation

Reactive forms manage form state and validation through FormControl, FormGroup, and FormArray, offering a robust validator system.

Basic Validators

Add validation rules directly to a FormControl.

// app.component.ts
import { Component } from '@angular/core';
import { FormBuilder, FormGroup, Validators } from '@angular/forms';

@Component({
  selector: 'app-root',
  templateUrl: './app.component.html'
})
export class AppComponent {
  myForm: FormGroup;

  constructor(private fb: FormBuilder) {
    this.myForm = this.fb.group({
      email: ['', [Validators.required, Validators.email]],
      password: ['', Validators.minLength(6)]
    });
  }
}

Custom Validators

Create custom validation logic.

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

export function matchPassword(control: AbstractControl): { [key: string]: any } | null {
  const password = control.get('password');
  const confirmPassword = control.get('confirmPassword');

  return password && confirmPassword && password.value !== confirmPassword.value ? { mismatch: true } : null;
}

// Use in FormGroup
this.myForm = this.fb.group({
  password: ['', Validators.minLength(6)],
  confirmPassword: ['', Validators.minLength(6)],
}, { validators: matchPassword });

Template-Driven Form Validation

Template-driven forms apply validation rules directly in the template using directives.

Built-In Validation Directives

<!-- app.component.html -->
<input type="email" name="email" [(ngModel)]="user.email" required email>

Displaying Error Messages

<div *ngIf="f.submitted && f.controls.email.errors">
  <div *ngIf="f.controls.email.errors.required">Email is required</div>
  <div *ngIf="f.controls.email.errors.email">Invalid email format</div>
</div>

Asynchronous Validation

Asynchronous validation is useful for scenarios requiring server-side checks, such as verifying username availability.

Reactive Form Asynchronous Validation

import { Observable, of } from 'rxjs';
import { map, catchError } from 'rxjs/operators';
import { HttpClient } from '@angular/common/http';

checkUsername(username: string): Observable<any> {
  return this.http.get(`api/check-username/${username}`).pipe(
    map(response => response.available ? null : { taken: true }),
    catchError(() => of(null))
  );
}

this.myForm = this.fb.group({
  username: ['', [Validators.required], [this.checkUsername.bind(this)]]
});

Template-Driven Form Asynchronous Validation

Asynchronous validation in template-driven forms typically requires custom directives or services.

Cross-Field Validation

Cross-field validation involves validating based on multiple form fields, using a validator at the FormGroup level.

this.myForm = this.fb.group({
  password: ['', Validators.minLength(6)],
  confirmPassword: ['', Validators.minLength(6)],
}, { validators: matchPasswords });

function matchPasswords(group: FormGroup) {
  const password = group.get('password').value;
  const confirmPassword = group.get('confirmPassword').value;
  return password === confirmPassword ? null : { mismatch: true };
}

Dynamic Form Validation

Dynamic form validation follows the same principles but applies rules dynamically to generated controls.

addInput() {
  const control = new FormControl('', Validators.required);
  this.dynamicFormArray.push(control);
}

Conditional Validation

Conditional validation enables or disables rules based on specific conditions, e.g., requiring a job description only if the user selects “other” as their occupation.

// app.component.ts
this.myForm = this.fb.group({
  job: ['', Validators.required],
  jobDetails: ['', Validators.required]
});

validateJobDetails(control: AbstractControl) {
  const job = control.get('job');
  const jobDetails = control.get('jobDetails');

  if (job && job.value === 'other' && !jobDetails.value) {
    return { requiredWhenOther: true };
  }
  return null;
}

this.myForm.addValidators(validateJobDetails);

In the template:

<div *ngIf="myForm.get('jobDetails').errors?.requiredWhenOther">
  Job details are required when you select "other".
</div>

Advanced Custom Validator Usage

Custom validators are highly flexible, validating individual controls or the entire form state.

// app.component.ts
validatePasswordMatch(group: FormGroup) {
  const password = group.get('password');
  const confirmPassword = group.get('confirmPassword');

  if (password && confirmPassword && password.value !== confirmPassword.value) {
    return { passwordsDoNotMatch: true };
  }
  return null;
}

Complex Asynchronous Validation with RxJS

RxJS provides powerful tools for handling asynchronous operations, such as checking username availability.

// app.component.ts
import { Observable } from 'rxjs';
import { map, debounceTime, switchMap } from 'rxjs/operators';
import { HttpClient } from '@angular/common/http';

checkUsername(username: string): Observable<any> {
  return this.http.get(`api/check-username/${username}`).pipe(
    map(response => ({ isAvailable: response.available })),
    catchError(error => of({ isAvailable: false }))
  );
}

this.myForm.get('username').valueChanges.pipe(
  debounceTime(500),
  switchMap(username => this.checkUsername(username))
).subscribe(result => {
  if (!result.isAvailable) {
    this.myForm.get('username').setErrors({ usernameTaken: true });
  } else {
    this.myForm.get('username').setErrors(null);
  }
});

Optimizing Form Performance

  • Load Validators On-Demand: For large forms, avoid loading all validators initially; load them dynamically based on user interaction.
  • Use updateOn Option: By default, FormControl validates on every value change. Set updateOn to 'blur' or 'submit' to reduce unnecessary computations.
  • Throttle Asynchronous Validation: Use RxJS operators like debounceTime to prevent frequent network requests.
const control = new FormControl('', Validators.required, this.checkUsername.bind(this));
control.updateOn = 'blur';

Binding Dynamic Forms

Core Concepts of Dynamic Forms

Dynamic forms enable generating form controls based on data sources or logic conditions. Key aspects include:

  • Data Model: JSON or classes defining the form structure and fields.
  • Form Controls: Dynamically generated FormControl or FormGroup instances based on the data model.
  • Template Rendering: Using Angular structural directives (e.g., *ngFor, *ngIf) to dynamically display controls.

Building Dynamic Forms with Reactive Forms

Reactive forms provide a robust API for dynamically managing and validating form controls.

Defining the Data Model

// models.ts
export interface FormField {
  key: string;
  label: string;
  type: string;
  options?: string[];
  required?: boolean;
}

export const FORM_FIELDS: FormField[] = [
  { key: 'name', label: 'Name', type: 'text', required: true },
  { key: 'age', label: 'Age', type: 'number' },
  { key: 'gender', label: 'Gender', type: 'select', options: ['Male', 'Female'] },
];

Building the Dynamic Form

// app.component.ts
import { Component } from '@angular/core';
import { FormBuilder, FormGroup, Validators } from '@angular/forms';
import { FORM_FIELDS } from './models';

@Component({
  selector: 'app-root',
  templateUrl: './app.component.html'
})
export class AppComponent {
  dynamicForm: FormGroup;
  formFields: FormField[];

  constructor(private fb: FormBuilder) {
    this.formFields = FORM_FIELDS;
    this.dynamicForm = this.fb.group({});
    this.createFormControls();
  }

  createFormControls() {
    this.formFields.forEach(field => {
      let controlOptions: any = {};
      if (field.required) {
        controlOptions.validators = Validators.required;
      }
      this.dynamicForm.addControl(field.key, this.fb.control('', controlOptions));
    });
  }
}

Rendering the Dynamic Form

<!-- app.component.html -->
<form [formGroup]="dynamicForm">
  <div *ngFor="let field of formFields">
    <label>{{ field.label }}</label>
    <input *ngIf="field.type === 'text'" type="text" formControlName="{{ field.key }}">
    <input *ngIf="field.type === 'number'" type="number" formControlName="{{ field.key }}">
    <select *ngIf="field.type === 'select'" formControlName="{{ field.key }}">
      <option *ngFor="let option of field.options" [value]="option">{{ option }}</option>
    </select>
  </div>
  <button type="submit">Submit</button>
</form>

Dynamically Adding and Removing Form Controls

Use FormArray to manage a variable number of form controls.

// app.component.ts
this.dynamicForm.addControl('items', this.fb.array([]));

addFormItem() {
  const items = this.dynamicForm.get('items') as FormArray;
  items.push(this.fb.group({
    name: ['', Validators.required],
    description: ''
  }));
}

removeFormItem(index: number) {
  const items = this.dynamicForm.get('items') as FormArray;
  items.removeAt(index);
}

In the template:

<!-- app.component.html -->
<div formArrayName="items">
  <div *ngFor="let item of dynamicForm.controls.items.controls; let i = index">
    <div [formGroupName]="i">
      <input type="text" formControlName="name">
      <textarea formControlName="description"></textarea>
      <button (click)="removeFormItem(i)">Remove</button>
    </div>
  </div>
  <button (click)="addFormItem()">Add Item</button>
</div>

Advanced Applications of Dynamic Forms

  • Conditional Rendering: Dynamically show or hide controls based on other control values.
  • Dynamic Validation: Enable or disable validation rules based on control values.
  • Dynamic Form Layout: Use Angular Flex Layout or CSS Grid frameworks to adapt to different screen sizes.

Share your love