Repeatable groups
Repeatable field groups
A FieldDescription with fields becomes a repeatable group instead of a registry-rendered leaf field: data[name] becomes an array of items, each shaped by the nested fields. Every adapter's MultiFieldInput renders one nested form per item plus "Add"/"Remove" controls automatically - no adapter-specific wiring required.
const fields: FieldDescription[] = [
{
name: 'contacts',
type: 'group', // any type key works; only `fields` matters for grouping
label: 'Contacts',
fields: [
{ name: 'email', type: 'text', label: 'Email' },
{ name: 'phone', type: 'text', label: 'Phone' },
],
defaultItem: { email: '', phone: '' }, // seed values for a new item
minItems: 1,
maxItems: 5,
},
];Set keyField to an item property (e.g. an id) so each item keeps a stable list identity; without it the array index is used, which reassociates item state when an item is reordered or removed from the middle.
The isFieldGroup, createGroupItem, canAddGroupItem, and canRemoveGroupItem helpers back this feature and are exported for adapters (or apps) that need to replicate the same add/remove bounds logic outside of MultiFieldInput.
Group array helpers
Every adapter's MultiFieldInput renders add/remove controls itself. These helpers are for driving a group's array yourself — a drag handle, a "duplicate row" button, a custom group renderer. All are pure and never mutate the input:
import {
moveGroupItem,
swapGroupItems,
insertGroupItem,
isFieldGroup,
createGroupItem,
canAddGroupItem,
canRemoveGroupItem,
focusFirstInvalidField,
} from '@dynamic-field-kit/core';
const reordered = moveGroupItem(items, 3, 0); // same array back if out of range
const withRow = insertGroupItem(items, 1, createGroupItem(field));
if (canAddGroupItem(field, items)) {
/* respects maxItems */
}
// After a failed submit: focus + scroll to the first [aria-invalid="true"] field
focusFirstInvalidField(formElement);