Template-Driven vs Reactive Forms in Angular
Question 1: What are the key differences between Template-Driven and Reactive Forms?
Answer: The main differences are:
Setup and Configuration
- Template-Driven Forms: Simpler setup, more HTML-centric
- Reactive Forms: More explicit setup, TypeScript-centric
Form Building
- Template-Driven: Forms built using directives in template
- Reactive: Forms built programmatically in component class
Data Flow
- Template-Driven: Two-way binding with [(ngModel)]
- Reactive: Reactive patterns with observables
Validation
- Template-Driven: HTML5 validation attributes
- Reactive: Custom validation functions
Testing
- Template-Driven: More challenging to test
- Reactive: Easier to test due to synchronous nature
Question 2: How do you implement Template-Driven Forms?
Answer: Here’s a complete example of a Template-Driven Form:
// user-form.component.ts
import { Component } from '@angular/core';
import { FormsModule } from '@angular/forms';
@Component({
selector: 'app-user-form',
standalone: true,
imports: [FormsModule],
template: `
<form #userForm="ngForm" (ngSubmit)="onSubmit(userForm)">
<!-- Name field with validation -->
<div class="form-group">
<label for="name">Name</label>
<input
type="text"
id="name"
name="name"
#name="ngModel"
[(ngModel)]="user.name"
required
minlength="3"
class="form-control"
[class.is-invalid]="name.invalid && name.touched"
>
@if (name.invalid && name.touched) {
<div class="invalid-feedback">
@if (name.errors?.['required']) {
Name is required
}
@if (name.errors?.['minlength']) {
Name must be at least 3 characters
}
</div>
}
</div>
<!-- Email field with pattern validation -->
<div class="form-group">
<label for="email">Email</label>
<input
type="email"
id="email"
name="email"
#email="ngModel"
[(ngModel)]="user.email"
required
pattern="[a-z0-9._%+-]+@[a-z0-9.-]+\.[a-z]{2,}$"
class="form-control"
[class.is-invalid]="email.invalid && email.touched"
>
@if (email.invalid && email.touched) {
<div class="invalid-feedback">
Please enter a valid email
</div>
}
</div>
<button
type="submit"
[disabled]="userForm.invalid"
class="btn btn-primary"
>
Submit
</button>
</form>
`
})
export class UserFormComponent {
user = {
name: '',
email: ''
};
onSubmit(form: NgForm) {
if (form.valid) {
console.log('Form submitted', this.user);
}
}
}
Question 3: How do you implement Reactive Forms?
Answer: Here’s a comprehensive example of a Reactive Form:
// user-form.component.ts
import { Component, OnInit } from '@angular/core';
import {
FormBuilder,
FormGroup,
Validators,
ReactiveFormsModule
} from '@angular/forms';
@Component({
selector: 'app-user-form',
standalone: true,
imports: [ReactiveFormsModule],
template: `
<form [formGroup]="userForm" (ngSubmit)="onSubmit()">
<!-- Name field -->
<div class="form-group">
<label for="name">Name</label>
<input
id="name"
type="text"
formControlName="name"
class="form-control"
[class.is-invalid]="isFieldInvalid('name')"
>
@if (isFieldInvalid('name')) {
<div class="invalid-feedback">
@if (userForm.get('name')?.errors?.['required']) {
Name is required
}
@if (userForm.get('name')?.errors?.['minlength']) {
Name must be at least 3 characters
}
</div>
}
</div>
<!-- Email field -->
<div class="form-group">
<label for="email">Email</label>
<input
id="email"
type="email"
formControlName="email"
class="form-control"
[class.is-invalid]="isFieldInvalid('email')"
>
@if (isFieldInvalid('email')) {
<div class="invalid-feedback">
@if (userForm.get('email')?.errors?.['required']) {
Email is required
}
@if (userForm.get('email')?.errors?.['email']) {
Please enter a valid email
}
</div>
}
</div>
<!-- Password with custom validator -->
<div class="form-group">
<label for="password">Password</label>
<input
id="password"
type="password"
formControlName="password"
class="form-control"
[class.is-invalid]="isFieldInvalid('password')"
>
@if (isFieldInvalid('password')) {
<div class="invalid-feedback">
Password must contain at least 8 characters,
one uppercase letter, and one number
</div>
}
</div>
<button
type="submit"
[disabled]="userForm.invalid"
class="btn btn-primary"
>
Submit
</button>
</form>
`
})
export class UserFormComponent implements OnInit {
userForm!: FormGroup;
constructor(private fb: FormBuilder) {}
ngOnInit() {
this.userForm = this.fb.group({
name: ['', [
Validators.required,
Validators.minLength(3)
]],
email: ['', [
Validators.required,
Validators.email
]],
password: ['', [
Validators.required,
this.passwordValidator()
]]
});
// Subscribe to form changes
this.userForm.valueChanges.subscribe(value => {
console.log('Form value changed:', value);
});
}
passwordValidator() {
return (control: AbstractControl): ValidationErrors | null => {
const value = control.value;
const hasNumber = /[0-9]/.test(value);
const hasUpperCase = /[A-Z]/.test(value);
const isLengthValid = value?.length >= 8;
const valid = hasNumber && hasUpperCase && isLengthValid;
return valid ? null : { invalidPassword: true };
};
}
isFieldInvalid(fieldName: string): boolean {
const field = this.userForm.get(fieldName);
return field ? field.invalid && field.touched : false;
}
onSubmit() {
if (this.userForm.valid) {
console.log('Form submitted', this.userForm.value);
} else {
this.markFormGroupTouched(this.userForm);
}
}
private markFormGroupTouched(formGroup: FormGroup) {
Object.values(formGroup.controls).forEach(control => {
control.markAsTouched();
if (control instanceof FormGroup) {
this.markFormGroupTouched(control);
}
});
}
}
Interview Tips 💡
When to Use Each Type
- Template-Driven Forms:
- Simple forms
- Basic validation
- Quick to implement
- Reactive Forms:
- Complex forms
- Dynamic form fields
- Custom validation
- Unit testing requirements
- Template-Driven Forms:
Performance Considerations
// Reactive Forms: Better performance with large forms // Use form builder for better performance this.form = this.fb.group({ // form controls }); // Template-Driven: Can impact performance with many fields // Use trackBy for better performance in lists @for (item of items; track item.id) { <input [(ngModel)]="item.value" name="item-{{item.id}}"> }
Advanced Form Features
// Dynamic form controls addControl() { const control = new FormControl(''); (this.form.get('items') as FormArray).push(control); } // Custom async validator emailExistsValidator(http: HttpClient) { return (control: AbstractControl): Promise<ValidationErrors | null> => { return http.get(`/api/check-email/${control.value}`) .toPromise() .then(exists => exists ? { emailTaken: true } : null); }; }
Form State Management
// Reactive Forms state tracking this.form.statusChanges.subscribe(status => { console.log('Form status:', status); }); this.form.valueChanges.subscribe(value => { console.log('Form value:', value); });
Testing Strategies
// Testing Reactive Forms describe('UserFormComponent', () => { it('should validate form', () => { component.userForm.controls['email'].setValue('invalid'); expect(component.userForm.valid).toBeFalsy(); component.userForm.controls['email'].setValue('valid@email.com'); expect(component.userForm.valid).toBeTruthy(); }); });
Error Handling Best Practices
// Centralized error messages const errorMessages = { required: 'This field is required', email: 'Please enter a valid email', minlength: (params: any) => `Minimum length is ${params.requiredLength} characters` }; getErrorMessage(control: AbstractControl): string { for (const errorName in control.errors) { if (control.errors[errorName]) { return errorMessages[errorName]; } } return ''; }
Remember: In interviews, emphasize:
- Understanding of both form types and their use cases
- Knowledge of form validation strategies
- Experience with complex form scenarios
- Understanding of reactive programming concepts
- Testing approaches for forms
- Performance implications
- Error handling strategies