Intermediate

Angular: Template-driven Forms

Template-driven forms with ngModel, validation, custom ControlValueAccessors and dynamic forms.

Complete guide to template-based forms in Angular — covering ngModel, validation, custom ControlValueAccessors, and dynamic forms.


Table of Contents

  1. Introduction to Template-driven Forms
  2. Getting Started with Template-driven Forms
  3. Working with Different Input Types
  4. Submitting a Form and Working with FormGroups
  5. Validating Template-driven Forms
  6. Creating Custom Controls and ControlValueAccessors
  7. Creating Dynamic Forms
  8. Quick Reference Tables

1. Introduction to Template-driven Forms

What is a Template-driven Form?

Angular offers two approaches to building forms:

ApproachDescriptionTypical Use
Template-driven FormsForm logic is defined in the HTML template via directivesSimple to moderately complex forms
Reactive FormsLogic is defined in the component class in TypeScriptVery complex forms, advanced unit testing

Template-driven Forms rely on Angular’s FormsModule. They use the ngModel directive to create two-way data binding between HTML elements and the component’s data model.

Overall Architecture

flowchart TD
    A[FormsModule] --> B[ngModel Directive]
    B --> C[FormControl created implicitly]
    C --> D[Two-way Data Binding]
    D --> E[HTML Template ←→ TypeScript Data Model]

    F[ngForm Directive] --> G[Root FormGroup]
    G --> H[Groups all FormControls]
    H --> I[Tracks validity / dirty / touched]

    J[ngModelGroup Directive] --> K[Nested FormGroup]
    K --> L[Groups related fields]

Prerequisites

To use Template-driven Forms in a standalone Angular application (without NgModule):

// In the component
import { FormsModule } from '@angular/forms';

@Component({
  standalone: true,
  imports: [CommonModule, FormsModule], // ← required
  templateUrl: './edit-profile.component.html',
})
export class EditProfileComponent { }

2. Getting Started with Template-driven Forms

Creating a Data Model

Before building a form, define the TypeScript interfaces that represent the data.

// user.model.ts
export interface User {
  id: string;
  firstName: string;
  lastName: string;
  dateOfBirth: Date | null;
  favoritesRanking: number | null;
  phone: Phone;
  address: Address;
  notes: string;
}

export interface Phone {
  phoneNumber: string;
  phoneType: string;
}

export interface Address {
  streetAddress: string;
  city: string;
  state: string;
  postalCode: string;
  addressType: string;
}

In the component, initialize an instance of this model that will serve as the source of truth:

user: User = {
  id: '',
  personal: false,
  firstName: '',
  lastName: '',
  dateOfBirth: null,
  favoritesRanking: 0,
  phone: { phoneNumber: '', phoneType: '' },
  address: {
    streetAddress: '', city: '', state: '',
    postalCode: '', addressType: '',
  },
  notes: '',
};

The ngModel Directive and Two-way Data Binding

ngModel is the central directive of Template-driven Forms. It establishes two-way binding between an HTML field and a data model property.

flowchart LR
    A[Data Model\nuser.firstName] -- "writeValue()" --> B[HTML Input]
    B -- "onChange event" --> A
    style A fill:#e8f4fd
    style B fill:#fef9e7

Banana-in-a-box syntax: [(ngModel)] is a combination of:

  • [ngModel] — property binding (model → view)
  • (ngModelChange) — event binding (view → model)
<!-- One-way binding (model → view only) -->
<input [ngModel]="user.firstName" name="firstName" />

<!-- Two-way data binding -->
<input [(ngModel)]="user.firstName" name="firstName" />

Important: The name attribute is required on any element using ngModel. Angular uses it to register the control in the parent FormGroup.

Complete Example of a Basic Form

<form (ngSubmit)="saveUser(profileForm)" #profileForm="ngForm">
  <input
    placeholder="First Name"
    [(ngModel)]="user.firstName"
    name="firstName"
  />
  <input
    placeholder="Last Name"
    [(ngModel)]="user.lastName"
    name="lastName"
  />
  <button type="submit">Save</button>
</form>
saveUser(form: NgForm) {
  this.usersService.saveUser(form.value).subscribe({
    next: () => this.router.navigate(['/users'])
  });
}

Accessing NgForm in the Template

The ngForm directive is automatically applied to any <form> element. It can be accessed via a template reference variable:

<form #profileForm="ngForm" (ngSubmit)="saveUser(profileForm)">
  ...
</form>

profileForm is then an instance of NgForm that exposes:

  • .value — object representing all control values
  • .valid / .invalid — overall validity state
  • .dirty / .pristine — whether the user has made changes
  • .touched / .untouched — whether the user has interacted

3. Working with Different Input Types

Overview of ControlValueAccessors

Behind each form element, Angular injects a ControlValueAccessor that acts as a bridge between the HTML element and the Angular forms system.

flowchart TD
    A[ngModel directive] --> B{Element type?}
    B --> C["input[type=text]\nDefaultValueAccessor"]
    B --> D["input[type=checkbox]\nCheckboxValueAccessor"]
    B --> E["input[type=radio]\nRadioControlValueAccessor"]
    B --> F["select\nSelectControlValueAccessor"]
    B --> G["textarea\nDefaultValueAccessor"]
    B --> H["input[type=date]\n⚠️ No native one!"]
    H --> I[Custom DateValueAccessor\nrequired]

Text Fields (input[type=text])

The simplest case — uses the DefaultValueAccessor:

<input placeholder="First Name" [(ngModel)]="user.firstName" name="firstName" />

Numeric Fields (input[type=range] / input[type=number])

<div>
  <span>Favorites Ranking:</span>
  <input
    type="range"
    min="0" max="5"
    [(ngModel)]="user.favoritesRanking"
    name="favoritesRanking"
  />
  <span>{{ user.favoritesRanking }}</span>
</div>

Checkboxes (input[type=checkbox])

Angular uses the CheckboxValueAccessor. The bound value is a boolean:

<input type="checkbox" [(ngModel)]="user.personal" name="personal" />
Personal

Radio Buttons (input[type=radio])

Radio buttons share the same name but have different value attributes. Angular uses the RadioControlValueAccessor:

@for (phoneType of phoneTypes; track phoneType.value) {
  <input
    type="radio"
    [value]="phoneType.value"
    [(ngModel)]="user.phone.phoneType"
    name="phoneType"
  />
  {{ phoneType.title }}
}

Use [value] (property binding) to pass the actual value, not just a string.

Angular uses the SelectControlValueAccessor:

<select [(ngModel)]="user.address.addressType" name="addressType">
  @for (addressType of addressTypes; track addressType.value) {
    <option [value]="addressType.value">{{ addressType.title }}</option>
  }
</select>

Text Areas (textarea)

textarea elements work exactly like input[type=text] — the same DefaultValueAccessor is used:

<textarea
  placeholder="Notes"
  rows="3"
  [(ngModel)]="user.notes"
  name="notes"
></textarea>

Date Values — a Special Case

The DefaultValueAccessor treats all values as strings, regardless of the original JavaScript type. This causes problems with Date objects.

Problematic behavior:

// In the save method — for debugging purposes
saveUser(form: NgForm) {
  console.log(this.user.dateOfBirth, typeof this.user.dateOfBirth);
  // If we haven't modified the field: Date object ✓
  // If we modified the field via input: string ✗
}

Temporary solution: treat the date as a string in the data model.

Definitive solution: create a custom DateValueAccessor (see Module 6).


4. Submitting a Form and Working with FormGroups

The ngModelGroup Directive

ngModelGroup allows grouping fields into a nested object, matching the data model structure.

<!-- Without ngModelGroup: form.value = { phoneNumber: '...', phoneType: '...' } -->

<!-- With ngModelGroup: form.value = { phone: { phoneNumber: '...', phoneType: '...' } } -->
<div ngModelGroup="phone">
  <input [(ngModel)]="user.phone.phoneNumber" name="phoneNumber" />
  <input [(ngModel)]="user.phone.phoneType" name="phoneType" />
</div>

Structure of Submitted Data

flowchart TD
    A["form.value"] --> B["id: ''"]
    A --> C["firstName: 'Jane'"]
    A --> D["lastName: 'Smith'"]
    A --> E["phone (ngModelGroup)"]
    E --> F["phoneNumber: '555-9876'"]
    E --> G["phoneType: 'mobile'"]
    A --> H["address (ngModelGroup)"]
    H --> I["streetAddress: '...'\ncity: '...'\nstate: '...'\n..."]

Submitting with ngForm vs the Data Model

Approach with ngForm (retrieves values from the form):

saveUser(form: NgForm) {
  this.usersService.saveUser(form.value).subscribe(...);
}

Preferred approach (uses the data model directly):

saveUser(form: NgForm) {
  if (form.invalid) return;
  this.usersService.saveUser(this.user).subscribe(...);
}

The data model approach is preferred because it preserves TypeScript types (no string conversion) and better handles dynamic forms with arrays.

Disabling the Save Button When the Form is Invalid

<button
  type="submit"
  class="primary"
  [disabled]="profileForm.invalid"
>
  Save
</button>

5. Validating Template-driven Forms

Built-in Validators

Angular provides several ready-to-use validation directives:

DirectiveDescriptionExample
requiredNon-empty value required<input required />
minlengthMinimum string length<input minlength="3" />
maxlengthMaximum string length<input maxlength="50" />
minMinimum numeric value<input type="number" min="0" />
maxMaximum numeric value<input type="number" max="100" />
emailValid email format<input type="email" email />
patternMust match a regex<input pattern="[A-Za-z]+" />
requiredTrueValue must be true (checkbox)<input type="checkbox" requiredTrue />

The FormControl and Its State Properties

When ngModel is added to an element, Angular creates a FormControl in the background. This FormControl exposes several state properties:

PropertyInverseDescription
validinvalidAll validators pass
dirtypristineThe user has modified the value
toucheduntouchedThe user clicked and left the field
errorsObject containing active errors

Accessing the FormControl in the Template

Use a template reference variable on the element:

<input
  placeholder="First Name"
  [(ngModel)]="user.firstName"
  required
  minlength="3"
  name="firstName"
  #firstName="ngModel"
  [class.error]="firstName.invalid && firstName.touched"
/>

Displaying Targeted Error Messages

<input
  placeholder="First Name"
  [(ngModel)]="user.firstName"
  required
  minlength="3"
  name="firstName"
  #firstName="ngModel"
  [class.error]="firstName.invalid && firstName.touched"
/>

@if (firstName.errors?.['required'] && firstName.touched) {
  <em>Please enter a First Name</em>
}
@if (firstName.errors?.['minlength'] && firstName.touched) {
  <em>First Name must be at least 3 characters</em>
}

Errors are conditioned on touched to avoid showing them before the user has interacted with the field.

Validation Pipeline

flowchart LR
    A[User types] --> B[Validators evaluate\nthe new value]
    B --> C{Valid?}
    C -- Yes --> D["errors = null\nFormControl.valid = true"]
    C -- No --> E["errors = { required: true, minlength: {...} }\nFormControl.invalid = true"]
    D --> F[CSS class ng-valid]
    E --> G[CSS class ng-invalid]

Angular’s Automatic CSS States

Angular automatically applies CSS classes to form elements based on their state:

CSS ClassCondition
ng-validThe control is valid
ng-invalidThe control is invalid
ng-pristineThe user has not yet made changes
ng-dirtyThe user has modified the value
ng-untouchedThe user has not interacted
ng-touchedThe user has interacted (focus + blur)
/* Example styles using Angular classes */
input.ng-invalid.ng-touched {
  border: 2px solid red;
}
input.ng-valid.ng-dirty {
  border: 2px solid green;
}

Validation at the FormGroup Level (ngModelGroup)

By adding a template reference variable on ngModelGroup, you can validate an entire group:

<div
  ngModelGroup="address"
  #addressGroup="ngModelGroup"
  [class.error]="addressGroup.invalid && addressGroup.dirty"
>
  <input [(ngModel)]="user.address.streetAddress" name="streetAddress" required />
  <input [(ngModel)]="user.address.city" name="city" required />
  <input [(ngModel)]="user.address.state" name="state" required />
  <input [(ngModel)]="user.address.postalCode" name="postalCode" required />
  <select [(ngModel)]="user.address.addressType" name="addressType">
    ...
  </select>
</div>

@if (addressGroup.invalid && addressGroup.dirty) {
  <em>Incomplete Address</em>
}

If a single field in the group is invalid, the entire group addressGroup becomes invalid.

Creating a Custom Validator

In Template-driven Forms, a custom validator is an Angular directive:

Basic structure:

// restricted-words-validator.directive.ts
import { Directive, Input } from '@angular/core';
import {
  AbstractControl,
  NG_VALIDATORS,
  ValidationErrors,
  Validator
} from '@angular/forms';

@Directive({
  selector: '[restrictedWords]',
  standalone: true,
  providers: [{
    provide: NG_VALIDATORS,
    multi: true,
    useExisting: RestrictedWordsValidator,
  }],
})
export class RestrictedWordsValidator implements Validator {
  @Input('restrictedWords') restrictedWords: string[] = [];

  validate(control: AbstractControl): null | ValidationErrors {
    if (!control.value) return null;  // ← guard against null

    const invalidWords = this.restrictedWords
      .map(w => control.value.includes(w) ? w : null)
      .filter(w => w !== null);

    return invalidWords.length > 0
      ? { restrictedWords: invalidWords.join(',') }
      : null;
  }
}

Key points:

  • NG_VALIDATORS + multi: true — registers the directive as a validation provider
  • useExisting — references the class itself (not useClass)
  • validate() returns a ValidationErrors object (invalid) or null (valid)
  • Properties of the returned object are added to control.errors

Usage in the template:

<!-- Import of the directive in the component required -->
<textarea
  [(ngModel)]="user.notes"
  name="notes"
  [restrictedWords]="['spam', 'junk']"
  #notes="ngModel"
  [class.error]="notes.invalid"
></textarea>

@if (notes.errors?.['restrictedWords']) {
  <em>Restricted words found: {{ notes.errors?.['restrictedWords'] }}</em>
}

Import the directive in the component:

@Component({
  imports: [CommonModule, FormsModule, RestrictedWordsValidator],
  ...
})
export class EditProfileComponent { }

6. Creating Custom Controls and ControlValueAccessors

What is a ControlValueAccessor?

A ControlValueAccessor is the interface that bridges an HTML element (or custom component) and the Angular forms system.

sequenceDiagram
    participant DM as Data Model
    participant CVA as ControlValueAccessor
    participant HTML as HTML Element

    DM->>CVA: Value change
    CVA->>CVA: writeValue(newValue)
    CVA->>HTML: Updates displayed value

    HTML->>CVA: input/change event
    CVA->>CVA: onChange(newValue)
    CVA->>DM: Updates data model

The ControlValueAccessor Interface

interface ControlValueAccessor {
  writeValue(obj: any): void;          // Model → View
  registerOnChange(fn: any): void;     // Registers the onChange callback
  registerOnTouched(fn: any): void;    // Registers the onTouched callback
  setDisabledState?(isDisabled: boolean): void; // Optional
}

Creating a Custom DateValueAccessor

Angular provides no native ControlValueAccessor for input[type=date]. You must create one.

// date-value-accessor.directive.ts
import {
  Directive, ElementRef, HostListener, Provider, forwardRef
} from '@angular/core';
import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms';

const DATE_VALUE_PROVIDER: Provider = {
  provide: NG_VALUE_ACCESSOR,
  useExisting: forwardRef(() => DateValueAccessorDirective),
  multi: true,
};

@Directive({
  // Targeted selector: applies only to input[type=date] with ngModel/formControl
  selector: `input([type=date])[ngModel],
             input([type=date])[formControl],
             input([type=date])[formControlName]`,
  standalone: true,
  providers: [DATE_VALUE_PROVIDER],
})
export class DateValueAccessorDirective implements ControlValueAccessor {

  constructor(private element: ElementRef) { }

  // Listens to the 'input' event and passes valueAsDate (Date object) to onChange
  @HostListener('input', ['$event.target.valueAsDate'])
  private onChange!: Function;

  // Listens to blur to mark the control as 'touched'
  @HostListener('blur', [])
  private onTouched!: Function;

  // Called when the data model changes → updates the HTML element
  writeValue(newValue: any) {
    if (newValue instanceof Date) {
      // input[type=date] expects a string in YYYY-MM-DD format
      this.element.nativeElement.value = newValue.toISOString().split('T')[0];
    }
  }

  // Angular calls this method to give us the onChange callback
  registerOnChange(fn: Function) {
    this.onChange = (valueAsDate: Date) => { fn(valueAsDate); };
  }

  // Angular calls this method to give us the onTouched callback
  registerOnTouched(fn: Function) {
    this.onTouched = fn;
  }
}

Directive flow:

flowchart TD
    A["Data Model\nuser.dateOfBirth: Date"] -- "writeValue(date)" --> B["DateValueAccessorDirective"]
    B -- "toISOString().split('T')[0]\n→ '2024-11-09'" --> C["input[type=date]\nvalue = '2024-11-09'"]
    C -- "input event\ntarget.valueAsDate" --> B
    B -- "fn(dateObject)" --> A

Creating a Custom Component with ControlValueAccessor

To bind a custom Angular component to the forms system, implement ControlValueAccessor directly on the component.

AvatarSelectorComponent component:

// avatar-selector.component.ts
import { CommonModule } from '@angular/common';
import { Component, Provider, forwardRef } from '@angular/core';
import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms';
import { avatarIconNames } from './avatar-icon-names';

const AVATAR_ICON_VALUE_ACCESSOR: Provider = {
  provide: NG_VALUE_ACCESSOR,
  useExisting: forwardRef(() => AvatarSelectorComponent),
  multi: true,
};

@Component({
  selector: 'app-avatar-selector',
  standalone: true,
  imports: [CommonModule],
  templateUrl: './avatar-selector.component.html',
  providers: [AVATAR_ICON_VALUE_ACCESSOR],  // ← registration as CVA
})
export class AvatarSelectorComponent implements ControlValueAccessor {
  avatarIcons = avatarIconNames;
  showAllIcons: boolean = true;
  selectedIcon!: string | null;

  onChange!: Function;
  onTouched!: Function;

  // Called when the user clicks on an icon
  iconSelected(icon: string) {
    this.showAllIcons = false;
    this.selectedIcon = icon;
    this.onChange(icon);  // ← notifies Angular of the change
  }

  // Model → View: updates the component state
  writeValue(icon: string | null) {
    this.selectedIcon = icon;
    this.showAllIcons = !icon || icon === '';
  }

  registerOnChange(fn: Function) {
    this.onChange = (icon: string) => { fn(icon); };
  }

  registerOnTouched(fn: Function) {
    this.onTouched = fn;
  }
}

Usage in the parent template:

<!-- Works exactly like a native input! -->
<app-avatar-selector
  [(ngModel)]="user.icon"
  name="icon"
/>

Import in the parent component:

@Component({
  imports: [
    CommonModule,
    FormsModule,
    RestrictedWordsValidator,
    DateValueAccessorDirective,
    AvatarSelectorComponent,  // ← the custom CVA component
  ],
  ...
})
export class EditProfileComponent { }

Summary: Implementing a ControlValueAccessor

StepWhat to do
1. ProviderAdd NG_VALUE_ACCESSOR with multi: true and forwardRef
2. InterfaceImplement ControlValueAccessor
3. writeValueUpdate the DOM/component state when the model changes
4. registerOnChangeStore the callback to notify Angular of changes
5. registerOnTouchedStore the callback to mark the control as touched
6. EventsCall onChange on every change, onTouched on blur

7. Creating Dynamic Forms

The Problem with Arrays and ngForm

When a form uses an array of objects, the submitted value via form.value does not preserve the array structure:

// What we expect:
form.value.phones = [
  { phoneNumber: '555-1234', phoneType: 'mobile' },
  { phoneNumber: '555-5678', phoneType: 'work' }
]

// What ngForm actually returns:
form.value.phones = {
  phoneNumber0: '555-1234',
  phoneType0: 'mobile',
  phoneNumber1: '555-5678',
  phoneType1: 'work'
}

Solution: Use the data model directly rather than form.value.

Updating the Data Model to Support Arrays

Before (single phone):

user: User = {
  phone: { phoneNumber: '', phoneType: '' },
  ...
};

After (array of phones):

user: User = {
  phones: [{ phoneNumber: '', phoneType: '' }],
  ...
};
// user.model.ts — updated interface
export interface User {
  id: string;
  phones: Phone[];  // ← array instead of a single object
  ...
}

Template with @for and Dynamic Names

To render repeated elements, each name must be unique:

<div ngModelGroup="phones">
  @for (phone of user.phones; track phone; let i = $index) {
    <div class="flex-column">
      <div class="flex-group">
        <!-- dynamic name with index for uniqueness -->
        <input
          placeholder="Phone"
          [(ngModel)]="phone.phoneNumber"
          name="phoneNumber{{ i }}"
        />
        <img
          src="/assets/plus-grey-blue.png"
          class="add"
          (click)="addPhone()"
        />
      </div>
      <div class="radio">
        @for (phoneType of phoneTypes; track phoneType.value) {
          <input
            type="radio"
            [value]="phoneType.value"
            [(ngModel)]="phone.phoneType"
            name="phoneType{{ i }}"
          />
          {{ phoneType.title }}
        }
      </div>
    </div>
  }
</div>

Dynamically Adding Elements

// edit-profile.component.ts
addPhone() {
  this.user.phones.push({
    phoneNumber: '',
    phoneType: '',
  });
  // The template updates automatically thanks to @for!
}

Angular detects the change in the array and re-renders the template automatically. This is the power of data binding combined with @for.

Submitting a Dynamic Form Correctly

saveUser(form: NgForm) {
  if (form.invalid) return;

  // ✅ Use this.user (data model) — not form.value
  // The data model correctly preserves the array structure
  this.usersService.saveUser(this.user).subscribe({
    next: () => this.router.navigate(['/users'])
  });
}
<!-- Still pass ngForm to access form.invalid -->
<form (ngSubmit)="saveUser(profileForm)" #profileForm="ngForm">
  ...
  <button type="submit" [disabled]="profileForm.invalid">Save</button>
</form>

Complete Final Form (Module 7)

TypeScript Component:

// edit-profile.component.ts — final version
import { CommonModule } from '@angular/common';
import { Component, OnInit } from '@angular/core';
import { ActivatedRoute, Router } from '@angular/router';
import { FormsModule, NgForm } from '@angular/forms';
import { User, phoneTypeValues, addressTypeValues } from '../users/user.model';
import { UsersService } from '../users/users.service';
import { RestrictedWordsValidator } from '../validators/restricted-words-validator.directive';
import { DateValueAccessorDirective } from '../date-value-accessor/date-value-accessor.directive';
import { AvatarSelectorComponent } from '../avatar-selector/avatar-selector.component';

@Component({
  imports: [
    CommonModule, FormsModule,
    RestrictedWordsValidator,
    DateValueAccessorDirective,
    AvatarSelectorComponent,
  ],
  standalone: true,
  templateUrl: './edit-profile.component.html',
})
export class EditProfileComponent implements OnInit {
  phoneTypes = phoneTypeValues;
  addressTypes = addressTypeValues;

  user: User = {
    id: '', icon: '', personal: false,
    firstName: '', lastName: '',
    dateOfBirth: null,
    favoritesRanking: 0,
    phones: [{ phoneNumber: '', phoneType: '' }],
    address: {
      streetAddress: '', city: '', state: '',
      postalCode: '', addressType: '',
    },
    notes: '',
  };

  constructor(
    private route: ActivatedRoute,
    private usersService: UsersService,
    private router: Router) { }

  ngOnInit() {
    const userId = this.route.snapshot.params['id'];
    if (!userId) return;
    this.usersService.getUser(userId).subscribe((user) => {
      if (user) this.user = user;
    });
  }

  addPhone() {
    this.user.phones.push({ phoneNumber: '', phoneType: '' });
  }

  saveUser(form: NgForm) {
    if (form.invalid) return;
    this.usersService.saveUser(this.user).subscribe({
      next: () => this.router.navigate(['/users'])
    });
  }
}

8. Quick Reference Tables

Template-driven Forms Directives

DirectiveModuleRole
NgFormFormsModuleDirective automatically applied to <form>. Creates the root FormGroup.
NgModelFormsModuleCreates a FormControl for the element. Enables two-way data binding.
NgModelGroupFormsModuleCreates a nested FormGroup. Groups related fields.

Built-in Validators (HTML Directives)

DirectiveKey in errorsError Value
requiredrequiredtrue
minlength="3"minlength{ requiredLength: 3, actualLength: 1 }
maxlength="50"maxlength{ requiredLength: 50, actualLength: 55 }
min="0"min{ min: 0, actual: -1 }
max="100"max{ max: 100, actual: 150 }
emailemailtrue
pattern="[A-Z]+"pattern{ requiredPattern: ..., actualValue: ... }
requiredTruerequiredtrue

FormControl / NgModel States

PropertyInverseCSS ClassDescription
validinvalidng-valid / ng-invalidValidators satisfied or not
pristinedirtyng-pristine / ng-dirtyValue unchanged / changed
untouchedtouchedng-untouched / ng-touchedField not visited / visited (focus + blur)
errorsObject with active errors
valueCurrent value

Form Injection Tokens

TokenUsage
NG_VALIDATORSRegister a custom validator
NG_VALUE_ACCESSORRegister a custom ControlValueAccessor
NG_ASYNC_VALIDATORSRegister an async validator

ControlValueAccessor Interface Methods

MethodCalled by Angular when…Responsibility
writeValue(value)The data model changesUpdate the DOM / component state
registerOnChange(fn)Angular initializes the controlStore fn to notify Angular of a value change
registerOnTouched(fn)Angular initializes the controlStore fn to mark the control as touched
setDisabledState(isDisabled)The control is enabled/disabled(Optional) Enable/disable the DOM

Template Reference Variable Patterns

VariableSyntaxGives Access To
Form reference#myForm="ngForm"NgForm instance
Control reference#myInput="ngModel"NgModel instance (FormControl)
Group reference#myGroup="ngModelGroup"NgModelGroup instance (FormGroup)

Checklist — Creating a Custom Validator

  • Create a directive class with @Directive({ selector: '[myValidator]', standalone: true })
  • Add the provider { provide: NG_VALIDATORS, multi: true, useExisting: MyValidator }
  • Implement the Validator interface with the validate(control: AbstractControl) method
  • Return a ValidationErrors object if invalid, or null if valid
  • Add @Input() if parameters need to be passed from the template
  • Import the directive in the component that uses it

Checklist — Creating a Custom ControlValueAccessor

  • Create the provider { provide: NG_VALUE_ACCESSOR, useExisting: forwardRef(() => MyClass), multi: true }
  • Add the provider in providers of the @Component or @Directive
  • Implement the ControlValueAccessor interface
  • Implement writeValue(value) — update the DOM/state
  • Implement registerOnChange(fn) — store and call fn on changes
  • Implement registerOnTouched(fn) — store and call fn on blur
  • Call this.onChange(value) when the value changes in the component
  • Import the component/directive in any parent component that uses it

Search Terms

angular · template-driven · forms · frontend · development · form · controlvalueaccessor · custom · data · input · template · type · dynamic · formcontrol · model · ngform · submitting · accessing · arrays · built-in · checklist · controlvalueaccessors · directive · directives

Interested in this course?

Contact us to book it or get a custom training plan for your team.