Suggest an editImprove this articleRefine the answer for “Difference between template-driven and reactive forms in Angular”. Your changes go to moderation before they’re published.Approval requiredContentWhat you’re changing🇺🇸EN🇺🇦UAPreviewTitle (EN)Short answer (EN)**Template-driven forms** use `ngModel` in HTML and let Angular build the form model from the template automatically. **Reactive forms** define the model explicitly in TypeScript using `FormGroup`. ```typescript form = new FormGroup({ email: new FormControl('', [Validators.required, Validators.email]) }); ``` **Key:** Reactive forms give programmatic access to every control via RxJS, support adding fields at runtime with `addControl()`, and are easier to unit test. Template-driven forms work well for simple, static forms with few fields and no dynamic logic.Shown above the full answer for quick recall.Answer (EN)Image**Template-driven and reactive forms** - two approaches Angular provides for building forms, each requiring different imports, a different mental model, and a different tradeoff between speed of setup and depth of control. ## Theory ### TL;DR - Template-driven: import `FormsModule`, add `ngModel` to inputs, Angular builds the form model from the template - Reactive: import `ReactiveFormsModule`, define `FormGroup` in TypeScript, bind with `formControlName` - Main difference: template-driven uses two-way binding through `ngModel`; reactive uses unidirectional flow via the `valueChanges` observable - Fewer than 5 static fields with no dynamic logic? Template-driven works. Dynamic fields, cross-field validators, server errors? Reactive. - Reactive forms test without a DOM: create `FormGroup` directly, call `form.setValue()`, check `form.valid` ### Quick example Template-driven form: ```html <form #userForm="ngForm" (ngSubmit)="onSubmit(userForm)"> <input name="email" ngModel required email #email="ngModel"> <div *ngIf="email.invalid && email.touched">Invalid email</div> <button [disabled]="userForm.invalid">Submit</button> </form> ``` ```typescript onSubmit(form: NgForm) { console.log(form.value); // { email: "test@example.com" } } ``` Angular reads the `ngModel` directive and auto-creates the form model from the template. The HTML is in charge here. Reactive form: ```typescript form = new FormGroup({ email: new FormControl('', [Validators.required, Validators.email]) }); onSubmit() { console.log(this.form.value); // { email: "test@example.com" } } ``` ```html <form [formGroup]="form" (ngSubmit)="onSubmit()"> <input formControlName="email"> <div *ngIf="form.get('email')?.invalid && form.get('email')?.touched"> Invalid email </div> <button [disabled]="form.invalid">Submit</button> </form> ``` TypeScript defines the model first. The template just binds to it. ### Key difference Template-driven forms use directives like `ngModel` and `NgForm` to automatically generate and sync a form model from your HTML. You never create `FormControl` objects yourself. Reactive forms start with an explicit `FormGroup` built in TypeScript. That group is a programmable tree of controls you manipulate directly, observe through RxJS, and test without any DOM. State flows one way: from model to view. ### When to use - Login or contact form with 3-4 static fields: template-driven, faster to write - Fields that appear or disappear based on user input: reactive (`addControl()` / `removeControl()`) - Cross-field validation (password must match confirm-password): reactive, group-level validator function - Server-side error mapped to a specific field: reactive (`control.setErrors()`) - Form logic that needs unit tests without a browser: reactive, pure TypeScript - Quick prototype or a simple Ionic screen: template-driven In my experience, most teams end up switching to reactive after the first time they need to dynamically add a field to a template-driven form and realize there is no API for it. ### Comparison table | Characteristic | Template-driven | Reactive | |---|---|---| | Setup | `FormsModule` + `ngModel` in HTML | `ReactiveFormsModule` + `FormGroup` in TS | | Data flow | Two-way binding (`ngModel`) | Unidirectional (`valueChanges` observable) | | Validation | HTML attributes + directive errors | `Validators` array on `FormControl` | | Dynamic fields | Workarounds with `*ngFor` | `addControl()` / `removeControl()` | | Async validators | Possible, more setup | Built-in via `asyncValidators` option | | Testing | Requires DOM, harder to isolate | Unit-test `FormGroup` directly | | When to use | Simple, static forms | Dynamic, complex, or tested forms | ### How Angular handles both internally For template-driven forms, Angular's compiler scans the template for `ngModel` directives and builds a `NgForm` model by matching `name` attributes to a directive tree. Miss the `name` attribute on an input and that field simply won't appear in `form.value`. Reactive forms construct the `FormGroup` tree in TypeScript at runtime. Value changes emit through `valueChanges` and `statusChanges` observables without any template involvement. That is why tests can call `form.setValue()` and read `form.valid` instantly, with no component rendering needed. ### Common mistakes **1. Mixing `ngModel` with reactive forms** ```html <!-- Causes NG0100 error or auto-override of the reactive model --> <form [formGroup]="form"> <input formControlName="email" [(ngModel)]="model.email"> </form> ``` Pick one approach. Remove `ngModel` when using `formControlName`. **2. Mutating `form.value` directly** ```typescript // Wrong: Angular does not detect this, UI won't update this.form.value.email = 'new@email.com'; // Correct this.form.patchValue({ email: 'new@email.com' }); ``` `form.value` is a snapshot. It is not a writable model. **3. Forgetting `name` on template-driven inputs** ```html <!-- This field won't appear in form.value --> <input ngModel required> <!-- Correct --> <input name="email" ngModel required> ``` **4. Not subscribing to `valueChanges`** ```typescript // Does nothing this.form.valueChanges; // Correct this.form.valueChanges .pipe(takeUntil(this.destroy$)) .subscribe(value => console.log(value)); ``` `valueChanges` is an observable. Reading it without subscribing has no effect. **5. Expecting async validators to run after `patchValue`** ```typescript // Won't trigger the async validator if updateOn: 'blur' is set this.emailControl.patchValue('new@example.com'); // Force revalidation manually this.emailControl.updateValueAndValidity(); ``` Angular recommends `updateOn: 'blur'` for async validators to avoid per-keystroke API calls. But `patchValue` won't trigger that validator automatically. ### Real-world usage - Angular Material `mat-stepper` wizards: reactive, because cross-step validation is needed - NgRx store integration: reactive `FormGroup` values stored in state for undo/redo - JSON-driven dynamic forms (e.g., `jsonforms.io`): reactive with `FormArray` for variable-length lists - Ionic login screens and simple contact forms: template-driven, less setup ### Follow-up questions **Q:** How do you add a new field to a form after it has already been rendered? **A:** Reactive only: `this.form.addControl('phone', new FormControl(''))`. Template-driven has no API for this. You can work around it with `*ngFor` over an array of field configs, but that approach gets messy quickly. **Q:** What is the difference between `setValue` and `patchValue`? **A:** `setValue` requires every field in the `FormGroup` or it throws an error. `patchValue` accepts a partial object and only updates the fields you include. **Q:** How do you test a reactive form without a browser? **A:** Create the `FormGroup` directly in the test, call `form.setValue()` or `form.patchValue()`, then check `form.valid` or specific control errors. No component rendering needed at all. **Q:** How do you stop an async validator from firing on every keystroke? **A:** Set `updateOn: 'blur'` in the `FormControl` options. The validator runs only when the field loses focus, not on every change event. **Q:** A `FormArray` has 10 rows. The user reorders them. Async validators fire on the wrong rows after the swap. How do you fix it? **A:** Track rows by a stable unique id, not by array index. Add `debounceTime` to the async validator to batch rapid changes. After a reorder, call `updateValueAndValidity()` explicitly on affected controls rather than relying on Angular to re-trigger it automatically. Referencing controls by index with `AbstractControl.at(i)` after the swap is also safer than holding stale references. ## Examples ### Login form (template-driven) ```html <form #loginForm="ngForm" (ngSubmit)="onSubmit(loginForm)"> <input name="email" ngModel required email #emailField="ngModel"> <div *ngIf="emailField.invalid && emailField.touched">Enter a valid email</div> <input name="password" type="password" ngModel required minlength="8"> <button [disabled]="loginForm.invalid">Log in</button> </form> ``` ```typescript onSubmit(form: NgForm) { if (form.valid) { console.log(form.value); // { email: "...", password: "..." } this.authService.login(form.value); } } ``` Angular builds the `NgForm` model from `name` attributes and `ngModel` directives. Validation rules come from HTML attributes. This covers the majority of simple form cases with minimal code. ### Registration with cross-field validation (reactive) ```typescript import { FormBuilder, FormGroup, Validators, AbstractControl } from '@angular/forms'; export class RegisterComponent { form: FormGroup; constructor(private fb: FormBuilder) { this.form = this.fb.group({ email: ['', [Validators.required, Validators.email]], confirmEmail: ['', Validators.required], password: ['', [Validators.required, Validators.minLength(8)]] }, { validators: this.emailMatchValidator }); } emailMatchValidator(group: AbstractControl) { const email = group.get('email')?.value; const confirm = group.get('confirmEmail')?.value; return email === confirm ? null : { mismatch: true }; } addPhoneField() { this.form.addControl('phone', this.fb.control('')); } } ``` ```html <form [formGroup]="form" (ngSubmit)="onSubmit()"> <input formControlName="email"> <input formControlName="confirmEmail" [class.error]="form.hasError('mismatch')"> <div *ngIf="form.hasError('mismatch')">Emails do not match</div> <button type="button" (click)="addPhoneField()">Add phone</button> <button [disabled]="form.invalid">Register</button> </form> ``` The group-level validator runs on the whole `FormGroup` and sets `form.errors = { mismatch: true }` when emails differ. `addPhoneField()` adds a new control at runtime. Neither of these things is possible with a template-driven form. ### Async validator with server check ```typescript import { AbstractControl } from '@angular/forms'; import { timer, of } from 'rxjs'; import { switchMap, map, catchError } from 'rxjs/operators'; asyncEmailValidator(control: AbstractControl) { return timer(500).pipe( switchMap(() => this.http.get<boolean>(`/api/check-email/${control.value}`) ), map(isTaken => isTaken ? { emailTaken: true } : null), catchError(() => of(null)) // network error: do not block the form ); } // Usage email = new FormControl('', { validators: [Validators.required, Validators.email], asyncValidators: [this.asyncEmailValidator.bind(this)], updateOn: 'blur' // only checks when the user leaves the field }); ``` `timer(500)` debounces the call so rapid typing doesn't fire multiple requests. `updateOn: 'blur'` limits the trigger to when the field loses focus. If the API is unavailable, `catchError` returns `null` so the form doesn't get stuck in an invalid state. One edge case: calling `patchValue()` on this control won't rerun the async validator. Call `updateValueAndValidity()` explicitly when you need to force it.For the reviewerNote to the moderator (optional)Visible only to the moderator. Helps review go faster.