Angular

Template-Driven Forms

15 Questions

Template-driven forms use directives like ngModel in the template and are easier for simple forms. Reactive forms define the form model programmatically in the component class using FormControl and FormGroup. Template-driven forms use FormsModule while reactive forms use ReactiveFormsModule. Reactive forms offer more control over validation, dynamic fields, and unit testing.
<!-- Template-driven (uses FormsModule) -->
<form #myForm="ngForm" (ngSubmit)="onSubmit(myForm.value)">
  <input name="email" ngModel required />
</form>

<!-- Reactive (uses ReactiveFormsModule) -->
// Component class
form = new FormGroup({
  email: new FormControl('', Validators.required)
});
// Template: <form [formGroup]="form"><input formControlName="email" /></form>
Template-driven forms are best for simple scenarios. Reactive forms are preferred for complex forms with dynamic fields, custom validation, and unit testing.

Why it matters: Tests understanding of when to use each approach. Shows you know form architecture tradeoffs.

Real applications: Contact forms, signup forms, simple surveys use template-driven. E-commerce checkout, dynamic questionnaires, complex workflows use reactive.

Common mistakes: Developers use template-driven for complex forms losing control. They use reactive for simple forms adding unnecessary boilerplate. They don't understand reactive is more testable.

ngModel creates a two-way data binding between a form control element and a component property. Each ngModel creates a FormControl instance behind the scenes to track the value and validation state. You can use it in three ways: [(ngModel)] for two-way binding, [ngModel] for one-way, or just ngModel to register the control without binding.
<form #userForm="ngForm">
  <!-- Two-way binding -->
  <input name="name" [(ngModel)]="user.name" />

  <!-- One-way binding (read only from component) -->
  <input name="email" [ngModel]="user.email" />

  <!-- No binding (just registers control) -->
  <input name="phone" ngModel />
</form>

// Component
export class FormComponent {
  user = { name: '', email: '' };
}
Each input using ngModel must have a name attribute so the form can register the control. Import FormsModule to use ngModel.

Why it matters: Tests understanding of two-way binding mechanics. Shows you know how ngModel works under the hood.

Real applications: Login forms, search inputs, real-time filters, inline editing all use ngModel for instant value synchronization.

Common mistakes: Developers forget name attributes breaking form registration. They use [(ngModel)] without FormsModule import. They don't understand FormControl is created automatically.

ngForm is automatically applied to every <form> element when FormsModule is imported. It creates a top-level FormGroup that tracks the form's overall validity, value, and submission state. You export it using a template reference variable like #myForm="ngForm" to access form properties in the template. It provides methods like reset() and properties like valid, dirty, and value.
<form #registrationForm="ngForm" (ngSubmit)="onSubmit(registrationForm)">
  <input name="name" ngModel required />
  <input name="email" ngModel required email />

  <button [disabled]="registrationForm.invalid">Submit</button>

  <p>Form valid: {{ registrationForm.valid }}</p>
  <p>Form value: {{ registrationForm.value | json }}</p>
</form>

// Component
onSubmit(form: NgForm) {
  if (form.valid) {
    console.log('Form data:', form.value);
    form.reset(); // Reset form after submission
  }
}
Access form state via the template reference variable: .valid, .invalid, .dirty, .pristine, .touched, .value.

Why it matters: Tests understanding of form state tracking. Shows you know how to access form metadata in templates.

Real applications: Preventing submission of invalid forms, showing/hiding fields based on form state, disabling submit buttons until valid.

Common mistakes: Developers don't export ngForm losing state access. They check form.valid in component instead of template. They don't understand pristine vs dirty distinction.

Angular provides built-in validator directives that map to HTML5 validation attributes like required, minlength, and email. They are applied directly in the template as element attributes. You export the control with #name="ngModel" to access its error state and display validation messages. The errors object contains keys matching the failed validators.
<form #f="ngForm">
  <input name="name" ngModel required minlength="3" maxlength="50"
         #nameCtrl="ngModel" />

  <div *ngIf="nameCtrl.invalid && nameCtrl.touched">
    <p *ngIf="nameCtrl.errors?.['required']">Name is required</p>
    <p *ngIf="nameCtrl.errors?.['minlength']">
      Minimum {{ nameCtrl.errors?.['minlength'].requiredLength }} characters
    </p>
  </div>

  <input name="email" ngModel required email #emailCtrl="ngModel" />
  <input name="age" ngModel type="number" min="18" max="120" />
</form>
Built-in validators: required, minlength, maxlength, pattern, email, min, max. Export the control with #name="ngModel" to access its validation state.

Why it matters: Tests understanding of declarative validation. Shows you can validate forms in templates without component code.

Real applications: Email validation, password requirements, age checks, phone formatting, form field constraints.

Common mistakes: Developers don't export the control losing access to errors object. They check errors directly instead of specific error keys. They don't use pattern validator for complex rules.

Custom validators for template-driven forms are created as directives that implement the Validator interface. They are registered using the NG_VALIDATORS multi-provider token. The directive's validate() method receives the control and returns an error object or null. You apply the custom validator by adding the directive's selector as an attribute on the form control.
@Directive({
  selector: '[appForbiddenName]',
  providers: [{
    provide: NG_VALIDATORS,
    useExisting: ForbiddenNameDirective,
    multi: true
  }]
})
export class ForbiddenNameDirective implements Validator {
  @Input() appForbiddenName = '';

  validate(control: AbstractControl): ValidationErrors | null {
    if (!this.appForbiddenName) return null;
    const forbidden = new RegExp(this.appForbiddenName).test(control.value);
    return forbidden ? { forbiddenName: { value: control.value } } : null;
  }
}

// Usage
// <input name="username" ngModel appForbiddenName="admin" />
The directive registers itself as a validator using the NG_VALIDATORS multi-provider token. Angular calls the validate method on every value change.

Why it matters: Tests understanding of advanced validation patterns. Shows you can extend Angular's validation beyond built-in validators.

Real applications: Username availability checking, password confirmation matching, custom business rule validation, domain-specific rules.

Common mistakes: Developers implement Validator interface incorrectly. They don't use @Input for configuration parameters. They forget to register the directive in module declarations or component imports.

Export the ngModel as a template reference variable to access the control's validation state. Then use *ngIf to conditionally display error messages based on specific error keys. Check touched or dirty before showing errors so messages do not appear before the user interacts. Angular also adds CSS classes like ng-invalid and ng-touched automatically for styling.
<input name="email" ngModel required email
       #emailCtrl="ngModel" />

<div *ngIf="emailCtrl.invalid && (emailCtrl.dirty || emailCtrl.touched)"
     class="error-messages">
  <p *ngIf="emailCtrl.errors?.['required']" class="error">
    Email is required.
  </p>
  <p *ngIf="emailCtrl.errors?.['email']" class="error">
    Please enter a valid email address.
  </p>
</div>

<!-- CSS classes Angular adds automatically -->
<!-- .ng-valid / .ng-invalid -->
<!-- .ng-pristine / .ng-dirty -->
<!-- .ng-untouched / .ng-touched -->
Check dirty or touched before showing errors to avoid displaying messages before the user interacts with the control. Angular automatically adds CSS classes you can style.

Why it matters: Tests understanding of error display best practices. Shows you can provide good user feedback without overwhelming users.

Real applications: Form validation messages, success indicators, inline field status, error summaries, required field indicators.

Common mistakes: Developers show errors on untouched fields frustrating users. They check only .invalid instead of .touched. They don't use ng- CSS classes for styling.

Use the (ngSubmit) event on the form element to handle submission. Access form data through the template reference variable or component properties bound with ngModel. You can disable the submit button using [disabled]="form.invalid" to prevent invalid submissions. Always use ngSubmit instead of the native submit event to let Angular handle validation first.
<form #contactForm="ngForm" (ngSubmit)="submitForm(contactForm)">
  <input name="name" [(ngModel)]="contact.name" required />
  <input name="email" [(ngModel)]="contact.email" required email />
  <textarea name="message" [(ngModel)]="contact.message" required></textarea>

  <button type="submit" [disabled]="contactForm.invalid">Send</button>
</form>

// Component
contact = { name: '', email: '', message: '' };

submitForm(form: NgForm) {
  if (form.valid) {
    this.contactService.send(this.contact).subscribe({
      next: () => form.reset(),
      error: (err) => console.error(err)
    });
  }
}
Use ngSubmit instead of the native submit event to prevent default browser form submission. Call form.reset() after successful submission to clear all fields and validation states.

Why it matters: Tests understanding of form submission workflow. Shows you know how to handle success and error states.

Real applications: Login forms, contact forms, checkout flows, data entry screens, survey submissions.

Common mistakes: Developers don't validate before submission. They use native (submit) instead of (ngSubmit). They forget form.reset() leaving old data visible.

Template reference variables in forms provide access to the NgForm, NgModel, or NgModelGroup directive instances. By exporting with #var="ngModel", you get access to the directive's properties like valid, touched, and value. Without the export, the variable refers to the plain DOM element instead. This is essential for checking validation state and displaying error messages in the template.
<!-- Form-level reference -->
<form #myForm="ngForm">
  <!-- Control-level reference -->
  <input name="username" ngModel required #username="ngModel" />

  <!-- Access control state -->
  <p>Valid: {{ username.valid }}</p>
  <p>Touched: {{ username.touched }}</p>
  <p>Value: {{ username.value }}</p>

  <!-- Access form state -->
  <p>Form valid: {{ myForm.valid }}</p>
  <button [disabled]="myForm.invalid">Submit</button>
</form>
Without ="ngForm" or ="ngModel", the variable refers to the DOM element. With it, the variable refers to the Angular directive instance, providing access to validation state and form control methods.

Why it matters: Tests understanding of template reference variables and directive instances. Shows you know the difference between DOM elements and Angular directives.

Real applications: Accessing form/control state, triggering component methods, passing form data to handlers, conditional rendering based on validation.

Common mistakes: Developers forget to export the directive. They use the variable thinking it's the directive when it's the DOM element. They don't understand the export syntax.

NgModelGroup groups related form controls together as a sub-group within a form, creating a nested object in the form's value. You apply it using the ngModelGroup directive on a container element like a div. It aggregates the validation status of all controls inside it, so you can check if the entire group is valid at once. This is useful for grouping related fields like address or contact information.
<form #f="ngForm">
  <div ngModelGroup="address" #addr="ngModelGroup">
    <input name="street" ngModel required />
    <input name="city" ngModel required />
    <input name="zip" ngModel required pattern="[0-9]{5}" />
  </div>

  <p *ngIf="addr.invalid">Address section has errors</p>

  <!-- Form value structure:
  {
    address: { street: '...', city: '...', zip: '...' }
  } -->
</form>
NgModelGroup nests the controls into a sub-object in the form value structure. It also aggregates validation status, so you can check if the entire group is valid or invalid as a unit.

Why it matters: Tests understanding of form structure and organization. Shows you can group related fields logically.

Real applications: Address fields grouped together, contact information grouping, multi-step form sections, nested object submission data.

Common mistakes: Developers don't use ngModelGroup for related fields losing structure. They don't understand it creates nested objects. They forget to export ngModelGroup for state access.

Async validators are directives registered under the NG_ASYNC_VALIDATORS token. They return a Promise or Observable that resolves to validation errors or null. This is useful for server-side checks like verifying if an email is already taken. Angular waits for all sync validators to pass before running async validators.
@Directive({
  selector: '[appUniqueEmail]',
  providers: [{
    provide: NG_ASYNC_VALIDATORS,
    useExisting: UniqueEmailDirective,
    multi: true
  }]
})
export class UniqueEmailDirective implements AsyncValidator {
  constructor(private userService: UserService) {}

  validate(control: AbstractControl): Observable<ValidationErrors | null> {
    return this.userService.checkEmail(control.value).pipe(
      map(exists => exists ? { emailTaken: true } : null),
      catchError(() => of(null))
    );
  }
}

// Usage: <input name="email" ngModel appUniqueEmail />
// <p *ngIf="emailCtrl.errors?.['emailTaken']">Email already taken</p>
While an async validator is pending, the control has a pending status. Angular waits for all sync validators to pass before running async validators.

Why it matters: Tests understanding of async validation patterns. Shows you can perform server-side validation asynchronously.

Real applications: Email uniqueness checking, username availability, code validation, SKU verification, domain availability checks.

Common mistakes: Developers run async validators for every keystroke without debouncing. They don't handle network errors properly. They forget the pending state exists.

Angular automatically adds CSS classes to form controls based on their state. The classes are ng-untouched/ng-touched for user focus, ng-pristine/ng-dirty for value changes, and ng-valid/ng-invalid for validation status. You can use these classes in your CSS to visually highlight fields with errors. These classes update automatically as the user interacts with the form.
/* Style invalid fields that have been touched */
input.ng-invalid.ng-touched {
  border: 2px solid red;
}

/* Style valid fields */
input.ng-valid.ng-touched {
  border: 2px solid green;
}

/* Style dirty fields */
input.ng-dirty {
  background-color: #fffde7;
}
The ng-touched and ng-dirty classes help you avoid showing error styles on a fresh, untouched form. Combine these classes for precise control over when validation styles appear.

Why it matters: Tests understanding of form styling and UX best practices. Shows you can provide visual feedback without component code.

Real applications: Styling valid vs invalid fields, highlighting required fields, showing error colors, disabling form sections.

Common mistakes: Developers don't use ng- classes using custom JavaScript instead. They style .ng-invalid on pristine forms showing errors prematurely. They forget these classes update automatically.

You can reset a template-driven form by calling the reset() method on the NgForm reference. This resets all controls to their initial values, clears validation errors, and reverts the pristine and touched states. You can optionally pass an object to reset() to set specific initial values for each control. This is commonly done after a successful form submission.
<form #myForm="ngForm" (ngSubmit)="onSubmit(myForm)">
  <input name="email" ngModel required />
  <input name="name" ngModel required />
  <button type="submit">Submit</button>
  <button type="button" (click)="myForm.reset()">Clear</button>
</form>

// Or reset with specific values
onSubmit(form: NgForm) {
  this.saveData(form.value);
  form.reset({ email: '', name: 'Default User' });
}
Calling reset() without arguments sets all fields to empty. Passing an object lets you set default values for specific controls after the reset.

Why it matters: Tests understanding of form state management. Shows you know how to clear forms properly after submission.

Real applications: After form submission success, clearing search inputs, resetting filter forms, multi-step wizard flows.

Common mistakes: Developers clear form by manually setting each field. They don't pass reset values losing opportunity to set defaults. They forget reset() clears validation state too.

Touched means the user has focused on the field and then moved away (blurred), regardless of whether the value changed. Dirty means the user has actually changed the value of the field. A field can be touched but not dirty if the user clicked in and out without typing. These states are commonly used to decide when to show validation error messages.
<input name="email" ngModel required #email="ngModel" />

<!-- Show error only after user has interacted -->
<div *ngIf="email.invalid && (email.touched || email.dirty)">
  Email is required
</div>

<!-- States explained -->
<!-- touched: user focused then blurred the field -->
<!-- dirty: user changed the value -->
<!-- pristine: value has not been changed (opposite of dirty) -->
Pristine is the opposite of dirty — it means the value has not been changed. Untouched is the opposite of touched. Use these states to control when validation messages and error styles appear.

Why it matters: Tests understanding of form interaction tracking. Shows you know how to provide user-friendly validation feedback.

Real applications: Showing errors only after engagement, save indicators on dirty forms, resetting pristine state on successful submit.

Common mistakes: Developers confuse touched/dirty treating them the same. They show validation errors on pristine forms frustrating users. They don't reset dirty/touched state after save.

You can disable the submit button by binding the [disabled] property to the form's invalid state. Export the form as a template reference variable with #form="ngForm" and check its valid or invalid property. When all required fields are filled and all validations pass, the button becomes enabled automatically. This is a common UX pattern that prevents users from submitting incomplete forms.
<form #registrationForm="ngForm" (ngSubmit)="submit(registrationForm)">
  <input name="name" ngModel required minlength="3" />
  <input name="email" ngModel required email />

  <button type="submit" [disabled]="registrationForm.invalid">
    Register
  </button>
  <p>Form valid: {{ registrationForm.valid }}</p>
</form>
The disabled binding reacts to form state changes in real time. You can also show the form's valid/invalid status to give users feedback as they fill in the fields.

Why it matters: Tests understanding of reactive UI patterns. Shows you can provide real-time feedback to users.

Real applications: Disabling submit button until valid, showing "form incomplete" messaging, enabling action buttons conditionally.

Common mistakes: Developers disable buttons with JavaScript instead of data binding. They don't check form.invalid before allowing submission. They don't show users why the button is disabled.

Yes, standalone components can use template-driven forms by importing FormsModule directly in the component's imports array. This is the recommended approach in Angular 14+ applications that use standalone components. You get the same ngModel, ngForm, and validation features as with NgModule-based apps. Each standalone component declares its own dependencies explicitly.
@Component({
  standalone: true,
  imports: [FormsModule, CommonModule],
  selector: 'app-contact',
  template: '<form #f="ngForm" (ngSubmit)="send(f)">' +
    '<input name="email" ngModel required email />' +
    '<button [disabled]="f.invalid">Send</button></form>'
})
export class ContactComponent {
  send(form: NgForm) { console.log(form.value); }
}
With standalone components, you import FormsModule per component instead of in a shared NgModule. This improves tree-shaking and makes dependencies more explicit.

Why it matters: Tests understanding of standalone components. Shows you know how to use template-driven forms in modern Angular.

Real applications: Contact forms in standalone components, survey components, modal content with forms, reusable form components.

Common mistakes: Developers forget to import FormsModule in standalone component. They think template-driven forms don't work with standalone. They don't understand imports array in standalone components.