Files
metabuilder/frontends/codegen/docs/JSON_QUICK_REFERENCE.md
2026-03-09 22:30:41 +00:00

7.2 KiB

Quick Reference: JSON-Driven Pages

File Structure

src/
├── config/pages/
│   ├── model-designer.json          # Models page schema
│   ├── component-tree.json          # Component Trees page schema
│   └── workflow-designer.json       # Workflows page schema
├── components/
│   ├── JSONModelDesigner.tsx        # Models wrapper component
│   ├── JSONComponentTreeManager.tsx # Trees wrapper component
│   └── JSONWorkflowDesigner.tsx     # Workflows wrapper component
└── lib/
    └── json-ui/
        └── page-renderer.tsx        # Core JSON renderer

Creating a New JSON Page

1. Create the JSON Schema

src/config/pages/my-page.json:

{
  "id": "my-page",
  "name": "My Page",
  "layout": { "type": "single" },
  "dataSources": [
    {
      "id": "items",
      "type": "kv",
      "key": "app-items",
      "defaultValue": []
    },
    {
      "id": "selectedId",
      "type": "static",
      "defaultValue": null
    },
    {
      "id": "selectedItem",
      "type": "computed",
      "compute": "(data) => data.items?.find(i => i.id === data.selectedId)",
      "dependencies": ["items", "selectedId"]
    }
  ],
  "components": [
    {
      "id": "root",
      "type": "div",
      "props": { "className": "h-full p-6" },
      "children": []
    }
  ]
}

2. Create the Wrapper Component

src/components/JSONMyPage.tsx:

import { PageRenderer } from '@/lib/json-ui/page-renderer'
import myPageSchema from '@/config/pages/my-page.json'
import { PageSchema } from '@/types/json-ui'

interface JSONMyPageProps {
  items: any[]
  onItemsChange: (items: any[]) => void
}

export function JSONMyPage({ items, onItemsChange }: JSONMyPageProps) {
  const schema = myPageSchema as PageSchema

  const handleCustomAction = async (action: any, event?: any) => {
    console.log('[JSONMyPage] Custom action:', action, event)
  }

  return <PageRenderer schema={schema} onCustomAction={handleCustomAction} />
}

3. Register in Component Registry

src/lib/component-registry.ts:

JSONMyPage: lazyWithPreload(
  () => import('@/components/JSONMyPage').then(m => ({ default: m.JSONMyPage })),
  'JSONMyPage'
),

4. Add to Pages Config

src/config/pages.json:

{
  "id": "my-page-json",
  "title": "My Page (JSON)",
  "icon": "Icon",
  "component": "JSONMyPage",
  "enabled": true,
  "toggleKey": "myPageJSON",
  "order": 99,
  "props": {
    "state": ["items"],
    "actions": ["onItemsChange:setItems"]
  }
}

5. Create Seed Data

seed_kv_store_tool({
  key: "app-items",
  operation: "set",
  value: [
    { id: "1", name: "Item 1", description: "First item" },
    { id: "2", name: "Item 2", description: "Second item" }
  ]
})

Common Patterns

Data Source Types

KV (Persistent):

{
  "id": "myData",
  "type": "kv",
  "key": "app-my-data",
  "defaultValue": []
}

Static (Component State):

{
  "id": "tempValue",
  "type": "static",
  "defaultValue": ""
}

Computed (Derived):

{
  "id": "filteredItems",
  "type": "computed",
  "compute": "(data) => data.items.filter(i => i.active)",
  "dependencies": ["items"]
}

Component Bindings

Simple Binding:

{
  "type": "Text",
  "bindings": {
    "children": { "source": "itemName" }
  }
}

Path Binding:

{
  "type": "Text",
  "bindings": {
    "children": {
      "source": "selectedItem",
      "path": "name"
    }
  }
}

Transform Binding:

{
  "type": "Badge",
  "bindings": {
    "variant": {
      "source": "status",
      "transform": "(val) => val === 'active' ? 'success' : 'secondary'"
    }
  }
}

Event Handlers

Simple Action:

{
  "type": "Button",
  "events": [
    {
      "event": "click",
      "actions": [
        {
          "id": "open-dialog",
          "type": "set-value",
          "target": "dialogOpen",
          "value": true
        }
      ]
    }
  ]
}

Conditional Action:

{
  "event": "click",
  "actions": [...],
  "condition": "(data) => data.items.length > 0"
}

Conditional Rendering

{
  "type": "div",
  "condition": {
    "source": "selectedItem",
    "transform": "(val) => !!val"
  },
  "children": [...]
}

Layout Patterns

Sidebar + Main Content

{
  "type": "div",
  "props": { "className": "h-full flex" },
  "children": [
    {
      "id": "sidebar",
      "type": "div",
      "props": { "className": "w-80 border-r" },
      "children": []
    },
    {
      "id": "main",
      "type": "div",
      "props": { "className": "flex-1" },
      "children": []
    }
  ]
}

Empty State

{
  "type": "div",
  "condition": {
    "source": "items",
    "transform": "(val) => !val || val.length === 0"
  },
  "props": { "className": "text-center p-12" },
  "children": [
    {
      "type": "Heading",
      "props": { "children": "No Items Yet" }
    },
    {
      "type": "Button",
      "props": { "children": "Create First Item" },
      "events": [...]
    }
  ]
}

List/Grid

{
  "type": "div",
  "props": { "className": "grid grid-cols-3 gap-4" },
  "children": []
}

Debugging Tips

Log Data Sources

Add this to your wrapper component:

console.log('[Page] Schema:', schema)
console.log('[Page] Data sources:', schema.dataSources)

Check Computed Values

The PageRenderer's data object contains all data sources:

const { data } = useDataSources(schema.dataSources)
console.log('[Page] All data:', data)

Validate Bindings

Ensure source IDs match data source IDs:

{
  "bindings": {
    "prop": {
      "source": "myDataSource"  // Must match dataSources[].id
    }
  }
}

Best Practices

  1. Use KV for persistent data - User preferences, saved items, app state
  2. Use static for UI state - Dialog open/closed, selected tabs, temp values
  3. Use computed for derived data - Filtered lists, calculated totals, selected items
  4. Keep compute functions simple - Complex logic should be in custom hooks
  5. Name sources descriptively - selectedWorkflow not sel, filteredItems not items2
  6. Document complex schemas - Add comments in the JSON (strip before runtime)
  7. Test with seed data - Always provide realistic default data
  8. Validate schemas - Use TypeScript types to catch errors early

Performance Tips

  • Minimize computed dependencies - Only include what's actually used
  • Use path bindings - { source: "item", path: "name" } is more efficient
  • Lazy load heavy components - Use code splitting for complex editors
  • Cache expensive computations - Consider memoization for heavy transforms
  • Limit nesting depth - Deep component trees slow rendering

Common Issues

Issue: Computed value not updating Fix: Check dependencies array includes all used sources

Issue: Binding shows undefined Fix: Ensure data source exists and has a value before binding

Issue: Event not firing Fix: Verify event name matches React event (e.g., click not onClick)

Issue: Condition not working Fix: Transform function must return boolean, check for null/undefined

Issue: Component not rendering Fix: Ensure component type matches registry name exactly