Angular

Reactive Forms

15 Questions

Reactive forms provide a model-driven approach where the form structure is defined explicitly in the component class. You use FormControl, FormGroup, and FormArray to build the form model with validators. The form model stays in sync with the template using directives like formGroup and formControlName. This approach provides better testability, immutable data flow, and reactive access to form state.
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.

FormControl represents a single input element in a form. It tracks the value, validation status, and user interaction state of the control. You create it with an initial value and optional validators as arguments. In Angular 14+, FormControl supports generics for type-safe value access.
// 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.

FormGroup groups multiple FormControls together as a single unit. It tracks the aggregate value and validation status of all children. If any child control is invalid, the entire group becomes invalid. You can nest FormGroups inside each other for complex form structures like address sub-forms.
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.

FormBuilder is a service that provides shorthand methods to create FormControl, FormGroup, and FormArray instances. It reduces the boilerplate of creating forms manually with new FormControl() calls. The array syntax ['value', validators] is the most concise way to define controls. Inject FormBuilder via the constructor or use inject(FormBuilder).
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.

FormArray manages a dynamic collection of controls indexed by number rather than name. It is ideal for lists like skills, phone numbers, or address entries that users can add and remove. You use push() to add controls and removeAt() to delete them. Access the FormArray through a getter for cleaner template binding.
@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.

Angular provides several built-in validators in the Validators class for common validation scenarios. These include required, minLength, maxLength, min, max, email, and pattern. You pass them as an array in the second argument of FormControl. Use Validators.compose() to combine multiple validators into one.
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.

Custom validators are plain functions that receive an AbstractControl and return a ValidationErrors object or null. They return null when valid and an error object with a descriptive key when invalid. For reusable validators with parameters, create a factory function that returns the validator. Cross-field validators are applied at the FormGroup level.
// 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.

Dynamic forms are built by programmatically adding and removing controls based on user actions or configuration data. You define a data structure describing the fields, then loop through it to create FormControls dynamically. Use methods like addControl() and removeControl() on FormGroup to modify the form at runtime. This pattern is useful for forms generated from server-side configurations or schemas.
@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.

valueChanges is an Observable that emits every time the value of a control, group, or array changes. It enables reactive programming patterns with forms like search-as-you-type and auto-save. You can also subscribe to statusChanges to track whether the form is VALID, INVALID, or PENDING. Both observables work on individual FormControls, FormGroups, and FormArrays.
// 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.

setValue requires you to provide values for all controls in the group and throws an error if any are missing. patchValue allows partial updates, only setting the controls you specify. Use setValue when loading complete data from an API and patchValue for partial UI interactions. Both methods also accept an options object to control event emission.
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 forms use directives like ngModel and Angular creates the form model automatically behind the scenes. Reactive forms define the form model explicitly in the component class using FormControl, FormGroup, and FormArray. Reactive forms are more testable because the model is in TypeScript, not the template. They also handle dynamic forms better and offer reactive access to changes via Observables.
// 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.

Angular 14 introduced strictly typed reactive forms. When you create a FormGroup or FormControl, Angular infers the types of all controls automatically. This means form.value, form.get(), and valueChanges all have proper TypeScript types instead of any. You can also use NonNullableFormBuilder to create controls that reset to their initial value instead of null.
// 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.

You can change validators at runtime using setValidators(), addValidators(), removeValidators(), and clearValidators(). After changing validators, you must call updateValueAndValidity() to re-run validation with the new rules. This is useful when validation requirements change based on user selections. For example, making a phone number required only when a specific contact method is chosen.
// 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.

You can disable or enable controls using the disable() and enable() methods on any FormControl or FormGroup. Disabled controls are excluded from the form's value by default but included when you use getRawValue(). You can also create a control as disabled initially using the object syntax. This is useful for read-only fields or fields editable only under specific conditions.
// 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 validators are validator functions that return a Promise or Observable instead of a synchronous result. They are used for validations that require server-side checks, like checking if a username is taken. Angular passes them as the third argument to FormControl, after sync validators. Async validators only run after all synchronous validators pass, and the control's status is set to PENDING while waiting.
// 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.