Angular provides two approaches to forms: template-driven (covered here) and reactive. Template-driven forms use FormsModule and keep most form logic in the template via directives.
1. FormsModule and ngSubmit
Import FormsModule into the component’s imports array to enable template-driven form directives.
When FormsModule is imported, Angular automatically attaches an NgForm directive to every <form> element. This directive tracks the form’s state (validity, touched, dirty) and intercepts the native browser submit event.
Use (ngSubmit) instead of the native (submit) — NgForm suppresses the default browser navigation and gives you the Angular-managed form object.
@Component({
selector: 'app-login',
templateUrl: './login.html',
imports: [FormsModule],
})
export class LoginComponent {
onSubmit() {
console.log('form submitted');
}
}
Enter fullscreen mode Exit fullscreen mode
<!-- login.html -->
<form (ngSubmit)="onSubmit()">
<input type="email" name="email" />
<button type="submit">Log in</button>
</form>
Enter fullscreen mode Exit fullscreen mode
Every
<input>inside a template-driven form must have anameattribute — Angular uses it to register the control withNgForm.
2. Two-way binding with [(ngModel)]
ngModel (from FormsModule) binds an input’s value to a component property in both directions: the DOM updates when the property changes, and the property updates when the user types.
export class LoginComponent {
email = '';
password = '';
onSubmit() {
console.log(this.email, this.password);
}
}
Enter fullscreen mode Exit fullscreen mode
<form (ngSubmit)="onSubmit()">
<input type="email" name="email" [(ngModel)]="email" />
<input type="password" name="password" [(ngModel)]="password" />
<button type="submit">Log in</button>
</form>
Enter fullscreen mode Exit fullscreen mode
[(ngModel)] is syntactic sugar for [ngModel]="email" (ngModelChange)="email = $event". The square brackets push the value in; the parentheses listen for changes coming out.
3. Template variables
A template variable is declared with a # prefix on any element or component in the template. It is similar to a ref in React — it gives you a direct handle to that element or component instance within the same template.
<input #emailInput type="email" />
<button (click)="emailInput.focus()">Focus email</button>
Enter fullscreen mode Exit fullscreen mode
emailInput is a reference to the native HTMLInputElement.
Template variables on custom components
When you place #ref on a custom component, Angular assigns the variable to the component class instance, not the underlying DOM element.
<!-- myRef is the CardComponent instance, not an HTMLElement -->
<app-card #myRef />
<button (click)="myRef.reset()">Reset card</button>
Enter fullscreen mode Exit fullscreen mode
This lets you call methods or read properties directly on the component from within the template.
Referencing the form with NgForm
When FormsModule is imported, Angular attaches an NgForm directive to every <form>. You can capture that directive instance by assigning ngForm to a template variable:
<form #loginForm="ngForm" (ngSubmit)="onSubmit(loginForm)">
<input type="email" name="email" ngModel />
<input type="password" name="password" ngModel />
<button type="submit">Log in</button>
<button type="button" (click)="loginForm.reset()">Reset</button>
</form>
Enter fullscreen mode Exit fullscreen mode
import { NgForm } from '@angular/forms';
export class LoginComponent {
onSubmit(form: NgForm) {
if (form.valid) {
console.log(form.value); // { email: '...', password: '...' }
}
}
}
Enter fullscreen mode Exit fullscreen mode
form.value is a plain object keyed by each input’s name attribute. loginForm.reset() clears all field values and resets validation state.
Without NgForm — binding fields to component member variables
If you don’t need NgForm‘s validity tracking or form.value object, bind each field to a component property with [(ngModel)] and use a plain template variable to get the native HTMLFormElement for reset.
<form #loginForm (ngSubmit)="onSubmit()">
<input type="email" name="email" [(ngModel)]="email" />
<input type="password" name="password" [(ngModel)]="password" />
<button type="submit">Log in</button>
<button type="button" (click)="reset(loginForm)">Reset</button>
</form>
Enter fullscreen mode Exit fullscreen mode
export class LoginComponent {
email = '';
password = '';
onSubmit() {
console.log(this.email, this.password);
}
reset(form: HTMLFormElement) {
this.email = ''; // [(ngModel)] propagates '' back to the input
this.password = '';
form.reset(); // also resets native browser state (validation highlights, autofill)
}
}
Enter fullscreen mode Exit fullscreen mode
#loginForm without ="ngForm" is a reference to the native HTMLFormElement — the DOM element itself, not the NgForm directive. So form in reset(form: HTMLFormElement) is the HTMLFormElement, and form.reset() is the native DOM method.
The difference between the two approaches (#form="ngForm" vs plain #form) is covered in the table below. For accessing the form element from the component class instead of passing it through a method argument, see @ViewChild in Section 5.
#form="ngForm"
#form (no assignment)
Type
NgForm directive instance
HTMLFormElement
form.value
Object of all field values
Not available
Validity tracking
form.valid, form.dirty, etc.
Not available
Reset
form.reset() (Angular)
form.reset() (native DOM)
When to use
When you need validation state
When fields are bound via [(ngModel)]
4. Form validation
When FormsModule is imported, Angular recognises standard HTML validation attributes (required, email, minlength, etc.) and tracks validity state on each control automatically.
Per-field validation with a template variable
Bind ngModel to a template variable to get the NgModel instance for that control and read its state directly in the template:
<form #signUpForm="ngForm" (ngSubmit)="onSubmit(signUpForm)">
<input name="email" ngModel required email #emailCtrl="ngModel" />
@if (emailCtrl.touched && emailCtrl.invalid) {
<p class="error">Enter a valid email address.</p>
}
<input name="password" ngModel required minlength="8" #passwordCtrl="ngModel" />
@if (passwordCtrl.touched && passwordCtrl.errors?.['minlength']) {
<p class="error">Password must be at least 8 characters.</p>
}
<button type="submit" [disabled]="signUpForm.invalid">Submit</button>
</form>
Enter fullscreen mode Exit fullscreen mode
import { NgForm } from '@angular/forms';
export class SignUpComponent {
onSubmit(form: NgForm) {
if (form.valid) {
console.log(form.value); // { email: '...', password: '...' }
form.reset();
}
}
}
Enter fullscreen mode Exit fullscreen mode
Key properties available on both NgModel (per-field) and NgForm (whole form):
valid / invalid
Passes / fails all validators
touched / untouched
Has / has not been focused and blurred
dirty / pristine
Value has / has not been changed by the user
errors
Object with error keys, e.g. { required: true }, { minlength: { ... } }
CSS classes added by Angular
Angular keeps these classes in sync on every form input automatically:
Class pair What it signalsng-pristine / ng-dirty
Value unchanged / changed by user
ng-untouched / ng-touched
Not yet focused / has been focused
ng-valid / ng-invalid
All validators pass / at least one fails
Use them in CSS to give validation feedback without extra logic in the component:
input.ng-touched.ng-invalid {
border-color: red;
}
input.ng-touched.ng-valid {
border-color: green;
}
Enter fullscreen mode Exit fullscreen mode
5. @ViewChild — querying a single element (decorator)
@ViewChild lets you access a template element, component, or directive from inside the component class. The result is available from ngAfterViewInit onwards — not in ngOnInit, because the view hasn’t been built yet.
@ViewChild accepts:
#variable points to
TemplateRef
An <ng-template> element
Example A — querying an NgForm directive instance
When the template variable uses ="ngForm", @ViewChild typed as NgForm gives you the directive directly — no .nativeElement needed.
<form #loginForm="ngForm" (ngSubmit)="onSubmit()">
<input type="email" name="email" ngModel />
<button type="submit">Log in</button>
<button type="button" (click)="resetForm()">Reset</button>
</form>
Enter fullscreen mode Exit fullscreen mode
import { ViewChild, AfterViewInit } from '@angular/core';
import { NgForm } from '@angular/forms';
export class LoginComponent implements AfterViewInit {
@ViewChild('loginForm') form!: NgForm;
ngAfterViewInit() {
console.log(this.form.value); // NgForm is available here
}
resetForm() {
this.form.reset();
}
}
Enter fullscreen mode Exit fullscreen mode
Example B — querying a native HTML element
When the template variable has no ="..." assignment, @ViewChild on a plain HTML element returns an ElementRef — a wrapper around the DOM node. The actual element is at .nativeElement.
<form #loginForm (ngSubmit)="onSubmit()">
<input type="email" name="email" [(ngModel)]="email" />
<input type="password" name="password" [(ngModel)]="password" />
<button type="submit">Log in</button>
<button type="button" (click)="reset()">Reset</button>
</form>
Enter fullscreen mode Exit fullscreen mode
import { ViewChild, ElementRef, AfterViewInit } from '@angular/core';
export class LoginComponent implements AfterViewInit {
@ViewChild('loginForm') loginFormEl!: ElementRef<HTMLFormElement>;
email = '';
password = '';
onSubmit() {
console.log(this.email, this.password);
}
reset() {
this.email = '';
this.password = '';
this.loginFormEl.nativeElement.reset(); // native DOM reset for browser-level state
}
}
Enter fullscreen mode Exit fullscreen mode
This is the @ViewChild equivalent of Option A from Section 3 — the difference is that the form element lives on the class rather than being passed as a method argument.
@ViewChildren — querying multiple elements
@ViewChildren selects all matching elements or directives in the view and returns a live QueryList. Useful when you have a dynamic list of child components.
import { ViewChildren, QueryList, AfterViewInit } from '@angular/core';
import { NgModel } from '@angular/forms';
@Component({
selector: 'app-signup',
templateUrl: './signup.html',
imports: [FormsModule],
})
export class SignupComponent implements AfterViewInit {
@ViewChildren(NgModel) controls!: QueryList<NgModel>;
ngAfterViewInit() {
this.controls.forEach(control => {
console.log(control.name, control.valid);
});
}
}
Enter fullscreen mode Exit fullscreen mode
QueryList is live — it updates when the view changes (e.g. when @if adds or removes elements). Subscribe to controls.changes to react to those updates.
6. viewChild() and viewChildren() — signal-based queries (Angular 17+)
Angular 17 introduced viewChild() and viewChildren() as function-based, signal alternatives to @ViewChild and @ViewChildren. They return signals, so the value is always up to date and composable with computed() and effect().
viewChild()
import { viewChild } from '@angular/core';
import { NgForm } from '@angular/forms';
export class LoginComponent {
form = viewChild<NgForm>('loginForm'); // signal<NgForm | undefined>
resetForm() {
this.form()?.reset(); // call the signal to read its current value
}
}
Enter fullscreen mode Exit fullscreen mode
No need to implement AfterViewInit — the signal is updated automatically when the view initializes.
viewChildren()
import { viewChildren } from '@angular/core';
import { NgModel } from '@angular/forms';
export class SignupComponent {
controls = viewChildren(NgModel); // signal<readonly NgModel[]>
logAll() {
this.controls().forEach(c => console.log(c.name, c.valid));
}
}
Enter fullscreen mode Exit fullscreen mode
viewChildren() returns a signal containing a readonly array (not a QueryList), which makes it straightforward to use in reactive contexts.
7. @ContentChild / contentChild() — querying projected content
@ContentChild (decorator) and contentChild() (signal, Angular 17+) work like their View equivalents, but they query content projected into the component via <ng-content> rather than elements defined in the component’s own template. They are rarely needed in day-to-day development.
// component that receives projected content
@Component({
selector: 'app-panel',
template: `
<div class="panel">
<ng-content />
</div>
`,
})
export class PanelComponent implements AfterContentInit {
@ContentChild('panelTitle') titleEl!: ElementRef;
ngAfterContentInit() {
console.log(this.titleEl.nativeElement.textContent);
}
}
Enter fullscreen mode Exit fullscreen mode
<!-- parent template -->
<app-panel>
<h2 #panelTitle>My Panel</h2>
<p>Some content</p>
</app-panel>
Enter fullscreen mode Exit fullscreen mode
The signal version with contentChild():
import { contentChild, ElementRef } from '@angular/core';
export class PanelComponent {
titleEl = contentChild<ElementRef>('panelTitle');
}
Enter fullscreen mode Exit fullscreen mode
@ContentChildren / contentChildren() follow the same pattern and return all matches instead of just the first.
8. Related lifecycle hooks
Hook Triggered when Use forngAfterViewInit
Once, after the component’s own template and all child views are initialized
First access to @ViewChild / viewChild() results
ngAfterViewChecked
After every change detection check of the component’s view
Rarely needed; reading DOM state after each render
ngAfterContentInit
Once, after projected content (<ng-content>) is initialized
First access to @ContentChild / contentChild() results
ngAfterContentChecked
After every change detection check of projected content
Rarely needed
With signal queries (viewChild(), contentChild()), you generally don’t need to implement these hooks — the signal itself handles timing. With decorator queries (@ViewChild, @ContentChild), access the result only inside or after the corresponding After*Init hook.
답글 남기기