BuzzForm
BuzzFormDocs

Dynamic Values

Reference other field values or external context in labels, defaults, and validator args.

Dynamic References (Expressions)

BuzzForm supports dynamic references in field properties using { $data: path } and { $context: path } syntax. These are powered by the unified Expression system (Expr<T>).

Expression Types

An expression can be a literal value, a reference, or complex logic:

type Expr<T> = 
  | T                                     // Literal (string, number, boolean)
  | { $data: string }                     // Reference form data
  | { $context: string }                  // Reference external context
  | { $args: string }                     // Reference dynamic arguments (e.g. validator args)
  | { $text: string }                     // Template interpolation
  | { $when: Condition; $then: Expr<T>; $else: Expr<T> } // Ternary logic
  | { $fn: string; args?: Record<string, Expr<any>> }    // Registry function
  | ((ctx: ExprContext) => T);            // Inline JS function

This works for:

  • ExprString — labels, descriptions, placeholders
  • ExprNumber — validator args, min/max values
  • ExprBooleandisabled, required, condition

Advanced Expressions

Unified expressions support powerful logic nodes that minimize the need for custom code.

Template Interpolation ($text)

Build dynamic strings using ${/path} syntax. References are resolved from form data:

{
  type: "text",
  name: "confirmation",
  // "Hello John, please confirm your email."
  label: { $text: "Hello ${/firstName}, please confirm your email." },
}

Validator Argument Interpolation

Inside validation messages, you can reference the resolved arguments of the validator using the /args/ prefix:

{
  type: "text",
  name: "username",
  validate: {
    onBlur: {
      checks: [
        {
          type: "minLength",
          // "Must be at least 10 characters (you have 5)."
          message: { $text: "Must be at least ${/args/min} characters (you have ${/username})." },
          args: { min: 10 },
        },
      ],
    },
  },
}

Conditional Branching ($when)

Return different values based on a boolean condition:

{
  type: "text",
  name: "discountCode",
  // Label changes based on membership status
  label: {
    $when: { $data: "/isVip", eq: true },
    $then: "Enter VIP Code",
    $else: "Enter Promo Code"
  }
}

Registry Functions ($fn)

Call custom functions registered in registries.fns. Arguments themselves can be expressions:

// 1. Register function
<FormProvider registries={{ 
  fns: { 
    calculateTotal: ({ args }) => (args.price as number) * 1.1 
  } 
}}>

// 2. Call in schema
{
  type: "number",
  name: "totalPrice",
  defaultValue: { 
    $fn: "calculateTotal", 
    args: { price: { $data: "/basePrice" } } 
  }
}

Inline Functions

For schemas defined in JavaScript (not JSON), you can use standard functions. This is the ultimate escape hatch:

{
  type: "text",
  name: "manager",
  // JS logic with full context access
  condition: ({ data, context }) => {
    return data.role === "Manager" && context.isEnterprise;
  }
}

Using $data

Reference other field values in your form:

In Labels and Descriptions

const contactSchema = defineSchema({
  fields: [
    {
      type: "text",
      name: "companyName",
      label: "Company Name",
      required: true,
    },
    {
      type: "text",
      name: "confirmationMessage",
      // Label shows the company name dynamically
      label: { $data: "/companyName" },
      description: "Confirm your registration for: { $data: /companyName }",
      placeholder: `Enter confirmation for { $data: "/companyName" }`,
    },
  ],
});

In Default Values

const profileSchema = defineSchema({
  fields: [
    { type: "text", name: "firstName", label: "First Name" },
    { type: "text", name: "lastName", label: "Last Name" },
    {
      type: "text",
      name: "fullName",
      label: "Full Name",
      // Mirror firstName as default (note: this doesn't concatenate)
      defaultValue: { $data: "/firstName" },
    },
  ],
});

In Validator Args

const customFormSchema = defineSchema({
  fields: [
    {
      type: "text",
      name: "minChars",
      label: "Minimum Characters",
      defaultValue: 5,
    },
    {
      type: "text",
      name: "username",
      label: "Username",
      validate: {
        onBlur: {
          checks: [
            {
              type: "minLength",
              message: "Username too short",
              // Use value from another field
              args: { min: { $data: "/minChars" } },
            },
          ],
        },
      },
    },
  ],
});

Using $context

Reference external data passed via contextData:

const form = useForm({
  schema: contactSchema,
  contextData: {
    userRole: "admin",
    maxUploadSize: 10,
    allowedDomains: ["company.com", "partner.com"],
    supportEmail: "support@example.com",
  },
});

const contactSchema = defineSchema({
  fields: [
    {
      type: "email",
      name: "workEmail",
      label: "Work Email",
      validate: {
        onBlur: {
          checks: [
            {
              type: "validateEmailDomain",
              message: "Please use a company email",
              args: {
                allowedDomains: { $context: "/allowedDomains" },
                maxSize: { $context: "/maxUploadSize" },
              },
            },
          ],
        },
      },
    },
    {
      type: "text",
      name: "adminNotes",
      label: "Admin Notes",
      // Only shown for admin users
      condition: { $context: "/userRole", eq: "admin" },
    },
    {
      type: "text",
      name: "supportContact",
      label: "Support Contact",
      // Display dynamic support email
      description: { $context: "/supportEmail" },
    },
  ],
});

Path Format

BuzzForm uses JSON Pointer format (RFC 6901):

Path TypeExampleDescription
Root field/usernameField at root level
Nested field/address/cityField in a group
Array item/items/0/nameSpecific array item
Context data/user/roleContext data path

Important:

  • Paths must start with /
  • Use / as separator (not .)
  • Array indices are numeric (/items/0)

resolveExpr Helper

Resolve dynamic values manually:

import { resolveExpr } from "@buildnbuzz/form-react";

const ctx = {
  data: { name: "John", age: 30 },
  context: { maxAge: 100 }
};

// Resolve a $data reference
resolveExpr<string>({ $data: "/name" }, ctx);
// "John"

// Resolve a $context reference
resolveExpr<number>({ $context: "/maxAge" }, ctx);
// 100

// Literal values pass through unchanged
resolveExpr("hello", ctx);
// "hello"

Examples

Dynamic Field Options

const contactSchema = defineSchema({
  fields: [
    {
      type: "select",
      name: "country",
      label: "Country",
      options: ["US", "UK", "Canada"],
    },
    {
      type: "select",
      name: "region",
      label: "State / Province",
      options: [
        { label: "California", value: "CA" },
        { label: "New York", value: "NY" },
        // Disable options dynamically
        { label: "Texas", value: "TX", disabled: { $data: "/country", neq: "US" } },
      ],
    },
  ],
});

Dynamic Validation Thresholds

const applicationSchema = defineSchema({
  fields: [
    {
      type: "number",
      name: "experienceLevel",
      label: "Experience Level (1-10)",
      min: 1,
      max: 10,
    },
    {
      type: "text",
      name: "justification",
      label: "Justification",
      // Required only for high experience
      required: { $data: "/experienceLevel", gte: 7 },
      validate: {
        onSubmit: {
          checks: [
            {
              type: "minLength",
              message: "High experience requires detailed justification",
              // Longer min length for higher experience
              args: {
                min: { $data: "/experienceLevel" }, // min = experienceLevel value
              },
            },
          ],
        },
      },
    },
  ],
});

Dynamic Collapsible State

const settingsSchema = defineSchema({
  fields: [
    {
      type: "checkbox",
      name: "showAdvanced",
      label: "Show Advanced Options",
    },
    {
      type: "collapsible",
      label: "Advanced Settings",
      // Collapse/expand based on checkbox
      collapsed: { $data: "/showAdvanced", eq: false },
      fields: [
        // ... advanced fields
      ],
    },
  ],
});

Limitations

Not supported:

  • ❌ Relative paths like ../sibling (use absolute /sibling)
  • ❌ Dot notation like user.name (use /user/name)

Supported:

  • ✅ In message strings (Interpolation & logic)
  • ✅ In args object of validation checks
  • ✅ In labels, descriptions, placeholders
  • ✅ In defaultValue
  • ✅ In boolean props (disabled, required, collapsed)
  • ✅ In validator args

Dynamic Options

For static option lists with dynamic disabled values, use Condition:

{
  type: "select",
  name: "state",
  options: [
    { label: "California", value: "CA" },
    { label: "Texas", value: "TX", disabled: { $data: "/country", neq: "US" } },
  ],
}

For async option loading (REST APIs, GraphQL, etc.), use the option resolver registry instead. This provides automatic dependency tracking, request deduplication, and cascading dropdown support:

const resolvers = defineOptionResolvers({
  listStates: async ({ data }) => {
    const res = await fetch(`/api/states?country=${data.country}`);
    const json = await res.json();
    return json.states.map((s: State) => ({ label: s.name, value: s.code }));
  },
});

// Schema
{
  type: "select",
  name: "state",
  options: { resolver: "listStates" },
  dependencies: ["/country"],  // auto re-fetches when country changes
}

See Option Resolvers for the full guide.

On this page