Angular Forms
Forms let users enter and edit data in your app.
Forms Essentials
- Two approaches: Template-driven (HTML-first with
[(ngModel)]
) and Reactive (code-first withFormGroup
/FormControl
). - When to use: Template-driven for simple forms; Reactive for complex validation, dynamic fields, and testability.
- Key concepts: A
FormControl
tracks a single input's value/state; aFormGroup
groups controls by name. - Imports:
FormsModule
(template-driven) andReactiveFormsModule
(reactive).
<form #f="ngForm">
<input name="name" [(ngModel)]="name">
</form>
form = new FormGroup({ name: new FormControl('') });
<form [formGroup]="form">
<input formControlName="name">
</form>
Notes:
- Related: See Data Binding for
[(ngModel)]
and property binding, Events for handling input, and Templates for interpolation. - Import
FormsModule
(template-driven) andReactiveFormsModule
(reactive).
Template-driven Forms
- Quick to start and feels like plain HTML.
- Bind with
[(ngModel)]
and uniquename
attributes. - Access overall state via exported
ngForm
(e.g.,valid
,touched
). - Import
FormsModule
in standalone components.
<form #f="ngForm" (ngSubmit)="onSubmit()">
<input name="name" [(ngModel)]="name" required minlength="3" #c="ngModel">
<div *ngIf="c.invalid && (c.dirty || c.touched)">Invalid</div>
<button [disabled]="f.invalid">Submit</button>
</form>
Example
import { bootstrapApplication } from '@angular/platform-browser';
import { Component } from '@angular/core';
import { CommonModule } from '@angular/common';
import { FormsModule } from '@angular/forms';
@Component({
selector: 'app-root',
standalone: true,
imports: [CommonModule, FormsModule],
template: `
<h3>Forms</h3>
<form #f="ngForm" (ngSubmit)="onSubmit()">
<label>
Name:
<input name="name" [(ngModel)]="name" placeholder="Enter your name">
</label>
<button type="submit">Submit</button>
</form>
<p>Value: {{ name }}</p>
<p *ngIf="submitted">Submitted!</p>
`
})
export class App {
name = '';
submitted = false;
onSubmit() { this.submitted = true; }
}
bootstrapApplication(App);
<app-root></app-root>
Example explained
- [(ngModel)]="name": Two-way binds the input to the
name
field. - #f="ngForm": Exports the form state (e.g.,
f.valid
,f.invalid
). - (ngSubmit)="onSubmit()": Handles submit using the component method.
- Display:
{{ name }}
shows the current value; a flag shows the submitted state.
Notes:
- Unique name required: Each control needs a unique
name
to register withngForm
. - Module imports: Template-driven forms require
FormsModule
(for standalone components, add it toimports
).
HTML Form Elements in Angular
- Text/Email/Number: Bind with
[(ngModel)]
orformControlName
. - Textarea: Works like text inputs.
- Checkbox: Boolean value via
[(ngModel)]
orformControlName
. - Radio group: Share the same
name
; bind group with[(ngModel)]
orformControlName
. Use[value]
. - Select: Bind selected value; use
[ngValue]
when options are objects. - File input: Read files with
(change)
handler; do not two-way bind file objects.
<input name="email" type="email" [(ngModel)]="model.email">
<textarea name="bio" [(ngModel)]="model.bio"></textarea>
<label><input type="checkbox" name="agree" [(ngModel)]="model.agree"> Agree</label>
<label><input type="radio" name="color" [value]="'red'" [(ngModel)]="model.color"> Red</label>
<label><input type="radio" name="color" [value]="'blue'" [(ngModel)]="model.color"> Blue</label>
<select name="pet" [(ngModel)]="model.pet">
<option [ngValue]="{ id: 1, name: 'Cat' }">Cat</option>
<option [ngValue]="{ id: 2, name: 'Dog' }">Dog</option>
</select>
<input type="file" (change)="onFiles($event)">
Tip: For object options in <select>
, use [ngValue]
instead of value
.
Radio with non-string values
- Use
[ngValue]
to bind non-string values (numbers or objects) to radios.
<label><input type="radio" name="size" [ngValue]="1" [(ngModel)]="model.size"> Small</label>
<label><input type="radio" name="size" [ngValue]="2" [(ngModel)]="model.size"> Medium</label>
Select multiple
- Add
multiple
and bind to an array; use[ngValue]
for non-strings.
<select name="tags" [(ngModel)]="model.tags" multiple>
<option [ngValue]="'news'">News</option>
<option [ngValue]="'tech'">Tech</option>
<option [ngValue]="'sports'">Sports</option>
</select>
Number inputs: coercion
- Template-driven binds values as strings; convert in code if you need numbers.
<input type="number" name="age" [ngModel]="age" (ngModelChange)="age = $any($event)">
File input (multiple)
- Read files with a
(change)
handler; do not two-way bind files.
<input type="file" multiple (change)="onFiles($event)">
compareWith for select of objects
- Use
[compareWith]
when options are objects that may be re-created across renders.
<select name="pet" [(ngModel)]="model.pet" [compareWith]="byId">
<option [ngValue]="{ id: 1, name: 'Cat' }">Cat</option>
<option [ngValue]="{ id: 2, name: 'Dog' }">Dog</option>
</select>
byId = (a: any, b: any) => a?.id === b?.id;
Validation
- Add rules like
required
,minlength
, andemail
. - Show errors when invalid and the control is
dirty
ortouched
, or after submit. - Disable submit when the form is invalid.
<input name="email" [(ngModel)]="email" email required #e="ngModel">
<div *ngIf="e.invalid && (e.dirty || e.touched)">
<small *ngIf="e.errors && e.errors['required']">Required</small>
<small *ngIf="e.errors && e.errors['email']">Invalid email</small>
</div>
Example
import { bootstrapApplication } from '@angular/platform-browser';
import { Component } from '@angular/core';
import { CommonModule } from '@angular/common';
import { FormsModule } from '@angular/forms';
@Component({
selector: 'app-root',
standalone: true,
imports: [CommonModule, FormsModule],
template: `
<h3>Forms Validation</h3>
<form #f="ngForm" (ngSubmit)="onSubmit()" novalidate>
<label>
Name:
<input name="name" [(ngModel)]="model.name" required minlength="3" #name="ngModel">
</label>
<div *ngIf="name.invalid && (name.dirty || name.touched || submitted)" style="color:crimson">
<small *ngIf="name.errors && name.errors['required']">Name is required.</small>
<small *ngIf="name.errors && name.errors['minlength']">Name must be at least 3 characters.</small>
</div>
<label>
Email:
<input name="email" [(ngModel)]="model.email" email required #email="ngModel">
</label>
<div *ngIf="email.invalid && (email.dirty || email.touched || submitted)" style="color:crimson">
<small *ngIf="email.errors && email.errors['required']">Email is required.</small>
<small *ngIf="email.errors && email.errors['email']">Email must be valid.</small>
</div>
<button type="submit" [disabled]="f.invalid">Submit</button>
</form>
<p *ngIf="submitted">Submitted: {{ model | json }}</p>
`
})
export class App {
model = { name: '', email: '' };
submitted = false;
onSubmit() { this.submitted = true; }
}
bootstrapApplication(App);
<app-root></app-root>
Example explained
- #name="ngModel": Exports the control state for the
name
input. - Errors:
name.errors['required']
andname.errors['minlength']
drive specific messages. - When to show: Messages appear when the control is invalid and
dirty || touched || submitted
. - Disable submit: The button binds to
f.invalid
to prevent invalid submission.
Notes:
- When to show errors: Gate messages behind
dirty || touched || submitted
so they don't flash too early. - Disable submit correctly: Bind to
f.invalid
(template) orform.invalid
(reactive).
Reactive Forms
- Build a tree of
FormGroup
/FormControl
in code. - Bind the template with
[formGroup]
andformControlName
. - Great for complex validation, conditional fields, and dynamic forms.
- Create controls with
FormBuilder
andValidators
.
form = this.fb.group({
name: ['', [Validators.required, Validators.minLength(3)]],
email: ['', [Validators.required, Validators.email]],
});
<form [formGroup]="form">
<input formControlName="name">
<input formControlName="email">
</form>
Example
import { bootstrapApplication } from '@angular/platform-browser';
import { Component } from '@angular/core';
import { CommonModule } from '@angular/common';
import { ReactiveFormsModule, FormBuilder, Validators } from '@angular/forms';
@Component({
selector: 'app-root',
standalone: true,
imports: [CommonModule, ReactiveFormsModule],
template: `
<h3>Reactive Forms</h3>
<form [formGroup]="form" (ngSubmit)="onSubmit()">
<label>
Name
<input formControlName="name" placeholder="Your name">
</label>
<div *ngIf="form.controls.name.invalid && (form.controls.name.dirty || form.controls.name.touched || submitted)" style="color:crimson">
<small *ngIf="form.controls.name.errors && form.controls.name.errors['required']">Name is required.</small>
<small *ngIf="form.controls.name.errors && form.controls.name.errors['minlength']">Min 3 characters.</small>
</div>
<label>
Email
<input formControlName="email" placeholder="you@example.com">
</label>
<div *ngIf="form.controls.email.invalid && (form.controls.email.dirty || form.controls.email.touched || submitted)" style="color:crimson">
<small *ngIf="form.controls.email.errors && form.controls.email.errors['required']">Email is required.</small>
<small *ngIf="form.controls.email.errors && form.controls.email.errors['email']">Email must be valid.</small>
</div>
<label>
<input type="checkbox" formControlName="newsletter">
Subscribe to newsletter
</label>
<button type="submit" [disabled]="form.invalid">Submit</button>
</form>
<p>Status: {{ form.status }}</p>
<p>Value: {{ form.value | json }}</p>
<p *ngIf="submitted" style="color: seagreen;">Submitted!</p>
`
})
export class App {
fb = new FormBuilder();
submitted = false;
form = this.fb.group({
name: ['', [Validators.required, Validators.minLength(3)]],
email: ['', [Validators.required, Validators.email]],
newsletter: [false],
});
onSubmit() { this.submitted = true; }
}
bootstrapApplication(App);
<app-root></app-root>
Example explained
- [formGroup]="form": Binds the form element to the
FormGroup
instance. - formControlName: Wires inputs to named controls (
name
,email
,newsletter
). - Validators: Created with
FormBuilder
andValidators
; error messages read control errors. - Submit: Disables the button when
form.invalid
; sets a submitted flag on submit.
Notes:
- Don't mix paradigms: Avoid
[(ngModel)]
on controls that also useformControlName
. - Update via API: Use
setValue
/patchValue
and validator methods rather than mutating control objects directly. - Module imports: Reactive forms require
ReactiveFormsModule
.