ObjectStackObjectStack

Basic Component Schema

In ObjectUI, every form element is a Leaf Node in the render tree. These nodes map directly to Shadcn UI primitives, ensuring accessibility and a modern aesthetic.

Unlike traditional React development where you write <Input value={val} onChange={setVal} />, in ObjectUI you define the Structure and the Data Binding, and the Engine handles the state management.

1. The Universal Contract

All form components (Inputs, Selects, Checkboxes) inherit from a base FieldComponent interface.

Base Properties

PropertyTypeDescription
typestringThe component identifier (e.g., input.text).
bindstringCritical. The path to the data field (e.g., order.customer_name). The engine uses this for two-way binding.
labelstringThe text label displayed above the input.
descriptionstringHelper text displayed below the input (muted color).
placeholderstringPlaceholder text.
disabledbooleanStatic disabled state (or use dynamic expressions).
classNamestringTailwind classes for overriding styles (e.g., w-1/2 bg-gray-50).

2. Text Input (input.text)

The workhorse of any form. It handles string, number, and password inputs.

Schema Definition

{
  "type": "input.text",
  "bind": "contact.email",
  "props": {
    "label": "Email Address",
    "placeholder": "john@example.com",
    "inputType": "email",  // text, password, email, url, tel
    "autoFocus": true,
    "startIcon": "mail"    // Lucide Icon Name
  }
}

Shadcn Mapping

  • Renders a <Label> component.
  • Renders an <Input> component wrapped in a formatting div.
  • Renders a generic <p className="text-destructive"> if validation errors exist in the context.

3. Number Input (input.number)

While input.text can handle numbers, input.number provides specific behaviors for numeric data types (Int/Float/Currency).

Schema Definition

{
  "type": "input.number",
  "bind": "order.quantity",
  "props": {
    "label": "Quantity",
    "min": 1,
    "max": 100,
    "step": 1,
    "showStepper": true // Shows +/- buttons
  }
}

Percent & Currency Support

The renderer detects the metadata from ObjectQL. If the underlying field is type: currency, the component automatically formats the display (e.g., adds $ prefix) while keeping the raw value as a number.


4. Select (input.select)

Handles single-select scenarios. For multi-select or complex relation lookups, see Advanced Components.

Static Options

Options defined explicitly in the JSON.

{
  "type": "input.select",
  "bind": "project.priority",
  "props": {
    "label": "Priority",
    "options": [
      { "label": "High", "value": "high", "icon": "flame" },
      { "label": "Medium", "value": "medium" },
      { "label": "Low", "value": "low" }
    ]
  }
}

Dynamic Options (Expression)

Options derived from the current data context or global config.

{
  "type": "input.select",
  "bind": "user.department",
  "props": {
    "label": "Department",
    "options_expression": "root.settings.available_departments"
  }
}

5. Date Picker (input.date)

Handles Dates (YYYY-MM-DD) and Datetimes (ISO 8601).

Schema Definition

{
  "type": "input.date",
  "bind": "task.due_date",
  "props": {
    "label": "Due Date",
    "mode": "single",      // single, range
    "withTime": false,     // if true, shows time selector
    "disablePast": true
  }
}

Shadcn Mapping

  • Renders a <Popover> containing the <Calendar> component.
  • The raw value is always normalized to UTC string (2023-10-01T00:00:00Z) before saving.

6. Switch & Checkbox (input.boolean)

For boolean values.

{
  "type": "input.switch", // or "input.checkbox"
  "bind": "user.is_active",
  "props": {
    "label": "Active User",
    "description": "If disabled, user cannot log in."
  }
}

7. Handling Validation

One of the biggest benefits of ObjectUI is Unified Validation. You do not write validation logic in the JSON schema for basic constraints.

  1. Source: The validation rules come from ObjectQL (e.g., required: true, max: 50).
  2. Propagation: When the user hits "Save", the Engine runs validation.
  3. Display: If an error returns { field: "contact.email", message: "Invalid format" }, the Engine automatically injects this error state into the input.text component bound to contact.email.

You define the UI structure, the Backend defines the Rules.

Example: A Complete Form

Here is how these atoms come together in a layout:

{
  "type": "layout.stack",
  "props": { "gap": 4 },
  "children": [
    { 
      "type": "input.text", 
      "bind": "title", 
      "props": { "label": "Task Title", "required": true } 
    },
    {
      "type": "layout.grid",
      "props": { "columns": 2, "gap": 4 },
      "children": [
        { 
          "type": "input.select", 
          "bind": "priority", 
          "props": { 
            "label": "Priority", 
            "options": [{ "label": "High", "value": "H" }, { "label": "Low", "value": "L" }] 
          } 
        },
        { 
          "type": "input.date", 
          "bind": "due_date", 
          "props": { "label": "Due Date" } 
        }
      ]
    }
  ]
}

On this page