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
- Use KV for persistent data - User preferences, saved items, app state
- Use static for UI state - Dialog open/closed, selected tabs, temp values
- Use computed for derived data - Filtered lists, calculated totals, selected items
- Keep compute functions simple - Complex logic should be in custom hooks
- Name sources descriptively -
selectedWorkflownotsel,filteredItemsnotitems2 - Document complex schemas - Add comments in the JSON (strip before runtime)
- Test with seed data - Always provide realistic default data
- 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