ObjectUIObjectUI

Expression System

Object UI includes a powerful expression system that enables dynamic, data-driven UIs. Expressions allow you to reference data, compute values, and create conditional logic directly in your JSON schemas.

Overview

Expressions are JavaScript-like code snippets embedded in schemas using the ${} syntax:

{
  "type": "text",
  "value": "Hello, ${user.name}!"
}

With data:

const data = { user: { name: "Alice" } }

This renders: "Hello, Alice!"

Basic Syntax

Simple Property Access

Access data properties using dot notation:

{
  "type": "text",
  "value": "${user.firstName}"
}

Nested Properties

Access nested objects:

{
  "type": "text",
  "value": "${user.address.city}"
}

Array Access

Access array elements:

{
  "type": "text",
  "value": "${users[0].name}"
}

String Interpolation

Mix expressions with static text:

{
  "type": "text",
  "value": "Welcome, ${user.firstName} ${user.lastName}!"
}

Operators

Arithmetic Operators

{
  "type": "text",
  "value": "Total: ${price * quantity}"
}

Supported: +, -, *, /, %

Comparison Operators

{
  "type": "badge",
  "variant": "${score >= 90 ? 'success' : 'default'}"
}

Supported: >, <, >=, <=, ==, ===, !=, !==

Logical Operators

{
  "type": "button",
  "visibleOn": "${user.isAdmin && user.isActive}"
}

Supported: &&, ||, !

Ternary Operator

{
  "type": "text",
  "value": "${count > 0 ? count + ' items' : 'No items'}"
}

Conditional Properties

visibleOn

Show component when expression is true:

{
  "type": "button",
  "label": "Admin Panel",
  "visibleOn": "${user.role === 'admin'}"
}

hiddenOn

Hide component when expression is true:

{
  "type": "section",
  "hiddenOn": "${user.settings.hideSection}"
}

disabledOn

Disable component when expression is true:

{
  "type": "button",
  "label": "Submit",
  "disabledOn": "${form.submitting || !form.isValid}"
}

Data Context

Accessing Root Data

The root data object is available directly:

const data = {
  user: { name: "Alice" },
  settings: { theme: "dark" }
}

<SchemaRenderer schema={schema} data={data} />
{
  "type": "text",
  "value": "Theme: ${settings.theme}"
}

Scoped Data

Some components provide scoped data:

{
  "type": "list",
  "items": "${users}",
  "itemTemplate": {
    "type": "card",
    "title": "${item.name}",    // 'item' is scoped data
    "body": "${item.email}"
  }
}

Index in Loops

Access the current index in loops:

{
  "type": "list",
  "items": "${users}",
  "itemTemplate": {
    "type": "text",
    "value": "#${index + 1}: ${item.name}"
  }
}

Built-in Functions

String Functions

{
  "type": "text",
  "value": "${user.name.toUpperCase()}"
}

Available:

  • toUpperCase(), toLowerCase()
  • trim(), trimStart(), trimEnd()
  • substring(start, end)
  • replace(search, replace)
  • split(separator)
  • includes(substring)
  • startsWith(prefix), endsWith(suffix)

Array Functions

{
  "type": "text",
  "value": "Total users: ${users.length}"
}
{
  "type": "text",
  "value": "${users.map(u => u.name).join(', ')}"
}

Available:

  • length
  • map(fn), filter(fn), reduce(fn, initial)
  • join(separator)
  • slice(start, end)
  • includes(item)
  • find(fn), findIndex(fn)
  • some(fn), every(fn)

Number Functions

{
  "type": "text",
  "value": "Price: ${price.toFixed(2)}"
}

Available:

  • toFixed(decimals)
  • toPrecision(digits)
  • toString()

Math Functions

{
  "type": "text",
  "value": "${Math.round(average)}"
}

Available: All standard Math functions

  • Math.round(), Math.floor(), Math.ceil()
  • Math.min(), Math.max()
  • Math.abs()
  • Math.random()

Date Functions

{
  "type": "text",
  "value": "${new Date().toLocaleDateString()}"
}

Complex Expressions

Nested Ternary

{
  "type": "badge",
  "variant": "${
    status === 'active' ? 'success' :
    status === 'pending' ? 'warning' :
    status === 'error' ? 'destructive' :
    'default'
  }"
}

Combining Operators

{
  "type": "alert",
  "visibleOn": "${
    (user.role === 'admin' || user.role === 'moderator') &&
    user.isActive &&
    !user.isSuspended
  }"
}

Array Methods

{
  "type": "text",
  "value": "${
    users
      .filter(u => u.isActive)
      .map(u => u.name)
      .join(', ')
  }"
}

Practical Examples

User Greeting

{
  "type": "text",
  "value": "${
    new Date().getHours() < 12 ? 'Good morning' :
    new Date().getHours() < 18 ? 'Good afternoon' :
    'Good evening'
  }, ${user.firstName}!"
}

Status Badge

{
  "type": "badge",
  "text": "${status}",
  "variant": "${
    status === 'completed' ? 'success' :
    status === 'in_progress' ? 'info' :
    status === 'pending' ? 'warning' :
    'default'
  }"
}

Price Formatting

{
  "type": "text",
  "value": "$${(price * quantity).toFixed(2)}"
}

Empty State

{
  "type": "empty-state",
  "visibleOn": "${items.length === 0}",
  "message": "No items to display",
  "description": "Start by adding your first item"
}

Percentage Bar

{
  "type": "progress",
  "value": "${(completed / total) * 100}",
  "label": "${completed} of ${total} completed"
}

Conditional Styling

{
  "type": "card",
  "className": "${
    isPriority ? 'border-red-500 border-2' :
    isCompleted ? 'opacity-50' :
    'border-gray-200'
  }"
}

Form Expressions

Dependent Fields

{
  "type": "form",
  "body": [
    {
      "type": "select",
      "name": "country",
      "label": "Country",
      "options": ["USA", "Canada", "Mexico"]
    },
    {
      "type": "select",
      "name": "state",
      "label": "State/Province",
      "visibleOn": "${form.country === 'USA'}",
      "options": ["CA", "NY", "TX"]
    }
  ]
}

Dynamic Validation

{
  "type": "input",
  "name": "email",
  "label": "Email",
  "required": true,
  "validations": {
    "isEmail": true,
    "errorMessage": "Please enter a valid email"
  }
}

Computed Fields

{
  "type": "input",
  "name": "total",
  "label": "Total",
  "value": "${form.price * form.quantity}",
  "disabled": true
}

Performance Considerations

Expensive Computations

Expressions are re-evaluated when data changes. Avoid expensive operations:

// ❌ Bad: Complex computation in expression
{
  "type": "text",
  "value": "${users.map(u => expensiveOperation(u)).join(', ')}"
}

// ✅ Good: Pre-compute in data
const data = {
  processedUsers: users.map(u => expensiveOperation(u))
}

Caching

The expression engine automatically caches results when data doesn't change.

Security

Sandboxed Execution

Expressions run in a sandboxed environment and can only access:

  • The data context you provide
  • Built-in JavaScript functions (Math, Date, String, Array methods)

They cannot access:

  • Browser APIs (window, document, localStorage)
  • Node.js APIs (fs, path, etc.)
  • Global variables
  • Function constructors

Sanitization

All expression outputs are automatically sanitized to prevent XSS attacks.

Debugging Expressions

Expression Errors

Invalid expressions show helpful error messages:

{
  "type": "text",
  "value": "${user.invalidProperty}"
}

Error: "Cannot read property 'invalidProperty' of undefined"

Debug Mode

Enable debug mode to see expression evaluation:

<SchemaRenderer 
  schema={schema} 
  data={data}
  debug={true}
/>

This logs all expression evaluations to the console.

Advanced Usage

Custom Functions

Extend the expression context with custom functions:

import { getExpressionEvaluator } from '@object-ui/core'

const evaluator = getExpressionEvaluator()

evaluator.registerFunction('formatCurrency', (value: number) => {
  return new Intl.NumberFormat('en-US', {
    style: 'currency',
    currency: 'USD'
  }).format(value)
})

Use in schemas:

{
  "type": "text",
  "value": "${formatCurrency(price)}"
}

Custom Operators

Register custom operators for domain-specific logic:

evaluator.registerOperator('contains', (array, item) => {
  return array.includes(item)
})
{
  "type": "button",
  "visibleOn": "${user.permissions contains 'admin'}"
}

Best Practices

1. Keep Expressions Simple

// ❌ Bad: Too complex
{
  "value": "${users.filter(u => u.age > 18).map(u => ({...u, isAdult: true})).reduce((acc, u) => acc + u.score, 0)}"
}

// ✅ Good: Pre-compute complex logic
{
  "value": "${adultUsersScore}"
}

2. Use Meaningful Variable Names

// ❌ Bad
{
  "visibleOn": "${x && y || z}"
}

// ✅ Good
{
  "visibleOn": "${isAdmin && isActive || isSuperUser}"
}

3. Handle Null/Undefined

// ❌ Bad: Might throw error
{
  "value": "${user.address.city}"
}

// ✅ Good: Safe access
{
  "value": "${user.address?.city || 'N/A'}"
}

4. Use TypeScript

Define your data types:

interface UserData {
  user: {
    name: string
    role: 'admin' | 'user'
    isActive: boolean
  }
}

const data: UserData = { /* ... */ }
<SchemaRenderer schema={schema} data={data} />

Next Steps

On this page