skip to content

In Angular reactive forms, why does form.value leave out a disabled control, and how should you disable a control bound with formControlName?

level: middleimportance: should knowfreq 55%

answer

  1. value versus getRawValue()
  2. DISABLED is a status
  3. validators skipped when disabled
  4. the disabled attribute only warns
  5. disable() emits events too

basics

~20 s

A disabled control is excluded from its parent's value and validity, so form.value omits it while getRawValue() includes it. Disable it through the model with control.disable() or {value, disabled: true}; a [disabled] binding on formControlName only logs a warning.

solid answer

~40 s

Disabled is a control status (`DISABLED`), not just a DOM attribute. A disabled control skips its validators, does not count toward its parent's validity, and is left out of the parent's `value`, which is why `form.value` for a shipping form with a locked country has no `country`. `getRawValue()` returns every child regardless, so submit handlers that need locked fields should use it. To disable a control in a reactive form, change the model: `control.disable()` and `control.enable()`, or create it with `new FormControl({value: 'FR', disabled: true})`; the directive then sets the DOM's `disabled` property. Binding `[disabled]` on an element with `formControlName` does not change the control and only logs a development warning. Remember that `disable()` emits `valueChanges` and `statusChanges`, so calling it from a value listener can loop.

code

ts · 31 lines
ts
import {Component, inject} from '@angular/core';
import {NonNullableFormBuilder, ReactiveFormsModule} from '@angular/forms';

@Component({
  selector: 'app-shipping-country',
  imports: [ReactiveFormsModule],
  template: `
    <form [formGroup]="form" (ngSubmit)="save()">
      <input formControlName="city" />
      <input formControlName="country" />
      <button type="button" (click)="lockCountry()">Lock country</button>
      <button type="submit">Save</button>
    </form>
  `,
})
export class ShippingCountry {
  private readonly fb = inject(NonNullableFormBuilder);
  readonly form = this.fb.group({
    city: '',
    country: {value: 'FR', disabled: false},
  });

  lockCountry(): void {
    this.form.controls.country.disable(); // model change; the directive disables the input
  }

  save(): void {
    console.log(this.form.value); // {city} while country is disabled
    console.log(this.form.getRawValue()); // {city, country}
  }
}

go deeper

for a junior

Know that disabled controls vanish from form.value and appear in getRawValue(), and that you disable them with disable(), not the attribute.

for a middle

Explain DISABLED as a status: validators skipped, excluded from parent validity and value, and why the [disabled] binding only warns.

for a senior

Catch the production bugs: locked fields dropped on save, listeners looping through disable(), and disabled chosen where read-only was intended.

for a principal

Agree on a convention for locked data, disabled versus read-only and value versus getRawValue, so forms save consistently across the app.

## Disabled is part of the model In reactive forms, "disabled" is one of the four control statuses: `VALID`, `INVALID`, `PENDING` and **`DISABLED`**. Setting it has three effects on the control and its ancestors: - **No validation.** A disabled control does not run its validators, so it has no `errors`. - **No say in validity.** A parent group's status ignores disabled children, so a disabled but empty required field does not make the form invalid. - **No place in `value`.** A `FormGroup` builds its `value` only from **enabled** children. The one exception is a group that is itself entirely disabled: then its value includes all children. ## value versus getRawValue() | Read | Includes disabled children | Typical use | |---|---|---| | `form.value` | no | the data a user can currently edit | | `form.getRawValue()` | yes | the full record to save, locked fields included | This surprises people in submit handlers. A shipping form might lock `country` once an address lookup succeeds. `form.value` then has no `country`, and a save that posts `form.value` quietly drops it. `getRawValue()` is the fix; its typed return also includes every key, while `value`'s type marks keys as optional for this reason. ## How to disable a reactive control Change the **model**, and let the directive update the DOM: 1. At creation: `new FormControl({value: 'FR', disabled: true})`, or the same `{value, disabled}` object in a builder. 2. Later: `form.controls.country.disable()` and `.enable()`. 3. A whole section: `form.controls.address.disable()` disables every child. What does **not** work is binding the attribute: ```html <select formControlName="country" [disabled]="locked"></select> ``` The reactive directives declare a `disabled` input only to log a **development-mode warning** telling you to set `disabled` when you create the control or to call `enable()`/`disable()`. The binding does not change the control's status, and the warning mentions "changed after checked" errors as the reason the model is the source of truth. ## Side effects of disable() and enable() - Both recompute status and value and **emit** `valueChanges`, `statusChanges` and events on the control and its ancestors, unless you pass `{emitEvent: false}`. - A listener that disables a control whenever a value changes can therefore trigger itself; guard with `emitEvent: false` or check the current state first. - A custom control bound to a disabled `FormControl` is told through its value accessor's `setDisabledState()`; implementing that is a custom-control concern. ## Toggling sections conditionally A common requirement is "enable the company fields only when the business checkbox is ticked". The reactive way is a single listener that calls `enable()` or `disable()` on the section: 1. Subscribe to the checkbox control's `valueChanges`, not to the whole form, so the section's own emissions do not re-trigger the listener. 2. Call `companyGroup.enable()` or `companyGroup.disable()` accordingly. 3. Read `getRawValue()` at submit time if the company fields must be sent even when locked, or `value` if a disabled section should be omitted. This keeps validation honest: while the section is disabled its required fields do not block the form. ## Disabled or read-only? Disabled fields are excluded from the value and cannot receive focus. If the user should see and tab to a value that will be submitted but not edited, a **read-only** input bound to an enabled control is often the better design, with `readonly` set on the element. Reach for `disable()` when the field genuinely should not take part in validation or the value. ## Checklist for "my field disappears on submit" 1. Is the control disabled at submit time? Check `form.controls.x.status`. 2. Is the handler posting `form.value` instead of `getRawValue()`? 3. Did a `valueChanges` listener disable it as a side effect? 4. Is a `[disabled]` binding hiding the fact that the control is still enabled, or the reverse?

  • Every control in an address group is disabled. What is address.value?
    The full object with every child. A group normally builds its value from enabled children only, but when the group itself is disabled, which happens when all its children are, it includes all of them. The status is `DISABLED`, and the parent form then leaves the whole group out of its own value.
  • A valueChanges subscriber disables the postcode control whenever the country changes, and the handler runs more often than expected. Why?
    `disable()` recomputes the group and emits `valueChanges` and `statusChanges` on the control and its ancestors. If the subscriber listens at group level, it is triggered again by its own `disable()` call. Pass `{emitEvent: false}` to `disable()`, or subscribe to the country control only.

saying these in an interview costs you the question

  • form.value always contains every control
  • A [disabled] binding on formControlName disables the control
  • A disabled required field makes the form invalid
  • getRawValue() skips disabled controls
  • disable() changes state without emitting any events