<!-- 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.
<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.
<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.
<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.
@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.
<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.
<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.
<!-- 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.
<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.
@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.
/* 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.
<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.
<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.
<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.
@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.