# Template-Driven Forms

Course: [Angular](https://www.devtutorials.ai/learn/angular) · Section: Angular Forms
Source: https://www.devtutorials.ai/learn/angular/forms/template-driven-forms

> Building forms in the template with FormsModule and ngModel: every common input type, handling input, tracking form state, submitting and resetting, and best practices.

## Introduction to Template-Driven Forms

A **template-driven form** is a form whose structure and data binding are written in the template. You add `ngModel` to each field, and Angular creates and tracks the form for you behind the scenes.

The class holds only the data, such as a `user` object. The template connects each input to a property of that data.

### Real-World Example

A newsletter sign-up page has a name field, an email field, a topic dropdown and a "Send me offers" checkbox. Each field is connected to a property of a `subscriber` object in the class.

When the user clicks **Subscribe**, the class already holds every value the user entered, ready to send to the API. After a successful request, the form is cleared for the next sign-up.

<figure className="ngc-fig">
<svg viewBox="0 0 640 195" role="img" aria-label="Each form field is bound with ngModel to a property of the subscriber object, and submitting sends the object to the API.">
<defs><marker id="ngc-f1" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse"><path d="M0 0L10 5L0 10z" className="ngc-arrowhead" /></marker></defs><g className="ngc-node ngc-soft c-blue"><rect x="10" y="10" width="120" height="36" rx="9" /><text x="70.0" y="33" textAnchor="middle" style={{fontSize:"13px"}}>Name</text></g><line className="ngc-link ngc-flow c-blue" x1="132" y1="28" x2="226" y2="95" markerEnd="url(#ngc-f1)" /><g className="ngc-node ngc-soft c-blue"><rect x="10" y="56" width="120" height="36" rx="9" /><text x="70.0" y="79" textAnchor="middle" style={{fontSize:"13px"}}>Email</text></g><line className="ngc-link ngc-flow c-blue" x1="132" y1="74" x2="226" y2="95" markerEnd="url(#ngc-f1)" /><g className="ngc-node ngc-soft c-purple"><rect x="10" y="102" width="120" height="36" rx="9" /><text x="70.0" y="125" textAnchor="middle" style={{fontSize:"13px"}}>Topic</text></g><line className="ngc-link ngc-flow c-purple" x1="132" y1="120" x2="226" y2="95" markerEnd="url(#ngc-f1)" /><g className="ngc-node ngc-soft c-green"><rect x="10" y="148" width="120" height="36" rx="9" /><text x="70.0" y="171" textAnchor="middle" style={{fontSize:"13px"}}>Offers ☑</text></g><line className="ngc-link ngc-flow c-green" x1="132" y1="166" x2="226" y2="95" markerEnd="url(#ngc-f1)" /><g className="ngc-pulse"><g className="ngc-node ngc-solid c-purple"><rect x="228" y="65" width="170" height="60" rx="9" /><text x="313.0" y="92" textAnchor="middle" style={{fontSize:"15px"}}>subscriber</text><text className="ngc-sub" x="313.0" y="110" textAnchor="middle">object in the class</text></g></g><line className="ngc-link ngc-flow c-orange" x1="400" y1="95" x2="456" y2="95" markerEnd="url(#ngc-f1)" /><g className="ngc-node ngc-solid c-orange"><rect x="458" y="65" width="172" height="60" rx="9" /><text x="544.0" y="92" textAnchor="middle" style={{fontSize:"15px"}}>Subscribe</text><text className="ngc-sub" x="544.0" y="110" textAnchor="middle">(ngSubmit) → API</text></g><text className="ngc-note" x="313" y="150" textAnchor="middle">[(ngModel)] keeps each field and its property in sync</text>
</svg>
<figcaption>Every field writes into one object. Submit sends that object.</figcaption>
</figure>

## FormsModule

`FormsModule` is the Angular package that provides template-driven forms. It contains the `ngModel` directive and the `ngForm` directive that Angular attaches to every `<form>` element.

Import it from `@angular/forms` and add it to the component's `imports` list:

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

@Component({
  selector: 'app-signup',
  imports: [FormsModule],
  templateUrl: './signup.html',
})
export class Signup {}
```

Without `FormsModule`, Angular does not recognise `ngModel` and reports `NG0303: Can't bind to 'ngModel' since it isn't a known property of 'input'`.

## `ngModel`

`ngModel` is a directive that connects a form field to a property in the class. It also registers the field with the surrounding form, so Angular can track its value and state.

Inside a `<form>`, every field with `ngModel` needs a `name` attribute. Angular uses the name as the key of the field in the form's value.

```html
<form>
  <input name="email" [(ngModel)]="email" />
</form>
```

If a field inside a `<form>` has `ngModel` but no `name`, Angular reports: `If ngModel is used within a form tag, either the name attribute must be set or the form control must be defined as 'standalone' in ngModelOptions`.

## Two-Way Data Binding with `ngModel`

`[(ngModel)]="property"` creates **two-way binding** between a field and a class property:

- When the user types, Angular writes the new value into the property.
- When code changes the property, Angular writes the new value into the field.

```angular runnable title="ngModel keeps the field and the property in sync"
import { Component } from '@angular/core';
import { FormsModule } from '@angular/forms';

@Component({
  selector: 'app-root',
  imports: [FormsModule],
  template: `
    <label>Project name: <input name="project" [(ngModel)]="projectName" /></label>
    <p>Repository URL: github.com/acme/{{ projectName.toLowerCase().split(' ').join('-') }}</p>
    <button (click)="projectName = 'Billing Service'">Use a suggested name</button>
  `,
})
export class App {
  projectName = 'Customer Portal';
}
```

Type in the field and the URL updates. Click the button and the field updates.

## Form Controls

A **form control** is the object Angular creates for each field that has `ngModel`. It stores the field's current value and its state, such as whether the user has changed it.

The `<form>` element gets an `NgForm` object that groups all its controls. You can reach it with a template reference variable: `#signupForm="ngForm"`.

| Object | Created for | Holds |
|---|---|---|
| `NgModel` (a form control) | Each field with `ngModel` and `name` | One value and its state |
| `NgForm` | Each `<form>` element | All controls, the combined value, the overall state |

```angular runnable title="NgForm collects every control into one value"
import { Component } from '@angular/core';
import { FormsModule, NgForm } from '@angular/forms';
import { JsonPipe } from '@angular/common';

@Component({
  selector: 'app-root',
  imports: [FormsModule, JsonPipe],
  template: `
    <form #profileForm="ngForm">
      <input name="firstName" [(ngModel)]="firstName" />
      <input name="city" [(ngModel)]="city" />
    </form>
    <p>Controls: {{ controlNames(profileForm) }}</p>
    <pre>{{ profileForm.value | json }}</pre>
  `,
})
export class App {
  firstName = 'Asha';
  city = 'Pune';

  controlNames(form: NgForm) {
    return Object.keys(form.controls).join(', ');
  }
}
```

## Form Input Elements

`ngModel` works with every standard form element. The type of the value depends on the element.

| Element | Value type in the class |
|---|---|
| Text, email, password input, textarea | `string` |
| Number input | `number` |
| Date input | `string` in `yyyy-mm-dd` format |
| Checkbox | `boolean` |
| Radio buttons | The `value` of the selected button |
| Select dropdown | The `value` of the selected option, or the object bound with `[ngValue]` |

### Text Input, Email Input and Password Input

- `type="text"` accepts any text.
- `type="email"` shows an email keyboard on phones and enables email validation.
- `type="password"` hides the characters as the user types.

### Textarea

`<textarea>` accepts text with several lines, such as comments or an address.

### Number Input and Date Input

- `type="number"` accepts only numbers. Angular converts the value to a `number`.
- `type="date"` shows the browser's date picker. The value is a text in `yyyy-mm-dd` format.

### Select Dropdown

`<select>` lets the user pick one option. Use `[value]` for text values, or `[ngValue]` to store a number or a whole object.

### Checkbox

A checkbox stores `true` when it is ticked and `false` when it is not.

### Radio Button

Radio buttons with the **same `name`** form one group. Only one can be selected, and the property stores that button's `value`.

```angular runnable title="Every common input type bound with ngModel"
import { Component } from '@angular/core';
import { FormsModule } from '@angular/forms';
import { JsonPipe } from '@angular/common';

@Component({
  selector: 'app-root',
  imports: [FormsModule, JsonPipe],
  template: `
    <form class="grid">
      <label>Name <input type="text" name="name" [(ngModel)]="model.name" /></label>
      <label>Email <input type="email" name="email" [(ngModel)]="model.email" /></label>
      <label>Password <input type="password" name="password" [(ngModel)]="model.password" /></label>
      <label>Seats <input type="number" name="seats" [(ngModel)]="model.seats" min="1" /></label>
      <label>Start date <input type="date" name="start" [(ngModel)]="model.start" /></label>
      <label>Plan
        <select name="plan" [(ngModel)]="model.plan">
          @for (p of plans; track p) { <option [value]="p">{{ p }}</option> }
        </select>
      </label>
      <label>Billing
        <span><input type="radio" name="billing" value="monthly" [(ngModel)]="model.billing" /> Monthly
        <input type="radio" name="billing" value="yearly" [(ngModel)]="model.billing" /> Yearly</span>
      </label>
      <label><input type="checkbox" name="terms" [(ngModel)]="model.terms" /> I accept the terms</label>
      <label>Notes <textarea name="notes" rows="2" [(ngModel)]="model.notes"></textarea></label>
    </form>
    <pre>{{ model | json }}</pre>
  `,
  styles: `
    .grid { display: grid; gap: 6px; max-width: 360px; }
    label { display: flex; justify-content: space-between; gap: 8px; }
  `,
})
export class App {
  plans = ['Starter', 'Team', 'Enterprise'];
  model = {
    name: 'Asha',
    email: 'asha@example.com',
    password: 'secret123',
    seats: 5,
    start: '2026-04-01',
    plan: 'Team',
    billing: 'yearly',
    terms: true,
    notes: '',
  };
}
```

Change any field and watch the JSON below update. `seats` stays a number, and `terms` stays a boolean.

### File Input (Overview)

`<input type="file">` lets the user choose files from their computer. `ngModel` does **not** work with file inputs, because the browser does not allow code to set a file input's value.

Instead, listen to the `change` event and read the selected files from the element:

```ts
onFileSelected(event: Event) {
  const input = event.target as HTMLInputElement;
  const file = input.files?.[0];
  if (file) {
    this.fileName = file.name;   // upload it with HttpClient later
  }
}
```

```html
<input type="file" (change)="onFileSelected($event)" />
```

Uploading the file to a server is covered in the HTTP chapter.

## Handling User Input

`ngModel` updates the property on every key press. To run your own code when the value changes, split the binding into `[ngModel]` and `(ngModelChange)`.

`(ngModelChange)` fires with the new value each time the user changes the field.

```angular runnable title="ngModelChange runs code on every change"
import { Component } from '@angular/core';
import { FormsModule } from '@angular/forms';

@Component({
  selector: 'app-root',
  imports: [FormsModule],
  template: `
    <label>Coupon: <input name="coupon" [ngModel]="coupon" (ngModelChange)="onCouponChange($event)" /></label>
    <p>Stored as: {{ coupon }}</p>
    <p>{{ message }}</p>
  `,
})
export class App {
  coupon = '';
  message = 'Try SAVE10';

  onCouponChange(value: string) {
    this.coupon = value.toUpperCase().trim();
    this.message = this.coupon === 'SAVE10' ? '✅ 10% discount applied' : 'Code not recognised yet';
  }
}
```

The handler stores the code in upper case, so `save10` and `SAVE10` are treated the same.

## Handling Multiple Inputs

When a form has many fields, keep their values in **one object** instead of separate properties. Bind each field to a property of that object: `[(ngModel)]="address.city"`.

One object is easier to validate, reset and send to an API in a single request.

```angular runnable title="Several fields bound to one address object"
import { Component } from '@angular/core';
import { FormsModule } from '@angular/forms';

interface Address {
  street: string;
  city: string;
  pin: string;
}

@Component({
  selector: 'app-root',
  imports: [FormsModule],
  template: `
    <form>
      <p><input name="street" placeholder="Street" [(ngModel)]="address.street" /></p>
      <p><input name="city" placeholder="City" [(ngModel)]="address.city" />
         <input name="pin" placeholder="PIN code" [(ngModel)]="address.pin" /></p>
    </form>
    <p>Ship to: {{ address.street }}, {{ address.city }} - {{ address.pin }}</p>
  `,
})
export class App {
  address: Address = { street: '12 MG Road', city: 'Bengaluru', pin: '560001' };
}
```

## Form State Tracking

Angular tracks the state of every control and of the whole form. Each state is a `true`/`false` property:

| Property | `true` when |
|---|---|
| `pristine` | The user has not changed the value yet |
| `dirty` | The user has changed the value |
| `untouched` | The user has not left the field yet |
| `touched` | The user focused the field and then left it |
| `valid` | The value passes all validation rules |
| `invalid` | The value fails at least one rule |

Angular also adds matching CSS classes to each field, such as `ng-dirty`, `ng-touched` and `ng-invalid`. Style invalid fields with a selector such as `input.ng-invalid.ng-touched`, so the red border appears only after the user has visited the field.

```angular runnable title="Form and field state, updated as you interact"
import { Component } from '@angular/core';
import { FormsModule } from '@angular/forms';

@Component({
  selector: 'app-root',
  imports: [FormsModule],
  template: `
    <form #f="ngForm">
      <input name="company" [(ngModel)]="company" #companyField="ngModel" required />
    </form>
    <p>Field: {{ companyField.dirty ? 'dirty' : 'pristine' }}, {{ companyField.touched ? 'touched' : 'untouched' }}, {{ companyField.valid ? 'valid' : 'invalid' }}</p>
    <p>Form: {{ f.valid ? 'valid' : 'invalid' }}</p>
  `,
  styles: `input.ng-invalid.ng-touched { border: 2px solid #dc2626; }`,
})
export class App {
  company = 'Acme Ltd';
}
```

Click in the field, delete the text and click outside. The field becomes `dirty`, `touched` and `invalid`, and it gets a red border.

## Form Submission

Listen to `(ngSubmit)` on the `<form>` element to handle submission. `ngSubmit` runs when the user clicks a submit button or presses Enter in a field, and it prevents the browser from reloading the page.

Put `(ngSubmit)` on the `<form>` element and give the button `type="submit"`. Then both a click and the Enter key submit the form, and the page never reloads. Disable the button while the form is invalid with `[disabled]="form.invalid"`.

```angular runnable title="ngSubmit sends the form data without reloading the page"
import { Component } from '@angular/core';
import { FormsModule } from '@angular/forms';

@Component({
  selector: 'app-root',
  imports: [FormsModule],
  template: `
    <form #contactForm="ngForm" (ngSubmit)="send()">
      <input name="email" type="email" [(ngModel)]="email" required />
      <button type="submit" [disabled]="contactForm.invalid">Request a demo</button>
    </form>
    <p>{{ status }}</p>
  `,
})
export class App {
  email = 'ravi@example.com';
  status = 'Not submitted';

  send() {
    this.status = 'Demo requested for ' + this.email;
  }
}
```

## Resetting Forms

`form.reset()` clears every field and returns every control to the `pristine` and `untouched` state. Bound properties become `null`.

To reset to specific values instead of empty fields, pass an object: `form.reset({ email: 'default@example.com' })`. To reset in code, read the form with `@ViewChild('f') form!: NgForm;`.

```angular runnable title="reset() clears the fields and the form state"
import { Component } from '@angular/core';
import { FormsModule } from '@angular/forms';

@Component({
  selector: 'app-root',
  imports: [FormsModule],
  template: `
    <form #feedbackForm="ngForm">
      <input name="subject" [(ngModel)]="subject" />
      <textarea name="message" [(ngModel)]="message" rows="2"></textarea>
    </form>
    <p>Subject: {{ subject || '(empty)' }} · Form is {{ feedbackForm.dirty ? 'dirty' : 'pristine' }}</p>
    <button (click)="feedbackForm.reset()">Clear</button>
    <button (click)="feedbackForm.reset({ subject: 'Bug report', message: '' })">Use template</button>
  `,
})
export class App {
  subject = 'Login page is slow';
  message = 'It takes 8 seconds to load.';
}
```

## Template-Driven Form Best Practices

- **Give every field a `name`.** It is required inside a `<form>`, and it becomes the key in `form.value`.
- **Keep the data in one model object**, such as `user`, so it can be validated, reset and sent in one step.
- **Use `(ngSubmit)`, not `(click)`, to submit.** It also handles the Enter key and prevents a page reload.
- **Disable submit while the form is invalid** with `[disabled]="form.invalid"`.
- **Match the input type to the data**, such as `type="email"` or `type="number"`, for better phone keyboards and correct value types.
- **Choose reactive forms for complex cases.** Template-driven forms are best for small, simple forms. Dynamic fields and complex validation are easier with reactive forms, covered in the next lesson.

## Common Mistakes

<div className="ngc-mistakes">

| Mistake | What you see | Fix |
|---|---|---|
| `FormsModule` not imported | `NG0303: Can't bind to 'ngModel'` | Add `FormsModule` to the component's `imports` |
| `ngModel` without `name` inside a form | `the name attribute must be set` error | Add a unique `name` to each field |
| Two fields with the same `name` | Both fields show the same value | Give each field its own `name` |
| `[ngModel]` without `(ngModelChange)` | Typing does not update the class | Use `[(ngModel)]` for two-way binding |
| `ngModel` on a file input | The selected file is never stored | Read `event.target.files` in a `(change)` handler |
| Submit button with `(click)` instead of `(ngSubmit)` | Pressing Enter does nothing, or the page reloads | Put `(ngSubmit)` on the `<form>` element |

</div>

## Check Yourself

## Exercises

## What to Remember

- Template-driven forms need `FormsModule`. Each field uses `[(ngModel)]` and a `name`.
- Angular tracks each control's value and state (`dirty`, `touched`, `valid`), and the whole form through `#f="ngForm"`.
- Submit with `(ngSubmit)`, disable the button while the form is invalid, and clear it with `form.reset()`.

Next: **Reactive Forms**, how to define a form's structure and rules in the component class.
