Form repeater
Edit an array of records in one form.
Use form.repeater(name, fieldsOrCallback) for an action input that contains
an array of objects. The schema checks the array key, row field names, and
field value types. Place the returned block inside the form content.
import { defineResource, fields, form } from '@backlit/sdk'
import { z } from 'zod'
const orders = defineResource('orders', [])
const save = orders.action(
'save',
z.object({
lines: z
.array(
z.object({
title: z.string(),
quantity: z.number(),
isGift: z.boolean(),
giftNote: z.string().optional(),
})
)
.min(1)
.max(5),
}),
() => {}
)
orders.view('create', () => {
const editor = form(save)
const lines = editor
.repeater('lines', (row) => [
fields.text('title'),
fields.number('quantity'),
fields.boolean('isGift'),
fields
.text('giftNote')
.revealWhen(row.when('isGift').equals(true)),
])
.peers(['title', 'quantity'])
.setRowTitle('Line')
.setLimits({ min: 1, max: 5 })
.setAddAction({ label: 'Add line' })
.setRemoveAction({ label: 'Remove line' })
return editor.setContent([lines]).withInitialData({
lines: [
{ title: 'Notebook', quantity: 2, isGift: false },
],
})
})
export default orders{
"kind": "success",
"status": 200,
"node": {
"kind": "view",
"resource": "orders",
"name": "create",
"slots": {
"content": [
{
"kind": "form",
"target": {
"resource": "orders",
"action": "save"
},
"initial": {
"lines": [
{
"title": "Notebook",
"quantity": 2,
"isGift": false
}
]
},
"slots": {
"content": [
{
"kind": "form-repeater",
"name": "lines",
"fields": [
{
"kind": "text",
"name": "title",
"label": ""
},
{
"kind": "number",
"name": "quantity",
"label": ""
},
{
"kind": "boolean",
"name": "isGift",
"label": ""
},
{
"kind": "text",
"name": "giftNote",
"label": "",
"reveal": {
"kind": "condition-predicate",
"path": "isGift",
"op": "eq",
"right": {
"value": true
}
}
}
],
"peers": [
[
"title",
"quantity"
]
],
"add": {
"label": "Add line"
},
"remove": {
"label": "Remove line"
},
"rowTitle": "Line",
"limits": {
"min": 1,
"max": 5
}
}
]
}
}
]
}
}
}The callback receives a condition composer for one row. A path such as
isGift reads that row. It does not read the top-level form.
A lookup field argument inside a repeater must name a field in that row.
Configuration
Use peers(...sets) for fields on one line and setRoles(map) for individual
field roles. setAddAction({ label }) and setRemoveAction({ label }) set
control text. setRowTitle(title) names each row. setLimits({ min, max })
sets the row limits in the client. Validate the same limits in the action schema.
Seed rows with form.withInitialData({ lines: [...] }). Each row is remapped
through its fields. Defects include an indexed path such as lines.2.quantity.
The row count comes from initial data. When no rows are supplied, the client
uses the minimum. A repeater holds one run of fields per row; it does not
accept arbitrary content blocks.
revealWhen(condition) controls the complete repeater. It has no enable
condition. Use a field’s enableWhen to disable that input.