In Angular reactive forms, which FormArray methods add, insert, replace and remove controls, and what does each one do to the array?
answer
- array-like, but not a plain array
- push, insert, removeAt, setControl, clear
- negative indices work like splice
- structural changes do not mark dirty
basics
~10 sA FormArray changes shape through push(), insert(index, c), removeAt(index), setControl(index, c) and clear(). Each one updates the array's value and validity and emits valueChanges unless emitEvent is false, but none marks the array dirty.
solid answer
~40 s`FormArray` keeps an ordered list of child controls in `controls`, and you change it only through its methods: `push(control)` appends (it also accepts an array of controls), `insert(index, control)` splices one in, `removeAt(index)` takes one out, `setControl(index, control)` replaces one, and `clear()` empties it. `at(index)` and `length` read it. Every change registers or unregisters the child's parent link and re-runs `updateValueAndValidity()`, so `value`, `status`, `valueChanges` and `statusChanges` follow; pass `{emitEvent: false}` to keep the events quiet during a bulk change. Negative indices wrap from the end like `Array.splice`, so `removeAt(-1)` removes the last row. One detail interviewers probe: none of these structural changes marks the array dirty, so call `markAsDirty()` yourself if an added row should count as a user edit.
code
ts · 15 linesimport {FormArray, FormControl} from '@angular/forms';
const phones = new FormArray([new FormControl('555-0100')]);
phones.push(new FormControl('555-0101'));
phones.insert(0, new FormControl('555-0199'));
console.log(phones.value); // ['555-0199', '555-0100', '555-0101']
phones.removeAt(-1); // removes the last control
phones.setControl(0, new FormControl('555-0142'));
console.log(phones.value); // ['555-0142', '555-0100']
console.log(phones.dirty); // false - structural changes are not user edits
phones.clear();
console.log(phones.length); // 0go deeper
Recall push, insert, removeAt, setControl, clear, at and length on a FormArray, and that each one updates the array's value.
Explain the bookkeeping behind each mutation: parent registration, updateValueAndValidity, events and the emitEvent option, and why dirty is not set.
Batch structural changes with emitEvent: false and one final update, guard computed indices against negative values, and choose setControl over addControl when replacing.
Set conventions for repeating sections so structural edits, dirty tracking and save-button rules behave the same across every form in the app.
## What a `FormArray` is for In Angular's `@angular/forms`, a `FormArray` is a container control whose children are identified by **position**, not by name. It is the model behind any repeating part of a reactive form: order line items, phone numbers, answers to a variable list of questions. Its `value` is an array of its children's values, and its `status` is aggregated from them like a `FormGroup`'s. The children live in `controls`, which is a real JavaScript array, but you must not `push` into it directly: that would skip registering the child's parent and would not recompute the array's value or validity. Use the `FormArray`'s own methods. ## The mutation methods | Method | Effect | Index rule | |---|---|---| | `push(control)` | Appends one control, or several when given an array | End of the array | | `insert(index, control)` | Inserts one control before `index` | Like `splice(index, 0, c)` | | `removeAt(index)` | Removes one control | Like `splice(index, 1)` | | `setControl(index, control)` | Replaces the control at `index` | Like `splice(index, 1, c)` | | `clear()` | Removes every control | Not applicable | The read side is small: `at(index)` returns a child, and `length` is the number of children. `at()` accepts negative indices too. Every mutation does the same bookkeeping: 1. An added child is registered, which sets its `parent` so its changes propagate to the array; a removed child is detached from the array's change notifications. 2. It calls `updateValueAndValidity()`, so `value` and `status` are recomputed for the array and its ancestors. 3. Unless you pass `{emitEvent: false}`, it emits on `valueChanges` and `statusChanges`. ## Negative indices The index-taking methods follow `Array.splice` semantics. `removeAt(-1)` removes the last control, `insert(-1, c)` inserts before the last one, and a very negative index clamps to the start. This is convenient for "remove the last row" buttons, and a trap if a computed index goes below zero by mistake, because it silently acts on a real row instead of failing. ## Dirty and touched are not updated The source documents this explicitly for `push`, `insert` and `removeAt`: changing the structure does **not** mark the array dirty. `dirty` reflects user edits through the view. If adding or removing a row should enable a "Save changes" button that checks `form.dirty`, call `markAsDirty()` on the array after the change. ## `clear()` versus a loop To empty an array you could loop `removeAt(0)` until `length` is zero, but each call re-runs validation and emits events. `clear()` removes everything and recomputes once, which is simpler and cheaper. ## The `FormGroup` counterparts Named containers have a parallel set of methods, with one behavioural difference worth remembering: - `addControl(name, control)` adds a control, but if a control with that name **already exists it is not replaced**. - `setControl(name, control)` replaces an existing control, or adds it if absent. - `removeControl(name)` removes it; `contains(name)` checks for an **enabled** control with that name. ## Moving a row There is no `move()` method. To reorder, take the control out and put the **same instance** back at the new position: ```ts moveUp(i: number) { if (i === 0) return; const row = this.items.at(i); this.items.removeAt(i, {emitEvent: false}); this.items.insert(i - 1, row); } ``` Reusing the instance keeps the row's value, validity and touched state, and the guard prevents `i - 1` from becoming `-1`, which would wrap to the end instead of failing. ## Bulk changes without event storms When you rebuild many rows at once, pass `{emitEvent: false}` to each call and finish with a single `updateValueAndValidity()` so subscribers see one final state: ```ts import {FormArray, FormControl} from '@angular/forms'; const tags = new FormArray<FormControl<string>>([]); for (const t of ['urgent', 'gift', 'fragile']) { tags.push(new FormControl(t, {nonNullable: true}), {emitEvent: false}); } tags.updateValueAndValidity(); ``` ## Pitfalls - Mutating `controls` directly with array methods, which leaves the model inconsistent. - Expecting `push()` to make the form dirty. - Looping `removeAt(0)` where `clear()` does the job. - Relying on `addControl()` to swap a control that already exists; use `setControl()`.
- What goes wrong if you call phones.controls.push(new FormControl()) instead of phones.push(...)?The new control is in the array but never registered: its `parent` is not set, so its changes do not propagate, and the array's `value` and `status` are not recomputed. The form looks as if nothing was added until something else triggers validation. Always use the `FormArray` methods.
- Why might a Save button bound to form.dirty stay disabled after the user adds a row?`push()`, `insert()` and `removeAt()` do not mark the array dirty; `dirty` changes when a user edits a value through the view. Call `markAsDirty()` on the array after a structural change that should count as an edit.
saying these in an interview costs you the question
- You can push directly into formArray.controls.
- push() marks the FormArray dirty.
- removeAt(-1) throws because the index is negative.
- addControl() replaces an existing control with the same name.
- clear() only resets the values and keeps the controls.