Docs
Edit and act

Table picker

Choose related records inside a form.

Use a tablePicker when a select does not show enough information to identify a relation. The nested table has its own catalog and stable row IDs.

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 },
          ],
        },
      ],
    },
  ],
};

Provide the nested catalog

The order recipe supplies customerConfig, customer records, order actions and the entry component used above. data handles a local choice list; actions.list can load remote choices. Keep syncUrl: false for a picker so editing the relation does not replace the main page’s query.

IDs and selection mode

getRowId must produce a stable string. parseValue converts that string to your stored value when needed. Use multiple: false for a scalar relation and multiple: true for an array. Keep initial form values consistent with that shape and validate the relationship again on the server.

When nesting this picker in a React catalog form, use the isolated Jotai wrapper and context-aware lookup from the complete order recipe. The verified registry shares form atoms between table instances; the wrapper prevents a duplicate editor while preserving the parent field value. Vue does not need this wrapper.

Try it inline

Open the existing order and choose a customer with the searchable table picker. Add a line to select a product.

Expand ↗

Sample data. Changes stay in this preview.

Expand ↗

Sample data. Changes stay in this preview.