import { ReactiveFormsModule, FormGroup, FormControl, Validators } from '@angular/forms';
@Component({
selector: 'app-login',
template: '<form [formGroup]="loginForm" (ngSubmit)="onSubmit()">
<input formControlName="email" />
<input formControlName="password" type="password" />
<button [disabled]="loginForm.invalid">Login</button>
</form>'
})
export class LoginComponent {
loginForm = new FormGroup({
email: new FormControl('', [Validators.required, Validators.email]),
password: new FormControl('', [Validators.required, Validators.minLength(8)])
});
onSubmit() { console.log(this.loginForm.value); }
}
Reactive forms require importing ReactiveFormsModule. They offer better testability, immutable data flow, and reactive access to form state via Observables.
Why it matters: Tests understanding of model-driven forms. Shows you know when to use reactive vs template-driven.
Real applications: Complex registration forms, multi-step wizards, dynamic form fields, unit-testable form logic.
Common mistakes: Developers don't import ReactiveFormsModule. They use reactive forms for simple one-field forms. They don't test form logic properly.
// Create with initial value and validators
const name = new FormControl('John', [
Validators.required,
Validators.minLength(2)
]);
// Access properties
console.log(name.value); // 'John'
console.log(name.valid); // true
console.log(name.errors); // null
console.log(name.dirty); // false
console.log(name.touched); // false
// Update value
name.setValue('Jane');
name.patchValue('Jane');
// Reset to initial state
name.reset();
// Disable/enable
name.disable();
name.enable();
FormControl can be typed in Angular 14+: new FormControl<string>('') ensures type safety when accessing the value.
Why it matters: Tests understanding of individual form control management. Shows you know form control lifecycle.
Real applications: Email inputs with validation, password fields with strength checking, search boxes with suggestions.
Common mistakes: Developers don't understand FormControl standalone use. They forget to access .value property. They don't use setValue vs patchValue correctly.
const profileForm = new FormGroup({
firstName: new FormControl('', Validators.required),
lastName: new FormControl('', Validators.required),
address: new FormGroup({
street: new FormControl(''),
city: new FormControl(''),
zip: new FormControl('', Validators.pattern('[0-9]{5}'))
})
});
// Access nested value
console.log(profileForm.value);
// { firstName: '', lastName: '', address: { street: '', city: '', zip: '' } }
// Access nested control
profileForm.get('address.city')?.setValue('New York');
FormGroup aggregates the status of its children: if any child is invalid, the group is invalid. Use get() with dot notation to access nested controls.
Why it matters: Tests understanding of form grouping and composition. Shows you can build complex forms hierarchically.
Real applications: User profile forms with address sub-group, checkout with billing and shipping addresses, multi-section surveys.
Common mistakes: Developers put all controls at top level. They don't nest FormGroups for complex forms. They forget get() uses dot notation for nested access.
export class RegisterComponent {
form: FormGroup;
constructor(private fb: FormBuilder) {
this.form = this.fb.group({
name: ['', [Validators.required, Validators.minLength(3)]],
email: ['', [Validators.required, Validators.email]],
password: ['', [Validators.required, Validators.minLength(8)]],
address: this.fb.group({
street: [''],
city: [''],
zip: ['']
}),
hobbies: this.fb.array(['Reading'])
});
}
}
FormBuilder shorthand: fb.control(value, validators), fb.group({...}), fb.array([...]). The array syntax ['value', validators] is the most concise way to define controls with validation.
Why it matters: Tests understanding of form builder patterns. Shows you know DRY principles applying to forms.
Real applications: Quick form generation from configuration, reducing boilerplate in large forms, factory pattern for form creation.
Common mistakes: Developers manually create FormControl instances. They don't know shorthand syntax. They don't leverage FormBuilder for complex forms.
@Component({
template: '<div formArrayName="skills">
<div *ngFor="let skill of skills.controls; let i = index">
<input [formControlName]="i" />
<button (click)="removeSkill(i)">X</button>
</div>
<button (click)="addSkill()">Add Skill</button>
</div>'
})
export class SkillsComponent {
form = this.fb.group({
skills: this.fb.array(['Angular', 'TypeScript'])
});
get skills() { return this.form.get('skills') as FormArray; }
addSkill() { this.skills.push(this.fb.control('', Validators.required)); }
removeSkill(i: number) { this.skills.removeAt(i); }
constructor(private fb: FormBuilder) {}
}
FormArray methods: push(), removeAt(), insert(), at(), clear(). Use a getter to access the FormArray for cleaner template binding.
Why it matters: Tests understanding of dynamic form management. Shows you can handle variable-length lists.
Real applications: Phone number collection, skill lists in resumes, shopping cart items, repeating survey questions.
Common mistakes: Developers don't know FormArray exists using nested FormGroups. They directly modify array breaking reactivity. They forget getter pattern for clean templates.
import { Validators } from '@angular/forms';
this.form = this.fb.group({
name: ['', [Validators.required, Validators.minLength(2), Validators.maxLength(50)]],
email: ['', [Validators.required, Validators.email]],
age: [null, [Validators.required, Validators.min(18), Validators.max(120)]],
website: ['', Validators.pattern('https?://.+')],
bio: [''] // no validation
});
// Composed validator
const ctrl = new FormControl('', Validators.compose([
Validators.required,
Validators.minLength(3)
]));
// Check errors
if (this.form.get('email')?.hasError('required')) {
console.log('Email is required');
}
Built-in validators: required, requiredTrue, min, max, minLength, maxLength, pattern, email, nullValidator, compose, composeAsync.
Why it matters: Tests understanding of form validation. Shows you know built-in validation strategies.
Real applications: Email validation, password requirements, age verification, phone formatting, form field constraints.
Common mistakes: Developers don't use validators leaving forms unvalidated. They create custom validators for common cases (unnecessary). They don't check specific error keys.
// Validator function
function forbiddenValue(forbidden: string): ValidatorFn {
return (control: AbstractControl): ValidationErrors | null => {
if (!control.value) return null;
return control.value === forbidden
? { forbiddenValue: { value: control.value } }
: null;
};
}
// Cross-field validator (applied to FormGroup)
function passwordMatch(group: AbstractControl): ValidationErrors | null {
const pass = group.get('password')?.value;
const confirm = group.get('confirmPassword')?.value;
return pass === confirm ? null : { passwordMismatch: true };
}
// Usage
this.form = this.fb.group({
username: ['', forbiddenValue('admin')],
password: [''],
confirmPassword: ['']
}, { validators: passwordMatch });
Custom validators return null for valid and an error object for invalid. Cross-field validators are applied at the group level and have access to all child controls.
Why it matters: Tests understanding of custom validation. Shows you can extend form validation beyond built-in validators.
Real applications: Password confirmation, conditional field validation, domain-specific business rules, complex interdependent fields.
Common mistakes: Developers put validators in components instead of factories. They don't understand cross-field validators run at group level. They forget to handle null/empty values in validators.
@Component({
template: '<form [formGroup]="form">
<div *ngFor="let field of fields">
<label>{{ field.label }}</label>
<input [formControlName]="field.key" />
</div>
</form>'
})
export class DynamicFormComponent implements OnInit {
fields = [
{ key: 'name', label: 'Name', required: true },
{ key: 'email', label: 'Email', required: true },
{ key: 'phone', label: 'Phone', required: false }
];
form!: FormGroup;
ngOnInit() {
const controls: Record<string, FormControl> = {};
this.fields.forEach(f => {
controls[f.key] = new FormControl('', f.required ? Validators.required : []);
});
this.form = new FormGroup(controls);
}
}
You can also use addControl(), removeControl(), and setControl() on a FormGroup to dynamically modify the form structure at runtime.
Why it matters: Tests understanding of form composition at runtime. Shows you can build schema-driven forms.
Real applications: Conditional form fields, multi-step wizards, survey forms, forms generated from server configuration.
Common mistakes: Developers don't leverage dynamic forms for conditional UI. They hardcode all possible fields instead. They forget to update form structure when configuration changes.
// Listen to a single control
this.form.get('search')?.valueChanges.pipe(
debounceTime(300),
distinctUntilChanged(),
switchMap(term => this.searchService.search(term))
).subscribe(results => this.results = results);
// Listen to entire form
this.form.valueChanges.subscribe(value => {
console.log('Form value:', value);
this.autoSave(value);
});
// Listen to status changes
this.form.statusChanges.subscribe(status => {
console.log('Form status:', status); // VALID, INVALID, PENDING
});
valueChanges is commonly combined with RxJS operators like debounceTime, distinctUntilChanged, and switchMap to implement search-as-you-type, auto-save, and other reactive patterns.
Why it matters: Tests understanding of reactive form patterns. Shows you know RxJS integration with forms.
Real applications: Auto-complete searches, live validation feedback, auto-save functionality, cascading form fields.
Common mistakes: Developers don't use valueChanges creating manual change tracking. They don't debounce frequent updates harming performance. They forget statusChanges exists.
const form = new FormGroup({
name: new FormControl(''),
email: new FormControl(''),
age: new FormControl(null)
});
// setValue: must provide ALL controls (throws if any missing)
form.setValue({ name: 'John', email: 'john@test.com', age: 30 });
// patchValue: can provide a SUBSET of controls
form.patchValue({ name: 'Jane' });
// email and age remain unchanged
// setValue throws error if incomplete:
// form.setValue({ name: 'John' }); // ERROR!
// Reset form to initial values
form.reset();
// Reset with specific values
form.reset({ name: 'Default', email: '', age: null });
Use setValue when you have complete data (e.g., loading from API). Use patchValue when updating specific fields (e.g., from a partial UI interaction).
Why it matters: Tests understanding of form value updates. Shows you know when to use each method.
Real applications: Loading form from API (setValue), user editing specific fields (patchValue), form resets, form filling scenarios.
Common mistakes: Developers use setValue with incomplete data (throws errors). They don't understand patchValue ignores missing controls. They don't leverage options object for event control.
// Template-driven: model defined in template
<input name="email" [(ngModel)]="email" required />
// Reactive: model defined in component class
email = new FormControl('', [Validators.required, Validators.email]);
// Template: <input [formControl]="email" />
Template-driven forms are simpler for basic scenarios with few fields. Reactive forms are preferred for complex forms with dynamic fields, custom validation, and unit testing.
Why it matters: Tests understanding of architecture choice. Shows you know when to use each approach.
Real applications: Simple login (template-driven), complex checkout (reactive), admin dashboards (reactive), quick surveys (template-driven).
Common mistakes: Developers use same approach for all scenarios. They don't understand trade-offs between architectures. They mix both in same form causing confusion.
// Typed form - Angular 14+
const form = new FormGroup({
name: new FormControl('', { nonNullable: true }),
age: new FormControl<number | null>(null)
});
form.value.name // type: string (not any)
form.value.age // type: number | null
// NonNullableFormBuilder
const fb = inject(NonNullableFormBuilder);
const form = fb.group({
name: [''], // resets to '' instead of null
});
Typed forms catch type errors at compile time and provide better autocompletion in IDEs. The nonNullable option ensures controls reset to their initial value rather than null.
Why it matters: Tests understanding of modern Angular type safety. Shows you know TypeScript integration benefits.
Real applications: Large forms with many fields, team projects where type errors help, performance-critical apps.
Common mistakes: Developers don't use typed forms in new code. They use NonNullableFormBuilder incorrectly. They don't understand type inference benefits.
// Add validators dynamically
this.form.get('phone')?.setValidators([Validators.required, Validators.minLength(10)]);
this.form.get('phone')?.updateValueAndValidity();
// Remove all validators
this.form.get('phone')?.clearValidators();
this.form.get('phone')?.updateValueAndValidity();
// Conditionally add based on another field
this.form.get('contactMethod')?.valueChanges.subscribe(method => {
const phone = this.form.get('phone');
if (method === 'phone') phone?.addValidators(Validators.required);
else phone?.removeValidators(Validators.required);
phone?.updateValueAndValidity();
});
Always call updateValueAndValidity() after modifying validators. Without it, the control retains its old validation state and won't re-evaluate.
Why it matters: Tests understanding of dynamic validation. Shows you can adjust validation rules at runtime.
Real applications: Conditional field validation, permission-based form rules, A/B testing variations, multi-step workflows.
Common mistakes: Developers forget updateValueAndValidity() so validation doesn't run. They don't understand when to add vs remove validators. They reset validators unnecessarily.
// Disable/enable a single control
this.form.get('email')?.disable();
this.form.get('email')?.enable();
// Create initially disabled
email: new FormControl({ value: 'readonly@test.com', disabled: true })
// Disabled controls excluded from form.value
console.log(this.form.value); // excludes disabled controls
console.log(this.form.getRawValue()); // includes ALL controls
Use getRawValue() when you need the values of all controls including disabled ones. The disabled state is also reflected in the template through the native disabled attribute.
Why it matters: Tests understanding of control state management. Shows you know disabled control behavior.
Real applications: Read-only fields, conditional UI states, permissions-based field disabling, submission handling.
Common mistakes: Developers don't know getRawValue() exists. They use form.value forgetting disabled controls are excluded. They don't understand disabled affects submission.
// Async validator function
function uniqueEmail(http: HttpClient): AsyncValidatorFn {
return (control: AbstractControl): Observable<ValidationErrors | null> => {
return http.get<boolean>('/api/check-email?email=' + control.value).pipe(
map(exists => exists ? { emailTaken: true } : null),
catchError(() => of(null))
);
};
}
// Usage: third argument is async validators
email = new FormControl('', [Validators.required], [uniqueEmail(this.http)]);
// Template: show loading while checking
<span *ngIf="email.pending">Checking...</span>
While the async validator is running, the control status is PENDING. You can use this to show a loading indicator in the template using *ngIf="control.pending".
Why it matters: Tests understanding of async validation patterns. Shows you can perform server-side validation asynchronously.
Real applications: Username availability checking, email uniqueness verification, SKU validation, code confirmation.
Common mistakes: Developers create async validators without debouncing causing excessive requests. They don't handle Promise/Observable errors. They don't understand PENDING status.