Docs
Edit and act

Collection fields

Edit arrays with stable identities, nested fields, and layered validation.

A collection field keeps an array in the parent form. It can add, edit, delete and reorder items inline or through an item dialog.

Configuration example

Start from the React quick start or Vue quick start. The following file extends their product catalog. Pass this configuration through getTableConfig in React or config in Vue.

order-form.ts
import type { FormConfig } from "@/components/ui/yayaw-table/components/forms/types";
import { customers, orderSchema } from "../shared/orders";
import { customerConfig } from "./order-config";
import { renderProductSelect } from "./product-select";

export const orderForm: FormConfig = {
  id: "orders",
  title: "Edit order",
  presentation: "modal",
  width: "64rem",
  schema: orderSchema,
  fields: [
    { name: "reference", label: "Reference", type: "text", bulkEdit: false },
    {
      name: "customerId",
      label: "Customer",
      type: "tablePicker",
      tablePicker: {
        tableType: "customers",
        config: customerConfig,
        data: customers,
        getRowId: (row) => String(row.id),
        multiple: false,
        syncUrl: false,
        maxHeight: "18rem",
      },
    },
    {
      name: "lines",
      label: "Order lines",
      type: "collection",
      collectionMode: "dialog",
      addLabel: "Add line",
      getItemKey: (line) => String(line.id),
      createItem: () => ({
        id: crypto.randomUUID(),
        productId: "",
        quantity: 1,
        allocations: [],
      }),
      columns: [
        { id: "productId", header: "Product" },
        { id: "quantity", header: "Quantity" },
      ],
      validateItem: (line) =>
        Number(line.quantity) > 0 && line.productId
          ? []
          : ["Choose a product and a positive quantity."],
      validateItems: (lines) =>
        lines.length ? [] : ["Add at least one line."],
      itemFields: [
        {
          name: "productId",
          label: "Product",
          type: "custom",
          renderField: renderProductSelect,
        },
        {
          name: "quantity",
          label: "Quantity",
          type: "number",
          min: 1,
          step: 1,
        },
        {
          name: "allocations",
          label: "Warehouse allocations",
          type: "collection",
          collectionMode: "inline",
          getItemKey: (allocation) => String(allocation.id),
          createItem: () => ({
            id: crypto.randomUUID(),
            warehouse: "",
            quantity: 1,
          }),
          itemFields: [
            { name: "warehouse", label: "Warehouse", type: "text" },
            { name: "quantity", label: "Quantity", type: "number", min: 1 },
          ],
        },
      ],
    },
  ],
};
order-form.ts
import type { FormConfig } from "@/components/ui/yayaw-table-vue/types";
import { customers, orderSchema } from "../shared/orders";
import { products } from "../shared/products";
import { customerConfig } from "./order-config";

export const orderForm: FormConfig = {
  id: "orders",
  title: "Edit order",
  presentation: "modal",
  width: "64rem",
  schema: orderSchema,
  fields: [
    { name: "reference", label: "Reference", type: "text", bulkEdit: false },
    {
      name: "customerId",
      label: "Customer",
      type: "tablePicker",
      tablePicker: {
        tableType: "customers",
        config: customerConfig,
        data: customers,
        getRowId: (row) => String(row.id),
        multiple: false,
        syncUrl: false,
        maxHeight: "18rem",
      },
    },
    {
      name: "lines",
      label: "Order lines",
      type: "collection",
      collectionMode: "dialog",
      addLabel: "Add line",
      getItemKey: (line) => String(line.id),
      createItem: () => ({
        id: crypto.randomUUID(),
        productId: "",
        quantity: 1,
        allocations: [],
      }),
      columns: [
        { id: "productId", header: "Product" },
        { id: "quantity", header: "Quantity" },
      ],
      validateItem: (line) =>
        Number(line.quantity) > 0 && line.productId
          ? []
          : ["Choose a product and a positive quantity."],
      validateItems: (lines) =>
        lines.length ? [] : ["Add at least one line."],
      itemFields: [
        {
          name: "productId",
          label: "Product",
          type: "select",
          options: products.map((product) => ({
            value: product.id,
            label: product.name,
          })),
        },
        {
          name: "quantity",
          label: "Quantity",
          type: "number",
          min: 1,
          step: 1,
        },
        {
          name: "allocations",
          label: "Warehouse allocations",
          type: "collection",
          collectionMode: "inline",
          getItemKey: (allocation) => String(allocation.id),
          createItem: () => ({
            id: crypto.randomUUID(),
            warehouse: "",
            quantity: 1,
          }),
          itemFields: [
            { name: "warehouse", label: "Warehouse", type: "text" },
            { name: "quantity", label: "Quantity", type: "number", min: 1 },
          ],
        },
      ],
    },
  ],
};

Stable item identity

Generate an item ID in createItem, then use it in getItemKey. An array index changes when items move. createActions can offer several item templates. columns defines the compact item summary; itemFields defines the editor. Use renderItemForm when the domain needs a custom editor.

Validate at each level

validateItem checks one item, validateItems checks the collection, and the form schema checks the submitted document. Nested itemFields can contain another collection. In the order recipe, lines contain warehouse allocations. Parent submission persists the complete nested value; the table does not invent a relational database schema.

Exercise cancellation

Open an item, change it, then cancel: the parent value must remain unchanged. Reorder two items and verify their IDs follow the records. Test an empty collection, an invalid nested item and a failed parent submission.

Try it inline

Open the existing order and add, reorder or remove nested lines before saving.

Expand ↗

Sample data. Changes stay in this preview.

Expand ↗

Sample data. Changes stay in this preview.