The simplified array API provides a concise way to define dynamic arrays. Instead of manually specifying each item in fields, you provide a template for the item structure and a value array with initial data. Add and remove buttons are generated automatically.
When should I use this? For most array use cases -- primitive lists, object lists, and standard add/remove workflows -- the simplified API is the right choice. For advanced scenarios like heterogeneous items or custom button placement, see Form Arrays (Complete).
Interactive Demo
Overview
The simplified API is built around template and value, plus optional minLength/maxLength:
| Property | Description |
|---|---|
template |
Defines the structure of a single array item (single field or field array) |
value |
Initial data array -- each element creates one pre-filled item |
minLength |
Minimum number of array items (form invalid if fewer) |
maxLength |
Maximum number of array items (form invalid if more) |
validators |
Rules over the item list -- ctx.value() is the array of items |
validationMessages |
Messages keyed by error kind for the array's own validators |
validateWhenHidden |
Run the array's validators while it is hidden (default false) |
The template shape determines the array variant:
- Single field (
template: { ... }) -- primitive array (e.g.['tag1', 'tag2']) - Array of fields (
template: [{ ... }, { ... }]) -- object array (e.g.[{ name: 'Jane', phone: '555' }])
// Discriminant: template is a single field → primitive array
{ key: 'tags', type: 'array', template: { key: 'value', type: 'input', label: 'Tag' } }
// Discriminant: template is an array of fields → object array
{ key: 'contacts', type: 'array', template: [
{ key: 'name', type: 'input', label: 'Name' },
{ key: 'phone', type: 'input', label: 'Phone' },
] }Primitive Arrays
For simple arrays of scalar values like ['angular', 'typescript'], provide a single field as the template and a flat array as the value:
{
key: 'tags',
type: 'array',
template: { key: 'value', type: 'input', label: 'Tag', required: true },
value: ['angular', 'typescript'],
}This renders two input fields pre-filled with 'angular' and 'typescript', each with an auto-generated Remove button. An Add button is placed after the array.
Form value output:
{
tags: ['angular', 'typescript'];
}Object Arrays
For arrays of objects like [{ name: 'Jane', phone: '555' }], provide an array of fields as the template and an array of objects as the value:
{
key: 'contacts',
type: 'array',
template: [
{ key: 'name', type: 'input', label: 'Contact Name', required: true },
{ key: 'phone', type: 'input', label: 'Phone Number' },
],
value: [
{ name: 'Jane', phone: '555-1234' },
{ name: 'Bob', phone: '555-5678' },
],
}Each value object is matched to the template by key. If a value object omits a key, the corresponding field renders without an initial value.
Form value output:
{
contacts: [
{ name: 'Jane', phone: '555-1234' },
{ name: 'Bob', phone: '555-5678' },
];
}Empty Arrays
Omit value or set value: [] to start with an empty array. Users populate it via the auto-generated Add button:
// No value property — starts empty
{
key: 'tags',
type: 'array',
template: { key: 'value', type: 'input', label: 'Tag' },
}
// Explicit empty array — same behavior
{
key: 'emails',
type: 'array',
template: [
{ key: 'address', type: 'input', label: 'Email Address', required: true },
{ key: 'label', type: 'select', label: 'Type', options: [
{ label: 'Work', value: 'work' },
{ label: 'Personal', value: 'personal' },
] },
],
value: [],
}Button Customization
By default, the library generates an Add button (label: "Add") after the array and a Remove button (label: "Remove") inside each item. You can customize both via addButton and removeButton:
{
key: 'tags',
type: 'array',
template: { key: 'value', type: 'input', label: 'Tag' },
value: ['angular'],
addButton: { label: 'Add Tag', props: { color: 'primary' } },
removeButton: { label: 'Delete', props: { color: 'warn' } },
}Button Config Properties
| Property | Type | Description |
|---|---|---|
label |
string |
Custom label text for the button |
props |
Record |
Additional properties passed to the button component |
Button Opt-Out
Set addButton: false or removeButton: false to suppress the corresponding auto-generated button entirely:
// No add button — items are only removable, not addable
{
key: 'defaultTags',
type: 'array',
template: { key: 'value', type: 'input', label: 'Tag' },
value: ['featured', 'popular'],
addButton: false,
}
// No remove button — items are only addable, not removable
{
key: 'skills',
type: 'array',
template: { key: 'value', type: 'input', label: 'Skill' },
value: ['TypeScript'],
removeButton: false,
}
// No buttons at all — static list rendered from initial values
{
key: 'readonlyItems',
type: 'array',
template: { key: 'value', type: 'input', label: 'Item' },
value: ['A', 'B', 'C'],
addButton: false,
removeButton: false,
}Setting removeButton: false simply omits the auto-generated remove control. Primitive items always render as plain fields, never wrapped in a row; for object arrays, the remove button is omitted from the item's field list.
Complete Example: Primitive Array
A full working configuration for a tag list with custom buttons:
import { FormConfig } from '@ng-forge/dynamic-forms';
const formConfig: FormConfig = {
fields: [
{
key: 'projectName',
type: 'input',
label: 'Project Name',
required: true,
},
{
key: 'tags',
type: 'array',
template: { key: 'value', type: 'input', label: 'Tag', required: true, minLength: 2 },
value: ['angular', 'open-source'],
addButton: { label: 'Add Tag' },
removeButton: { label: 'Remove Tag' },
},
],
};
// Form value:
// {
// projectName: 'My Project',
// tags: ['angular', 'open-source']
// }Complete Example: Object Array
A full working configuration for a contact list:
import { FormConfig } from '@ng-forge/dynamic-forms';
const formConfig: FormConfig = {
fields: [
{
key: 'teamName',
type: 'input',
label: 'Team Name',
required: true,
},
{
key: 'members',
type: 'array',
template: [
{ key: 'name', type: 'input', label: 'Full Name', required: true },
{ key: 'email', type: 'input', label: 'Email', required: true },
{
key: 'role',
type: 'select',
label: 'Role',
options: [
{ label: 'Developer', value: 'dev' },
{ label: 'Designer', value: 'design' },
{ label: 'Manager', value: 'manager' },
],
},
],
value: [
{ name: 'Alice', email: 'alice@example.com', role: 'dev' },
{ name: 'Bob', email: 'bob@example.com', role: 'design' },
],
addButton: { label: 'Add Member' },
removeButton: { label: 'Remove' },
},
],
};
// Form value:
// {
// teamName: 'Frontend Team',
// members: [
// { name: 'Alice', email: 'alice@example.com', role: 'dev' },
// { name: 'Bob', email: 'bob@example.com', role: 'design' },
// ]
// }Form Value Output
The simplified API produces the same form values as the complete API:
| Template Shape | Value Shape | Output |
|---|---|---|
| Single field | Flat array of scalars | { tags: ['angular', 'typescript'] } |
| Array of fields | Array of objects | { contacts: [{ name: 'Jane', phone: '555' }] } |
| Any template | [] or omitted |
{ items: [] } |
Array Size Validation
Use minLength and maxLength to constrain the number of items. When violated, the form becomes invalid (e.g. submit button is disabled).
{
key: 'tags',
type: 'array',
template: { key: 'value', type: 'input', label: 'Tag' },
value: ['angular'],
minLength: 1,
maxLength: 5,
}Both properties are optional and can be used independently or together.
Array-Level Validation
Size bounds only count items. To validate their contents -- typically comparing two fields of the same row -- declare validators on the array. ctx.value() resolves to the item list:
{
key: 'periods',
type: 'array',
template: [
{ key: 'from', type: 'input', label: 'From', props: { type: 'datetime-local' } },
{ key: 'to', type: 'input', label: 'To', props: { type: 'datetime-local' } },
],
value: [{ from: '', to: '' }],
minLength: 1,
validators: [{ type: 'custom', functionName: 'periodOrder' }],
validationMessages: { periodOrder: 'Every period must end after it starts.' },
}const periodOrder: CustomValidator = (ctx) => {
const rows = (ctx.value() as { from?: string; to?: string }[]) ?? [];
return rows.some((r) => r.from && r.to && r.to < r.from) ? { kind: 'periodOrder' } : null;
};The message renders below the array's items via the built-in field-errors wrapper, which is appended automatically when a container declares validators. See Array-Level Validation in the complete API guide for the full details and how to restyle the wrapper.
Conditional Visibility
Simplified arrays support logic for conditional visibility, just like the complete API:
{
key: 'extras',
type: 'array',
template: { key: 'value', type: 'input', label: 'Extra' },
value: [],
logic: [{ type: 'hidden', condition: { type: 'fieldValue', fieldPath: 'showExtras', operator: 'equals', value: false } }],
}Decision Guide: Simplified vs Complete API
| Scenario | Recommended API |
|---|---|
| Uniform items with standard add/remove | Simplified |
| Pre-filled values from a data source | Simplified |
| Custom button labels or styling | Simplified |
| Empty array populated by users | Simplified |
| Static list (no add/remove buttons) | Simplified |
| Heterogeneous items (different fields per item) | Complete |
| Custom button placement (buttons outside array) | Complete |
| Programmatic control via EventDispatcher | Complete |
| Multiple add buttons (append, prepend, insert) | Complete |
As a rule of thumb: start with the simplified API. If you hit a limitation, switch to the complete API.