5. Manage a dynamic array
Start with a small form-object template:
form = form({
contacts: array({
label: field(''),
email: field(''),
}),
});
The array starts empty. Each item added later gets an independent copy of the template.
Add array to the package import.
π Start with initial itemsβ
Pass an initial count when the form should start with ready-to-edit items:
form = form({
contacts: array({
label: field(''),
email: field(''),
}, 2),
});
Passing 2 as the second argument uses the initial-count shorthand and creates two
independent contact nodes from the template defaults. Basic structural operations act
directly on the array:
this.form.contacts.push({
label: 'Personal',
email: 'me@example.com',
});
this.form.contacts.removeAt(0);
this.form.contacts();
// Expected output:
// [
// { label: '', email: '' },
// { label: 'Personal', email: 'me@example.com' },
// ]
π Start with existing dataβ
Use initialValue when the array should start with complete domain values:
form = form({
contacts: array({
label: field(''),
email: field(''),
}, {
initialValue: [
{ label: 'Work', email: 'work@example.com' },
{ label: 'Personal', email: 'me@example.com' },
],
}),
});
Each value initializes an independent item node created from the same template.
β Add identity and collection validationβ
Once the basic collection is clear, add stable domain identity and array-level validation:
form = form({
// Existing profile and address branches...
contacts: array({
id: field(''),
label: field('Work', [required]),
email: field('', [required, email]),
primary: field(false),
}, {
initialValue: [{ id: 'primary', label: 'Primary', email: '', primary: true }],
trackBy: 'id',
validators: [uniqueItems('email')],
}),
});
Add uniqueItems to the package import.
The template is cloned into independent item nodes. trackBy: 'id' preserves those nodesβand their touched, dirty, and pending stateβwhen complete values arrive in a different order.
π Render and change the collectionβ
Track each node instance in Angular so structural moves retain their DOM and bindings:
<section>
<h2>Contacts</h2>
@for (contact of form.contacts; track contact; let index = $index) {
<input [formNode]="contact.label" />
<input type="email" [formNode]="contact.email" />
<button type="button" (click)="form.contacts.moveUp(index)">
Move up
</button>
<button type="button" (click)="form.contacts.removeAt(index)">
Remove
</button>
}
<button type="button" (click)="form.contacts.push()">
Add contact
</button>
</section>
Operations preserve the array's programmatic dirty state. New items use template defaults and start pristine and untouched.
this.form.contacts.push({
id: crypto.randomUUID(),
label: 'Personal',
email: '',
primary: false,
});
this.form.contacts.swap(0, 1);
π Related guides and referenceβ
- Dynamic arrays covers templates, factories, identity, reconciliation, patching, and every structural operation.
array()reference lists signatures, options, state, and edge cases.- Build a reorderable array shows movement controls and
server reconciliation with
trackBy. - Choosing a primitive compares an
array()node with an array-valuedfield().
Continue with Step 6: Validate asynchronously.