Complete guide to template-based forms in Angular — covering
ngModel, validation, customControlValueAccessors, and dynamic forms.
Table of Contents
- Introduction to Template-driven Forms
- Getting Started with Template-driven Forms
- Working with Different Input Types
- Submitting a Form and Working with FormGroups
- Validating Template-driven Forms
- Creating Custom Controls and ControlValueAccessors
- Creating Dynamic Forms
- Quick Reference Tables
1. Introduction to Template-driven Forms
What is a Template-driven Form?
Angular offers two approaches to building forms:
| Approach | Description | Typical Use |
|---|---|---|
| Template-driven Forms | Form logic is defined in the HTML template via directives | Simple to moderately complex forms |
| Reactive Forms | Logic is defined in the component class in TypeScript | Very 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
nameattribute is required on any element usingngModel. Angular uses it to register the control in the parentFormGroup.
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.
Dropdown Lists (select)
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:
| Directive | Description | Example |
|---|---|---|
required | Non-empty value required | <input required /> |
minlength | Minimum string length | <input minlength="3" /> |
maxlength | Maximum string length | <input maxlength="50" /> |
min | Minimum numeric value | <input type="number" min="0" /> |
max | Maximum numeric value | <input type="number" max="100" /> |
email | Valid email format | <input type="email" email /> |
pattern | Must match a regex | <input pattern="[A-Za-z]+" /> |
requiredTrue | Value 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:
| Property | Inverse | Description |
|---|---|---|
valid | invalid | All validators pass |
dirty | pristine | The user has modified the value |
touched | untouched | The user clicked and left the field |
errors | — | Object 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
touchedto 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 Class | Condition |
|---|---|
ng-valid | The control is valid |
ng-invalid | The control is invalid |
ng-pristine | The user has not yet made changes |
ng-dirty | The user has modified the value |
ng-untouched | The user has not interacted |
ng-touched | The 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
addressGroupbecomes 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 provideruseExisting— references the class itself (notuseClass)validate()returns aValidationErrorsobject (invalid) ornull(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
| Step | What to do |
|---|---|
| 1. Provider | Add NG_VALUE_ACCESSOR with multi: true and forwardRef |
| 2. Interface | Implement ControlValueAccessor |
3. writeValue | Update the DOM/component state when the model changes |
4. registerOnChange | Store the callback to notify Angular of changes |
5. registerOnTouched | Store the callback to mark the control as touched |
| 6. Events | Call 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
| Directive | Module | Role |
|---|---|---|
NgForm | FormsModule | Directive automatically applied to <form>. Creates the root FormGroup. |
NgModel | FormsModule | Creates a FormControl for the element. Enables two-way data binding. |
NgModelGroup | FormsModule | Creates a nested FormGroup. Groups related fields. |
Built-in Validators (HTML Directives)
| Directive | Key in errors | Error Value |
|---|---|---|
required | required | true |
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 } |
email | email | true |
pattern="[A-Z]+" | pattern | { requiredPattern: ..., actualValue: ... } |
requiredTrue | required | true |
FormControl / NgModel States
| Property | Inverse | CSS Class | Description |
|---|---|---|---|
valid | invalid | ng-valid / ng-invalid | Validators satisfied or not |
pristine | dirty | ng-pristine / ng-dirty | Value unchanged / changed |
untouched | touched | ng-untouched / ng-touched | Field not visited / visited (focus + blur) |
errors | — | — | Object with active errors |
value | — | — | Current value |
Form Injection Tokens
| Token | Usage |
|---|---|
NG_VALIDATORS | Register a custom validator |
NG_VALUE_ACCESSOR | Register a custom ControlValueAccessor |
NG_ASYNC_VALIDATORS | Register an async validator |
ControlValueAccessor Interface Methods
| Method | Called by Angular when… | Responsibility |
|---|---|---|
writeValue(value) | The data model changes | Update the DOM / component state |
registerOnChange(fn) | Angular initializes the control | Store fn to notify Angular of a value change |
registerOnTouched(fn) | Angular initializes the control | Store fn to mark the control as touched |
setDisabledState(isDisabled) | The control is enabled/disabled | (Optional) Enable/disable the DOM |
Template Reference Variable Patterns
| Variable | Syntax | Gives 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
Validatorinterface with thevalidate(control: AbstractControl)method - Return a
ValidationErrorsobject if invalid, ornullif 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
providersof the@Componentor@Directive - Implement the
ControlValueAccessorinterface - Implement
writeValue(value)— update the DOM/state - Implement
registerOnChange(fn)— store and callfnon changes - Implement
registerOnTouched(fn)— store and callfnon 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