@mackehansson/schemaform

Repeaters

Render array fields as repeatable rows with add, remove, reorder, and row-scoped Controls.

A Repeater is an array field rendered from an Array UI Schema node. JSON Schema describes the array and its item shape; UI Schema describes how each row appears.

Array node

An Array node points at an array property with scope. Its rowTemplate is a layout tree used for every row.

{
  type: "Array",
  scope: "#/properties/members",
  label: "Team members",
  rowTemplate: [
    { type: "Control", scope: "#/properties/name" },
    { type: "Control", scope: "#/properties/role", widget: "select" },
  ],
}

The ShadCN Renderer turns that node into a dynamic list with add, remove, move-up, and move-down controls. minItems and maxItems remain JSON Schema constraints because they describe valid data.

Row-scoped Controls

Controls inside rowTemplate are scoped relative to the array item schema, not the root form schema. In an array at #/properties/members, this row Control:

{ type: "Control", scope: "#/properties/email" }

resolves per row to Working State paths such as members.0.email and members.1.email. The same template is reused as rows are added, removed, or reordered.

Add, remove, and reorder

The Renderer owns the row interaction. Adding appends a new empty object, removing deletes that row from Working State, and reordering moves the row value with its inputs. The Submission Payload includes the array when the Array node is visible, or when it opts into retainWhenHidden.

Live example

Try adding a row, removing one, and moving rows up or down. Each row uses the same row template, but resolves to its own item in Working State.

Live form

Team

Team members
Row 1
Row 2

Source

import type { FormDefinition } from "@mackehansson/schemaform";import { SchemaForm } from "@mackehansson/schemaform-shadcn";const teamForm = {  formatVersion: 1,  schema: {    type: "object",    required: ["teamName"],    properties: {      teamName: { type: "string", title: "Team name", minLength: 2 },      members: {        type: "array",        title: "Team members",        minItems: 1,        items: {          type: "object",          required: ["name", "role"],          properties: {            name: { type: "string", title: "Name", minLength: 1 },            role: {              type: "string",              title: "Role",              enum: ["Designer", "Engineer", "Manager"],            },            email: { type: "string", title: "Email", format: "email" },          },        },      },    },  },  uiSchema: {    pages: [      {        type: "Page",        title: "Team",        children: [          { type: "Control", scope: "#/properties/teamName" },          {            type: "Array",            scope: "#/properties/members",            label: "Team members",            rowTemplate: [              {                type: "Columns",                children: [                  { type: "Control", scope: "#/properties/name" },                  {                    type: "Control",                    scope: "#/properties/role",                    widget: "select",                  },                ],              },              { type: "Control", scope: "#/properties/email" },            ],          },        ],      },    ],  },  rules: [],} satisfies FormDefinition;export function App() {  return (    <SchemaForm      definition={teamForm}      defaultValues={{        teamName: "Launch team",        members: [          { name: "Mina", role: "Designer", email: "mina@example.com" },          { name: "Sam", role: "Engineer", email: "sam@example.com" },        ],      }}      onSubmit={(data) => console.log(data)}    />  );}

On this page