AngularJSTemplate-Driven Forms

Template-Driven Forms

Template-driven forms let you build forms primarily in the HTML template using Angular directives. Angular reads the template and automatically constructs the underlying form model — you write less TypeScript at the cost of less programmatic control.

They are ideal for straightforward forms: login, contact, basic settings.

Setup: Importing FormsModule

TS
// Standalone component
import { Component } from '@angular/core';
import { FormsModule } from '@angular/forms';

@Component({
  standalone: true,
  imports: [FormsModule],  // enables ngModel, ngForm, ngModelGroup
  templateUrl: './login.component.html',
})
export class LoginComponent {}

// NgModule-based app — import once in the module
import { NgModule } from '@angular/core';
import { FormsModule } from '@angular/forms';

@NgModule({
  imports: [FormsModule],
})
export class AppModule {}
Your First Template-Driven Form

HTML
<!-- login.component.html -->
<form #loginForm="ngForm" (ngSubmit)="onSubmit(loginForm)">

  <label for="email">Email</label>
  <input
    id="email"
    type="email"
    name="email"          <!-- required for ngModel tracking -->
    ngModel              <!-- binds to Angular's form model -->
    required
    email
  />

  <label for="password">Password</label>
  <input
    id="password"
    type="password"
    name="password"
    ngModel
    required
    minlength="8"
  />

  <button type="submit" [disabled]="loginForm.invalid">Log In</button>
</form>

TS
import { Component } from '@angular/core';
import { NgForm } from '@angular/forms';
import { FormsModule } from '@angular/forms';

@Component({
  standalone: true,
  imports: [FormsModule],
  templateUrl: './login.component.html',
})
export class LoginComponent {
  onSubmit(form: NgForm) {
    if (form.valid) {
      console.log(form.value);  // { email: '...', password: '...' }
    }
  }
}
Note
The name attribute is mandatory on every input that uses ngModel. Angular uses it as the key in the form's value object. Omitting it causes a runtime error.
Two-Way Binding with ngModel

By default, ngModel creates a one-way binding that registers the field with the form. Add the banana-in-a-box syntax [(ngModel)] to also bind the value back to a component property.

TS
// component.ts
export class ProfileComponent {
  user = {
    name: 'Alice',
    email: 'alice@example.com',
  };
}

HTML
<!-- profile.component.html -->
<form #profileForm="ngForm" (ngSubmit)="onSubmit()">
  <!-- Two-way binding — model stays in sync as user types -->
  <input name="name" [(ngModel)]="user.name" required />
  <input name="email" [(ngModel)]="user.email" required email />

  <!-- Live preview powered by two-way binding -->
  <p>Preview: {{ user.name }} &lt;{{ user.email }}&gt;</p>

  <button [disabled]="profileForm.invalid">Save</button>
</form>
Template Reference Variables

Export the NgModel directive to a template variable to access a single control's state directly in the template.

HTML
<input
  name="username"
  ngModel
  #usernameField="ngModel"    <!-- export NgModel instance -->
  required
  minlength="3"
/>

<!-- Access state properties on the control -->
<div *ngIf="usernameField.invalid && usernameField.touched">
  <span *ngIf="usernameField.errors?.['required']">Username is required.</span>
  <span *ngIf="usernameField.errors?.['minlength']">
    Minimum 3 characters.
    (you typed {{ usernameField.errors?.['minlength'].actualLength }})
  </span>
</div>

<!-- Visual feedback via CSS classes -->
<p>
  Dirty: {{ usernameField.dirty }} |
  Touched: {{ usernameField.touched }} |
  Valid: {{ usernameField.valid }}
</p>
Angular's Automatic CSS Classes

Angular adds and removes CSS classes on form controls automatically, making it easy to style validation states without extra logic.

Class

Applied when

ng-valid

All validators pass

ng-invalid

At least one validator fails

ng-pristine

Value has not been changed

ng-dirty

Value has been changed

ng-untouched

Field has never been blurred

ng-touched

Field has been blurred at least once

ng-pending

An async validator is running

CSS
/* styles.css — style inputs based on validation state */
input.ng-invalid.ng-touched {
  border-color: red;
  outline-color: red;
}

input.ng-valid.ng-dirty {
  border-color: green;
}
Grouping Controls with ngModelGroup

Use ngModelGroup to nest controls into sub-objects inside the form value.

HTML
<form #checkoutForm="ngForm" (ngSubmit)="onSubmit(checkoutForm.value)">

  <fieldset ngModelGroup="shipping">
    <input name="street" ngModel required />
    <input name="city"   ngModel required />
    <input name="zip"    ngModel required />
  </fieldset>

  <fieldset ngModelGroup="billing">
    <input name="street" ngModel required />
    <input name="city"   ngModel required />
    <input name="zip"    ngModel required />
  </fieldset>

  <button>Place Order</button>
</form>

<!-- form.value will be:
{
  shipping: { street: '', city: '', zip: '' },
  billing:  { street: '', city: '', zip: '' }
}
-->
Setting and Resetting Values

Use the NgForm reference to programmatically set or reset form values.

TS
import { Component, ViewChild } from '@angular/core';
import { NgForm } from '@angular/forms';
import { FormsModule } from '@angular/forms';

@Component({
  standalone: true,
  imports: [FormsModule],
  template: `
    <form #f="ngForm" (ngSubmit)="onSubmit(f)">
      <input name="email" ngModel required email />
      <input name="name"  ngModel required />
      <button type="submit">Submit</button>
      <button type="button" (click)="reset(f)">Reset</button>
    </form>
  `,
})
export class EditUserComponent {
  @ViewChild('f') form!: NgForm;

  ngAfterViewInit() {
    // Populate form after view initialises
    setTimeout(() => {
      this.form.setValue({
        email: 'alice@example.com',
        name: 'Alice',
      });
    });
  }

  onSubmit(form: NgForm) {
    console.log(form.value);
  }

  reset(form: NgForm) {
    form.reset();         // clear values + reset pristine/touched state
    // or: form.resetForm({ email: '', name: '' }); to reset with defaults
  }
}
Select, Checkbox, and Radio Inputs

HTML
<!-- Select dropdown -->
<select name="role" [(ngModel)]="user.role">
  <option value="">-- Choose role --</option>
  <option value="admin">Admin</option>
  <option value="editor">Editor</option>
  <option value="viewer">Viewer</option>
</select>

<!-- Checkbox -->
<input
  type="checkbox"
  name="agreeToTerms"
  [(ngModel)]="agreeToTerms"
  required
/>

<!-- Radio buttons — same name, different values -->
<label>
  <input type="radio" name="gender" ngModel value="male" /> Male
</label>
<label>
  <input type="radio" name="gender" ngModel value="female" /> Female
</label>
<label>
  <input type="radio" name="gender" ngModel value="other" /> Other
</label>
Dynamic Options with ngFor

TS
export class OrderComponent {
  sizes = ['Small', 'Medium', 'Large', 'Extra Large'];
  order = { size: 'Medium' };
}

HTML
<select name="size" [(ngModel)]="order.size">
  <option *ngFor="let size of sizes" [value]="size">{{ size }}</option>
</select>
Warning
Template-driven forms do not support **strongly typed form values** by default. For large forms or TypeScript strict mode, consider reactive forms, which were updated to be fully typed in Angular 14.
Common Validation Directives

Directive / Attribute

Validates

required

Value is non-empty

email

Value matches email pattern

minlength="N"

String length >= N

maxlength="N"

String length <= N

min="N"

Number >= N

max="N"

Number <= N

pattern="regex"

Value matches the pattern

Tip
You can add custom validator directives to template-driven forms by creating a directive that implements Validator and providing itself under the NG_VALIDATORS token. This lets you use your custom logic just like a built-in attribute.

Summary: Template-driven forms are quick to build and require minimal TypeScript. Use ngModel and ngModelGroup to bind controls, #ref="ngForm" to access the form object, and Angular's automatic CSS classes for visual feedback. For complex dynamic forms, graduated to reactive forms which offer more programmatic control.