Skip to main content

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);

Continue with Step 6: Validate asynchronously.