Skip to main content
Template-driven forms use directives in templates to create and manipulate the underlying form object model. They rely on two-way data binding with ngModel to track changes and update the data model.

When to Use Template-Driven Forms

Template-driven forms are ideal for:
  • Simple forms with minimal validation
  • Forms that closely mirror the view structure
  • Quick prototypes and small applications
  • Scenarios where you prefer working in templates over component classes
For more complex scenarios, consider reactive forms.

Setting Up Template-Driven Forms

Import FormsModule in your component or module:
import { Component } from '@angular/core';
import { FormsModule } from '@angular/forms';

@Component({
  selector: 'app-contact',
  standalone: true,
  imports: [FormsModule],
  template: `...`
})
export class ContactComponent {}

Basic ngModel Usage

The ngModel directive creates a FormControl instance and binds it to a form control element.

One-Way Binding

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

@Component({
  selector: 'app-name',
  standalone: true,
  imports: [FormsModule],
  template: `
    <label for="name">Name:</label>
    <input id="name" type="text" [ngModel]="name">
    <p>Hello, {{ name }}!</p>
  `
})
export class NameComponent {
  name = 'Nancy';
}

Two-Way Binding

Use banana-in-a-box syntax [(ngModel)] for two-way data binding:
packages/forms/src/directives/ng_model.ts
import { Component } from '@angular/core';
import { FormsModule } from '@angular/forms';

@Component({
  selector: 'app-name-editor',
  standalone: true,
  imports: [FormsModule],
  template: `
    <label for="name">Name:</label>
    <input id="name" type="text" [(ngModel)]="name">
    <p>Hello, {{ name }}!</p>
  `
})
export class NameEditorComponent {
  name = 'Nancy';
}

Working with Forms

When using ngModel within a <form> tag, provide a name attribute so the control can be registered with the parent form.

Basic Form Example

packages/forms/src/directives/ng_model.ts
import { Component } from '@angular/core';
import { FormsModule } from '@angular/forms';

@Component({
  selector: 'app-contact-form',
  standalone: true,
  imports: [FormsModule],
  template: `
    <form #contactForm="ngForm" (ngSubmit)="onSubmit(contactForm)">
      <div>
        <label for="firstName">First Name:</label>
        <input 
          id="firstName" 
          name="firstName" 
          type="text" 
          [(ngModel)]="contact.firstName"
          required>
      </div>
      
      <div>
        <label for="lastName">Last Name:</label>
        <input 
          id="lastName" 
          name="lastName" 
          type="text" 
          [(ngModel)]="contact.lastName"
          required>
      </div>
      
      <div>
        <label for="email">Email:</label>
        <input 
          id="email" 
          name="email" 
          type="email" 
          [(ngModel)]="contact.email"
          required
          email>
      </div>
      
      <button type="submit" [disabled]="contactForm.invalid">Submit</button>
    </form>
    
    <p>Form Valid: {{ contactForm.valid }}</p>
    <p>Form Value: {{ contactForm.value | json }}</p>
  `
})
export class ContactFormComponent {
  contact = {
    firstName: '',
    lastName: '',
    email: ''
  };
  
  onSubmit(form: any) {
    console.log('Form submitted:', form.value);
    console.log('Model:', this.contact);
  }
}

Accessing Form State

You can export the form directive into a local template variable using #varName="ngForm":
<form #contactForm="ngForm">
  <!-- Form controls -->
</form>

<p>Form Status: {{ contactForm.status }}</p>
<p>Form Valid: {{ contactForm.valid }}</p>
<p>Form Pristine: {{ contactForm.pristine }}</p>
<p>Form Touched: {{ contactForm.touched }}</p>

Accessing Individual Controls

Export individual controls to access their state:
packages/forms/src/directives/ng_model.ts
import { Component } from '@angular/core';
import { FormsModule } from '@angular/forms';

@Component({
  selector: 'app-email-input',
  standalone: true,
  imports: [FormsModule],
  template: `
    <label for="email">Email:</label>
    <input 
      id="email" 
      name="email" 
      type="email" 
      [(ngModel)]="email"
      #emailField="ngModel"
      required
      email>
    
    <div *ngIf="emailField.invalid && emailField.touched">
      <p *ngIf="emailField.errors?.['required']">Email is required</p>
      <p *ngIf="emailField.errors?.['email']">Invalid email format</p>
    </div>
    
    <p>Valid: {{ emailField.valid }}</p>
    <p>Dirty: {{ emailField.dirty }}</p>
    <p>Touched: {{ emailField.touched }}</p>
  `
})
export class EmailInputComponent {
  email = '';
}

Form Validation

Template-driven forms use directives for validation:
<form #userForm="ngForm">
  <div>
    <label for="username">Username:</label>
    <input 
      id="username" 
      name="username" 
      type="text" 
      [(ngModel)]="username"
      #usernameField="ngModel"
      required
      minlength="3"
      maxlength="20"
      pattern="[a-zA-Z0-9]+">
    
    <div *ngIf="usernameField.invalid && usernameField.touched">
      <p *ngIf="usernameField.errors?.['required']">Username is required</p>
      <p *ngIf="usernameField.errors?.['minlength']">
        Username must be at least 3 characters
      </p>
      <p *ngIf="usernameField.errors?.['maxlength']">
        Username cannot exceed 20 characters
      </p>
      <p *ngIf="usernameField.errors?.['pattern']">
        Username can only contain letters and numbers
      </p>
    </div>
  </div>
  
  <div>
    <label for="age">Age:</label>
    <input 
      id="age" 
      name="age" 
      type="number" 
      [(ngModel)]="age"
      #ageField="ngModel"
      required
      min="18"
      max="100">
    
    <div *ngIf="ageField.invalid && ageField.touched">
      <p *ngIf="ageField.errors?.['required']">Age is required</p>
      <p *ngIf="ageField.errors?.['min']">Must be at least 18</p>
      <p *ngIf="ageField.errors?.['max']">Must be 100 or less</p>
    </div>
  </div>
</form>

Built-in Validation Directives

required

Requires a non-empty value

email

Validates email format

minlength

Minimum length for strings/arrays

maxlength

Maximum length for strings/arrays

min

Minimum value for numbers

max

Maximum value for numbers

pattern

Validates against a regex pattern

Grouped Controls

Use ngModelGroup to create sub-groups within a form:
packages/forms/src/directives/ng_model_group.ts
import { Component } from '@angular/core';
import { FormsModule } from '@angular/forms';
import { CommonModule } from '@angular/common';

@Component({
  selector: 'app-user-form',
  standalone: true,
  imports: [FormsModule, CommonModule],
  template: `
    <form #userForm="ngForm" (ngSubmit)="onSubmit()">
      <div ngModelGroup="name" #nameGroup="ngModelGroup">
        <label for="first">First Name:</label>
        <input 
          id="first" 
          name="first" 
          type="text" 
          [(ngModel)]="user.name.first"
          required>
        
        <label for="last">Last Name:</label>
        <input 
          id="last" 
          name="last" 
          type="text" 
          [(ngModel)]="user.name.last"
          required>
        
        <p *ngIf="nameGroup.invalid && nameGroup.touched">
          Name is incomplete
        </p>
      </div>
      
      <div ngModelGroup="address" #addressGroup="ngModelGroup">
        <label for="street">Street:</label>
        <input 
          id="street" 
          name="street" 
          type="text" 
          [(ngModel)]="user.address.street">
        
        <label for="city">City:</label>
        <input 
          id="city" 
          name="city" 
          type="text" 
          [(ngModel)]="user.address.city">
        
        <label for="state">State:</label>
        <input 
          id="state" 
          name="state" 
          type="text" 
          [(ngModel)]="user.address.state">
      </div>
      
      <button type="submit" [disabled]="userForm.invalid">Submit</button>
    </form>
    
    <pre>{{ userForm.value | json }}</pre>
  `
})
export class UserFormComponent {
  user = {
    name: {
      first: '',
      last: ''
    },
    address: {
      street: '',
      city: '',
      state: ''
    }
  };
  
  onSubmit() {
    console.log('User:', this.user);
  }
}

ngModelOptions

Customize how ngModel works with ngModelOptions:

Standalone Controls

Create a control that doesn’t register with the parent form:
<form>
  <input name="login" [(ngModel)]="login">
  
  <!-- This checkbox is standalone and won't be in form value -->
  <input 
    type="checkbox" 
    [(ngModel)]="showAdvanced"
    [ngModelOptions]="{standalone: true}">
  Show advanced options?
</form>
<!-- form value: {login: ''} -->

Update On Blur or Submit

Control when the model updates:
<input 
  name="description" 
  [(ngModel)]="description"
  [ngModelOptions]="{updateOn: 'blur'}">

<form [ngFormOptions]="{updateOn: 'submit'}">
  <!-- All controls update on submit -->
  <input name="username" [(ngModel)]="username">
  <input name="email" [(ngModel)]="email">
  <button type="submit">Submit</button>
</form>

Custom Name

Provide a name through options instead of the name attribute:
<form>
  <my-custom-form-control 
    name="Nancy" 
    [(ngModel)]="value"
    [ngModelOptions]="{name: 'user'}">
  </my-custom-form-control>
</form>
<!-- form value: {user: ''} -->

Handling Different Input Types

Text Inputs

<input type="text" name="username" [(ngModel)]="username">
<input type="email" name="email" [(ngModel)]="email">
<input type="password" name="password" [(ngModel)]="password">
<textarea name="description" [(ngModel)]="description"></textarea>

Checkboxes

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

@Component({
  selector: 'app-preferences',
  standalone: true,
  imports: [FormsModule],
  template: `
    <label>
      <input type="checkbox" name="newsletter" [(ngModel)]="newsletter">
      Subscribe to newsletter
    </label>
    <p>Newsletter: {{ newsletter }}</p>
  `
})
export class PreferencesComponent {
  newsletter = false;
}

Radio Buttons

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

@Component({
  selector: 'app-survey',
  standalone: true,
  imports: [FormsModule],
  template: `
    <div>
      <label>
        <input type="radio" name="size" [(ngModel)]="size" value="small">
        Small
      </label>
      <label>
        <input type="radio" name="size" [(ngModel)]="size" value="medium">
        Medium
      </label>
      <label>
        <input type="radio" name="size" [(ngModel)]="size" value="large">
        Large
      </label>
    </div>
    <p>Selected size: {{ size }}</p>
  `
})
export class SurveyComponent {
  size = 'medium';
}

Select Dropdowns

import { Component } from '@angular/core';
import { FormsModule } from '@angular/forms';
import { CommonModule } from '@angular/common';

@Component({
  selector: 'app-country-selector',
  standalone: true,
  imports: [FormsModule, CommonModule],
  template: `
    <label for="country">Country:</label>
    <select id="country" name="country" [(ngModel)]="selectedCountry">
      <option [ngValue]="null" disabled>Select a country</option>
      <option *ngFor="let country of countries" [ngValue]="country">
        {{ country.name }}
      </option>
    </select>
    <p>Selected: {{ selectedCountry?.name }}</p>
  `
})
export class CountrySelectorComponent {
  countries = [
    { code: 'US', name: 'United States' },
    { code: 'CA', name: 'Canada' },
    { code: 'MX', name: 'Mexico' }
  ];
  
  selectedCountry: any = null;
}

Multiple Select

import { Component } from '@angular/core';
import { FormsModule } from '@angular/forms';
import { CommonModule } from '@angular/common';

@Component({
  selector: 'app-skills-selector',
  standalone: true,
  imports: [FormsModule, CommonModule],
  template: `
    <label for="skills">Skills:</label>
    <select id="skills" name="skills" [(ngModel)]="selectedSkills" multiple>
      <option *ngFor="let skill of skills" [ngValue]="skill">
        {{ skill }}
      </option>
    </select>
    <p>Selected: {{ selectedSkills | json }}</p>
  `
})
export class SkillsSelectorComponent {
  skills = ['JavaScript', 'TypeScript', 'Angular', 'React', 'Vue'];
  selectedSkills: string[] = [];
}

Template-Driven Forms vs Reactive Forms

AspectTemplate-DrivenReactive
SetupFormsModuleReactiveFormsModule
Form model creationImplicit (by directives)Explicit (in component)
Data modelMutableImmutable
Form validationDirectivesFunctions
PredictabilityAsynchronousSynchronous
TestabilityLess testableMore testable
ScalabilityLimitedExcellent

When to Choose Template-Driven Forms

Template-driven forms are perfect for simple forms with basic validation.
Get forms up and running quickly without much setup.
If your team prefers working in templates rather than component classes.
Template-driven forms are similar to AngularJS forms, easing migration.

Best Practices

Always provide a name attribute when using ngModel within a <form> tag.
Use template reference variables to access form and control state in templates.
For complex forms with dynamic controls or extensive validation logic, consider reactive forms.

Next Steps

Form Validation

Learn about validation in template-driven forms

Reactive Forms

Explore reactive forms for more complex scenarios