Skip to content
Backlit
Esc
navigateopen⌘Jpreview
On this page

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.

Was this page helpful?