mirror of
https://github.com/johndoe6345789/low-code-react-app-b.git
synced 2026-04-24 13:44:54 +00:00
Merge pull request #109 from johndoe6345789/codex/fix-json-ui-framework-rendering-issues
Stabilize JSON UI: bindings, conditionals, loops, and types
This commit is contained in:
@@ -109,7 +109,8 @@ Render lists from arrays:
|
||||
{
|
||||
"loop": {
|
||||
"source": "items",
|
||||
"itemVar": "item"
|
||||
"itemVar": "item",
|
||||
"indexVar": "index"
|
||||
},
|
||||
"children": [...]
|
||||
}
|
||||
@@ -129,6 +130,107 @@ Show/hide based on conditions:
|
||||
}
|
||||
```
|
||||
|
||||
## 🧭 Schema Patterns & Examples
|
||||
|
||||
### Conditional Branches
|
||||
|
||||
Conditionals can return a single component, an array of components, or a string payload:
|
||||
|
||||
```json
|
||||
{
|
||||
"id": "admin-greeting",
|
||||
"type": "div",
|
||||
"conditional": {
|
||||
"if": "user.isAdmin",
|
||||
"then": [
|
||||
{ "id": "admin-title", "type": "h2", "children": "Welcome, Admin!" },
|
||||
{ "id": "admin-subtitle", "type": "p", "children": "You have full access." }
|
||||
],
|
||||
"else": "You do not have access."
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Loop Templates (itemVar/indexVar)
|
||||
|
||||
Loop containers render once and repeat their children as the template. The `itemVar` and
|
||||
`indexVar` values are available in bindings and expressions inside the loop:
|
||||
|
||||
```json
|
||||
{
|
||||
"id": "activity-list",
|
||||
"type": "div",
|
||||
"className": "space-y-2",
|
||||
"loop": {
|
||||
"source": "activities",
|
||||
"itemVar": "activity",
|
||||
"indexVar": "idx"
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"id": "activity-row",
|
||||
"type": "div",
|
||||
"children": [
|
||||
{ "id": "activity-index", "type": "span", "dataBinding": "idx" },
|
||||
{ "id": "activity-text", "type": "span", "dataBinding": "activity.text" }
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### Dot-Path Bindings
|
||||
|
||||
Bindings support `foo.bar` access for both `dataBinding` and `bindings`:
|
||||
|
||||
```json
|
||||
{
|
||||
"id": "profile-name",
|
||||
"type": "p",
|
||||
"dataBinding": "user.profile.fullName"
|
||||
}
|
||||
```
|
||||
|
||||
```json
|
||||
{
|
||||
"id": "profile-avatar",
|
||||
"type": "Avatar",
|
||||
"bindings": {
|
||||
"src": { "source": "user", "path": "profile.avatarUrl" },
|
||||
"alt": { "source": "user.profile.fullName" }
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Transforms
|
||||
|
||||
Transforms can be applied to bindings for light formatting in JSON:
|
||||
|
||||
```json
|
||||
{
|
||||
"id": "user-score",
|
||||
"type": "span",
|
||||
"dataBinding": {
|
||||
"source": "user",
|
||||
"path": "score",
|
||||
"transform": "data ?? 0"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
```json
|
||||
{
|
||||
"id": "user-initials",
|
||||
"type": "Badge",
|
||||
"bindings": {
|
||||
"children": {
|
||||
"source": "user.profile.fullName",
|
||||
"transform": "data.split(' ').map(part => part[0]).join('')"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 🧩 Available Components
|
||||
|
||||
### Layout
|
||||
|
||||
@@ -1,25 +1,10 @@
|
||||
import { createElement, useMemo } from 'react'
|
||||
import { createElement, useMemo, Fragment } from 'react'
|
||||
import { UIComponent, Binding, ComponentRendererProps } from '@/types/json-ui'
|
||||
import { getUIComponent } from './component-registry'
|
||||
import { transformData } from './utils'
|
||||
import { resolveDataBinding, evaluateCondition } from './utils'
|
||||
|
||||
function resolveBinding(binding: Binding, data: Record<string, unknown>): unknown {
|
||||
const sourceValue = binding.source.includes('.')
|
||||
? getNestedValue(data, binding.source)
|
||||
: data[binding.source]
|
||||
let value: unknown = sourceValue
|
||||
|
||||
if (binding.path) {
|
||||
value = getNestedValue(value, binding.path)
|
||||
}
|
||||
|
||||
if (binding.transform) {
|
||||
value = typeof binding.transform === 'string'
|
||||
? transformData(value, binding.transform)
|
||||
: binding.transform(value)
|
||||
}
|
||||
|
||||
return value
|
||||
return resolveDataBinding(binding, data)
|
||||
}
|
||||
|
||||
export function ComponentRenderer({ component, data, context = {}, onEvent }: ComponentRendererProps) {
|
||||
@@ -32,16 +17,38 @@ export function ComponentRenderer({ component, data, context = {}, onEvent }: Co
|
||||
resolved[propName] = resolveBinding(binding, mergedData)
|
||||
})
|
||||
}
|
||||
|
||||
if (component.dataBinding) {
|
||||
const boundData = resolveDataBinding(component.dataBinding, mergedData)
|
||||
if (boundData !== undefined) {
|
||||
resolved.value = boundData
|
||||
resolved.data = boundData
|
||||
}
|
||||
}
|
||||
|
||||
if (component.events && onEvent) {
|
||||
component.events.forEach(handler => {
|
||||
resolved[`on${handler.event.charAt(0).toUpperCase()}${handler.event.slice(1)}`] = (e: unknown) => {
|
||||
if (!handler.condition || handler.condition(mergedData as Record<string, any>)) {
|
||||
onEvent(component.id, handler.event, e)
|
||||
const conditionMet = !handler.condition
|
||||
|| (typeof handler.condition === 'function'
|
||||
? handler.condition(mergedData as Record<string, any>)
|
||||
: evaluateCondition(handler.condition, mergedData as Record<string, any>))
|
||||
if (conditionMet) {
|
||||
onEvent(component.id, handler, e)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
if (component.className) {
|
||||
resolved.className = resolved.className
|
||||
? `${resolved.className} ${component.className}`
|
||||
: component.className
|
||||
}
|
||||
|
||||
if (component.style) {
|
||||
resolved.style = { ...(resolved.style as Record<string, unknown>), ...component.style }
|
||||
}
|
||||
|
||||
return resolved
|
||||
}, [component, mergedData, onEvent])
|
||||
@@ -53,22 +60,128 @@ export function ComponentRenderer({ component, data, context = {}, onEvent }: Co
|
||||
return null
|
||||
}
|
||||
|
||||
const renderChildren = (
|
||||
children: UIComponent[] | string | undefined,
|
||||
renderContext: Record<string, unknown>
|
||||
) => {
|
||||
if (!children) return null
|
||||
if (typeof children === 'string') {
|
||||
return children
|
||||
}
|
||||
|
||||
return children.map((child, index) => (
|
||||
<Fragment key={typeof child === 'string' ? `text-${index}` : child.id || index}>
|
||||
{typeof child === 'string'
|
||||
? child
|
||||
: (
|
||||
<ComponentRenderer
|
||||
component={child}
|
||||
data={data}
|
||||
context={renderContext}
|
||||
onEvent={onEvent}
|
||||
/>
|
||||
)}
|
||||
</Fragment>
|
||||
))
|
||||
}
|
||||
|
||||
const renderBranch = (
|
||||
branch: UIComponent | (UIComponent | string)[] | string | undefined,
|
||||
renderContext: Record<string, unknown>
|
||||
) => {
|
||||
if (branch === undefined) return null
|
||||
if (typeof branch === 'string') {
|
||||
return branch
|
||||
}
|
||||
if (Array.isArray(branch)) {
|
||||
return branch.map((child, index) => (
|
||||
<Fragment key={typeof child === 'string' ? `text-${index}` : child.id || index}>
|
||||
{typeof child === 'string'
|
||||
? child
|
||||
: (
|
||||
<ComponentRenderer
|
||||
component={child}
|
||||
data={data}
|
||||
context={renderContext}
|
||||
onEvent={onEvent}
|
||||
/>
|
||||
)}
|
||||
</Fragment>
|
||||
))
|
||||
}
|
||||
return (
|
||||
<ComponentRenderer
|
||||
component={branch}
|
||||
data={data}
|
||||
context={renderContext}
|
||||
onEvent={onEvent}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
const renderConditionalContent = (renderContext: Record<string, unknown>) => {
|
||||
if (!component.conditional) return undefined
|
||||
const conditionMet = evaluateCondition(component.conditional.if, { ...data, ...renderContext } as Record<string, any>)
|
||||
if (conditionMet) {
|
||||
if (component.conditional.then !== undefined) {
|
||||
return renderBranch(component.conditional.then as UIComponent | (UIComponent | string)[] | string, renderContext)
|
||||
}
|
||||
return undefined
|
||||
}
|
||||
if (component.conditional.else !== undefined) {
|
||||
return renderBranch(component.conditional.else as UIComponent | (UIComponent | string)[] | string, renderContext)
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
if (component.loop) {
|
||||
const items = resolveDataBinding(component.loop.source, mergedData) || []
|
||||
const loopChildren = items.map((item: unknown, index: number) => {
|
||||
const loopContext = {
|
||||
...context,
|
||||
[component.loop!.itemVar]: item,
|
||||
...(component.loop!.indexVar ? { [component.loop!.indexVar]: index } : {}),
|
||||
}
|
||||
|
||||
if (component.conditional) {
|
||||
const conditionalContent = renderConditionalContent(loopContext)
|
||||
if (conditionalContent !== undefined) {
|
||||
return (
|
||||
<Fragment key={`${component.id}-${index}`}>{conditionalContent}</Fragment>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
if (component.condition) {
|
||||
const conditionValue = resolveBinding(component.condition, { ...data, ...loopContext })
|
||||
if (!conditionValue) {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<Fragment key={`${component.id}-${index}`}>
|
||||
{renderChildren(component.children, loopContext)}
|
||||
</Fragment>
|
||||
)
|
||||
})
|
||||
|
||||
return createElement(Component, resolvedProps, loopChildren)
|
||||
}
|
||||
|
||||
if (component.conditional) {
|
||||
const conditionalContent = renderConditionalContent(mergedData)
|
||||
if (conditionalContent !== undefined) {
|
||||
return conditionalContent
|
||||
}
|
||||
}
|
||||
|
||||
if (component.condition) {
|
||||
const conditionValue = resolveBinding(component.condition, mergedData)
|
||||
if (!conditionValue) {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
const children = component.children?.map((child, index) => (
|
||||
<ComponentRenderer
|
||||
key={child.id || index}
|
||||
component={child}
|
||||
data={data}
|
||||
context={context}
|
||||
onEvent={onEvent}
|
||||
/>
|
||||
))
|
||||
|
||||
return createElement(Component, resolvedProps, children)
|
||||
|
||||
return createElement(Component, resolvedProps, renderChildren(component.children, context))
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import React, { useCallback } from 'react'
|
||||
import type { Action, EventHandler, JSONFormRendererProps, JSONUIRendererProps, UIComponent } from './types'
|
||||
import { getUIComponent } from './component-registry'
|
||||
import { resolveDataBinding, evaluateCondition, mergeClassNames } from './utils'
|
||||
import { resolveDataBinding, evaluateCondition } from './utils'
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
export function JSONUIRenderer({
|
||||
@@ -15,146 +15,58 @@ export function JSONUIRenderer({
|
||||
renderContext: Record<string, unknown>
|
||||
) => {
|
||||
if (!children) return null
|
||||
|
||||
|
||||
if (typeof children === 'string') {
|
||||
return children
|
||||
}
|
||||
|
||||
|
||||
return children.map((child, index) => (
|
||||
<React.Fragment key={child.id || `child-${index}`}>
|
||||
{renderNode(child, renderContext)}
|
||||
</React.Fragment>
|
||||
))
|
||||
}
|
||||
|
||||
const renderNode = (
|
||||
node: UIComponent | string,
|
||||
renderContext: Record<string, unknown>
|
||||
) => {
|
||||
if (typeof node === 'string') {
|
||||
return node
|
||||
}
|
||||
|
||||
return (
|
||||
<JSONUIRenderer
|
||||
key={child.id || `child-${index}`}
|
||||
component={child}
|
||||
component={node}
|
||||
dataMap={dataMap}
|
||||
onAction={onAction}
|
||||
context={renderContext}
|
||||
/>
|
||||
))
|
||||
)
|
||||
}
|
||||
|
||||
const renderConditionalBranch = (
|
||||
branch: UIComponent | UIComponent[] | string | undefined,
|
||||
const renderBranch = (
|
||||
branch: UIComponent | (UIComponent | string)[] | string | undefined,
|
||||
renderContext: Record<string, unknown>
|
||||
) => {
|
||||
if (branch === undefined) return null
|
||||
|
||||
if (typeof branch === 'string' || Array.isArray(branch)) {
|
||||
return renderChildren(branch, renderContext)
|
||||
if (Array.isArray(branch)) {
|
||||
return branch.map((item, index) => (
|
||||
<React.Fragment key={typeof item === 'string' ? `text-${index}` : item.id || `branch-${index}`}>
|
||||
{renderNode(item, renderContext)}
|
||||
</React.Fragment>
|
||||
))
|
||||
}
|
||||
|
||||
return (
|
||||
<JSONUIRenderer
|
||||
component={branch as UIComponent}
|
||||
dataMap={dataMap}
|
||||
onAction={onAction}
|
||||
context={renderContext}
|
||||
/>
|
||||
)
|
||||
return renderNode(branch, renderContext)
|
||||
}
|
||||
|
||||
if (component.conditional) {
|
||||
const conditionMet = evaluateCondition(component.conditional.if, { ...dataMap, ...context })
|
||||
if (conditionMet) {
|
||||
if (component.conditional.then !== undefined) {
|
||||
return renderConditionalBranch(
|
||||
component.conditional.then as UIComponent | UIComponent[] | string,
|
||||
context
|
||||
)
|
||||
}
|
||||
} else {
|
||||
if (component.conditional.else !== undefined) {
|
||||
return renderConditionalBranch(
|
||||
component.conditional.else as UIComponent | UIComponent[] | string,
|
||||
context
|
||||
)
|
||||
}
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
if (component.loop) {
|
||||
const items = resolveDataBinding(component.loop.source, dataMap, context) || []
|
||||
const Component = getUIComponent(component.type)
|
||||
|
||||
if (!Component) {
|
||||
console.warn(`Component type "${component.type}" not found in registry`)
|
||||
return null
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
{items.map((item: any, index: number) => {
|
||||
const loopContext = {
|
||||
...context,
|
||||
[component.loop!.itemVar]: item,
|
||||
...(component.loop!.indexVar ? { [component.loop!.indexVar]: index } : {}),
|
||||
}
|
||||
const props: Record<string, any> = { ...component.props }
|
||||
|
||||
if (component.dataBinding) {
|
||||
const boundData = resolveDataBinding(component.dataBinding, dataMap, loopContext)
|
||||
if (boundData !== undefined) {
|
||||
props.value = boundData
|
||||
props.data = boundData
|
||||
}
|
||||
}
|
||||
|
||||
if (component.events) {
|
||||
Object.entries(component.events).forEach(([eventName, handler]) => {
|
||||
props[eventName] = (event?: any) => {
|
||||
if (onAction) {
|
||||
const eventHandler = typeof handler === 'string'
|
||||
? { action: handler } as EventHandler
|
||||
: handler as EventHandler
|
||||
onAction(eventHandler, event)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
if (component.className) {
|
||||
props.className = cn(props.className, component.className)
|
||||
}
|
||||
|
||||
if (component.style) {
|
||||
props.style = { ...props.style, ...component.style }
|
||||
}
|
||||
return (
|
||||
<React.Fragment key={`${component.id}-${index}`}>
|
||||
{typeof Component === 'string'
|
||||
? React.createElement(Component, props, renderChildren(component.children, loopContext))
|
||||
: (
|
||||
<Component {...props}>
|
||||
{renderChildren(component.children, loopContext)}
|
||||
</Component>
|
||||
)}
|
||||
</React.Fragment>
|
||||
)
|
||||
})}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
const Component = getUIComponent(component.type)
|
||||
|
||||
if (!Component) {
|
||||
console.warn(`Component type "${component.type}" not found in registry`)
|
||||
return null
|
||||
}
|
||||
|
||||
const props: Record<string, any> = { ...component.props }
|
||||
|
||||
if (component.bindings) {
|
||||
Object.entries(component.bindings).forEach(([propName, binding]) => {
|
||||
props[propName] = resolveDataBinding(binding, dataMap, context)
|
||||
})
|
||||
}
|
||||
|
||||
if (component.dataBinding) {
|
||||
const boundData = resolveDataBinding(component.dataBinding, dataMap, context)
|
||||
if (boundData !== undefined) {
|
||||
props.value = boundData
|
||||
props.data = boundData
|
||||
}
|
||||
const renderConditionalBranch = (
|
||||
branch: UIComponent | (UIComponent | string)[] | string | undefined,
|
||||
renderContext: Record<string, unknown>
|
||||
) => {
|
||||
return renderBranch(branch, renderContext)
|
||||
}
|
||||
|
||||
const normalizeEventName = (eventName: string) =>
|
||||
@@ -202,44 +114,165 @@ export function JSONUIRenderer({
|
||||
return null
|
||||
}
|
||||
|
||||
const eventHandlers: EventHandler[] = Array.isArray(component.events)
|
||||
? component.events
|
||||
: component.events
|
||||
? Object.entries(component.events).map(([eventName, handler]) =>
|
||||
normalizeLegacyHandler(eventName, handler)
|
||||
).filter(Boolean) as EventHandler[]
|
||||
: []
|
||||
const applyEventHandlers = (
|
||||
props: Record<string, any>,
|
||||
renderContext: Record<string, unknown>
|
||||
) => {
|
||||
const eventHandlers: EventHandler[] = Array.isArray(component.events)
|
||||
? component.events
|
||||
: component.events
|
||||
? Object.entries(component.events).map(([eventName, handler]) =>
|
||||
normalizeLegacyHandler(eventName, handler)
|
||||
).filter(Boolean) as EventHandler[]
|
||||
: []
|
||||
|
||||
if (eventHandlers.length > 0) {
|
||||
eventHandlers.forEach((handler) => {
|
||||
const propName = getEventPropName(handler.event)
|
||||
props[propName] = (event?: any) => {
|
||||
if (handler.condition && typeof handler.condition === 'function') {
|
||||
const conditionMet = handler.condition({ ...dataMap, ...context })
|
||||
if (!conditionMet) return
|
||||
if (eventHandlers.length > 0) {
|
||||
eventHandlers.forEach((handler) => {
|
||||
const propName = getEventPropName(handler.event)
|
||||
props[propName] = (event?: any) => {
|
||||
if (handler.condition) {
|
||||
const conditionMet = typeof handler.condition === 'function'
|
||||
? handler.condition({ ...dataMap, ...renderContext })
|
||||
: evaluateCondition(handler.condition, { ...dataMap, ...renderContext })
|
||||
if (!conditionMet) return
|
||||
}
|
||||
onAction?.(handler.actions, event)
|
||||
}
|
||||
onAction?.(handler.actions, event)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const resolveProps = (renderContext: Record<string, unknown>) => {
|
||||
const props: Record<string, any> = { ...component.props }
|
||||
|
||||
if (component.bindings) {
|
||||
Object.entries(component.bindings).forEach(([propName, binding]) => {
|
||||
props[propName] = resolveDataBinding(binding, dataMap, renderContext)
|
||||
})
|
||||
}
|
||||
|
||||
if (component.dataBinding) {
|
||||
const boundData = resolveDataBinding(component.dataBinding, dataMap, renderContext)
|
||||
if (boundData !== undefined) {
|
||||
props.value = boundData
|
||||
props.data = boundData
|
||||
}
|
||||
}
|
||||
|
||||
if (component.className) {
|
||||
props.className = cn(props.className, component.className)
|
||||
}
|
||||
|
||||
if (component.style) {
|
||||
props.style = { ...props.style, ...component.style }
|
||||
}
|
||||
|
||||
return props
|
||||
}
|
||||
|
||||
const renderWithContext = (renderContext: Record<string, unknown>) => {
|
||||
if (component.conditional) {
|
||||
const conditionMet = evaluateCondition(component.conditional.if, { ...dataMap, ...renderContext })
|
||||
if (conditionMet) {
|
||||
if (component.conditional.then !== undefined) {
|
||||
return renderConditionalBranch(
|
||||
component.conditional.then as UIComponent | (UIComponent | string)[] | string,
|
||||
renderContext
|
||||
)
|
||||
}
|
||||
} else {
|
||||
if (component.conditional.else !== undefined) {
|
||||
return renderConditionalBranch(
|
||||
component.conditional.else as UIComponent | (UIComponent | string)[] | string,
|
||||
renderContext
|
||||
)
|
||||
}
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
const Component = getUIComponent(component.type)
|
||||
|
||||
if (!Component) {
|
||||
console.warn(`Component type "${component.type}" not found in registry`)
|
||||
return null
|
||||
}
|
||||
|
||||
const props = resolveProps(renderContext)
|
||||
applyEventHandlers(props, renderContext)
|
||||
|
||||
if (typeof Component === 'string') {
|
||||
return React.createElement(Component, props, renderChildren(component.children, renderContext))
|
||||
}
|
||||
|
||||
return (
|
||||
<Component {...props}>
|
||||
{renderChildren(component.children, renderContext)}
|
||||
</Component>
|
||||
)
|
||||
}
|
||||
|
||||
if (component.loop) {
|
||||
const items = resolveDataBinding(component.loop.source, dataMap, context) || []
|
||||
const Component = getUIComponent(component.type)
|
||||
|
||||
if (!Component) {
|
||||
console.warn(`Component type "${component.type}" not found in registry`)
|
||||
return null
|
||||
}
|
||||
|
||||
const containerProps = resolveProps(context)
|
||||
applyEventHandlers(containerProps, context)
|
||||
|
||||
const loopChildren = items.map((item: any, index: number) => {
|
||||
const loopContext = {
|
||||
...context,
|
||||
[component.loop!.itemVar]: item,
|
||||
...(component.loop!.indexVar ? { [component.loop!.indexVar]: index } : {}),
|
||||
}
|
||||
|
||||
let content = renderChildren(component.children, loopContext)
|
||||
|
||||
if (component.conditional) {
|
||||
const conditionMet = evaluateCondition(component.conditional.if, { ...dataMap, ...loopContext })
|
||||
if (conditionMet) {
|
||||
if (component.conditional.then !== undefined) {
|
||||
content = renderConditionalBranch(
|
||||
component.conditional.then as UIComponent | (UIComponent | string)[] | string,
|
||||
loopContext
|
||||
)
|
||||
}
|
||||
} else {
|
||||
if (component.conditional.else !== undefined) {
|
||||
content = renderConditionalBranch(
|
||||
component.conditional.else as UIComponent | (UIComponent | string)[] | string,
|
||||
loopContext
|
||||
)
|
||||
} else {
|
||||
content = null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<React.Fragment key={`${component.id}-${index}`}>
|
||||
{content}
|
||||
</React.Fragment>
|
||||
)
|
||||
})
|
||||
|
||||
if (typeof Component === 'string') {
|
||||
return React.createElement(Component, containerProps, loopChildren)
|
||||
}
|
||||
|
||||
return (
|
||||
<Component {...containerProps}>
|
||||
{loopChildren}
|
||||
</Component>
|
||||
)
|
||||
}
|
||||
|
||||
if (component.className) {
|
||||
props.className = cn(props.className, component.className)
|
||||
}
|
||||
|
||||
if (component.style) {
|
||||
props.style = { ...props.style, ...component.style }
|
||||
}
|
||||
|
||||
if (typeof Component === 'string') {
|
||||
return React.createElement(Component, props, renderChildren(component.children, context))
|
||||
}
|
||||
|
||||
return (
|
||||
<Component {...props}>
|
||||
{renderChildren(component.children, context)}
|
||||
</Component>
|
||||
)
|
||||
return renderWithContext(context)
|
||||
}
|
||||
|
||||
export function JSONFormRenderer({ formData, fields, onSubmit, onChange }: JSONFormRendererProps) {
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
type BindingTransform = string | ((data: unknown) => unknown)
|
||||
|
||||
export function resolveDataBinding(
|
||||
binding: string | { source: string; path?: string; transform?: string },
|
||||
binding: string | { source: string; path?: string; transform?: BindingTransform },
|
||||
dataMap: Record<string, any>,
|
||||
context: Record<string, any> = {},
|
||||
): any {
|
||||
@@ -13,10 +15,24 @@ export function resolveDataBinding(
|
||||
}
|
||||
|
||||
const { source, path, transform } = binding
|
||||
const data = mergedContext[source]
|
||||
const resolvedValue = path ? getNestedValue(data, path) : data
|
||||
const sourceValue = source.includes('.')
|
||||
? getNestedValue(mergedContext, source)
|
||||
: mergedContext[source]
|
||||
const resolvedValue = path ? getNestedValue(sourceValue, path) : sourceValue
|
||||
|
||||
return transform ? transformData(resolvedValue, transform) : resolvedValue
|
||||
return applyTransform(resolvedValue, transform)
|
||||
}
|
||||
|
||||
function applyTransform(value: unknown, transform?: BindingTransform) {
|
||||
if (!transform) {
|
||||
return value
|
||||
}
|
||||
|
||||
if (typeof transform === 'function') {
|
||||
return transform(value)
|
||||
}
|
||||
|
||||
return transformData(value, transform)
|
||||
}
|
||||
|
||||
export function getNestedValue(obj: any, path: string): any {
|
||||
|
||||
@@ -83,11 +83,11 @@ function checkComponentTree(
|
||||
seenIds.add(component.id)
|
||||
}
|
||||
|
||||
if (component.dataBinding && !component.dataBinding.source) {
|
||||
if (component.dataBinding) {
|
||||
const bindingPath = typeof component.dataBinding === 'string'
|
||||
? component.dataBinding.split('.')[0]
|
||||
: ''
|
||||
|
||||
: component.dataBinding.source?.split('.')[0]
|
||||
|
||||
if (bindingPath) {
|
||||
result.warnings.push({
|
||||
path: `${path}.${component.id}`,
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
import { ReactNode } from 'react'
|
||||
|
||||
export type ComponentType =
|
||||
| 'div' | 'section' | 'article' | 'header' | 'footer' | 'main'
|
||||
| 'Button' | 'Card' | 'CardHeader' | 'CardTitle' | 'CardDescription' | 'CardContent' | 'CardFooter'
|
||||
@@ -37,7 +35,7 @@ export interface Action {
|
||||
value?: any
|
||||
params?: Record<string, any>
|
||||
// Legacy: function-based compute
|
||||
compute?: (data: Record<string, any>, event?: any) => any
|
||||
compute?: ((data: Record<string, any>, event?: any) => any) | string
|
||||
// New: JSON-friendly expression (e.g., "event.target.value", "data.fieldName")
|
||||
expression?: string
|
||||
// New: JSON template with dynamic values
|
||||
@@ -55,17 +53,34 @@ export interface Binding {
|
||||
export interface EventHandler {
|
||||
event: string
|
||||
actions: Action[]
|
||||
condition?: (data: Record<string, any>) => boolean
|
||||
condition?: string | ((data: Record<string, any>) => boolean)
|
||||
}
|
||||
|
||||
export interface Conditional {
|
||||
if: string
|
||||
then?: UIComponent | (UIComponent | string)[] | string
|
||||
else?: UIComponent | (UIComponent | string)[] | string
|
||||
}
|
||||
|
||||
export interface Loop {
|
||||
source: string
|
||||
itemVar: string
|
||||
indexVar?: string
|
||||
}
|
||||
|
||||
export interface UIComponent {
|
||||
id: string
|
||||
type: ComponentType
|
||||
props?: Record<string, any>
|
||||
className?: string
|
||||
style?: Record<string, any>
|
||||
bindings?: Record<string, Binding>
|
||||
dataBinding?: string | Binding
|
||||
events?: EventHandler[]
|
||||
children?: UIComponent[]
|
||||
children?: UIComponent[] | string
|
||||
condition?: Binding
|
||||
conditional?: Conditional
|
||||
loop?: Loop
|
||||
}
|
||||
|
||||
export interface Layout {
|
||||
@@ -100,7 +115,7 @@ export interface ComponentRendererProps {
|
||||
component: UIComponent
|
||||
data: Record<string, unknown>
|
||||
context?: Record<string, unknown>
|
||||
onEvent?: (componentId: string, event: string, eventData: unknown) => void
|
||||
onEvent?: (componentId: string, handler: EventHandler, eventData: unknown) => void
|
||||
}
|
||||
|
||||
export type ComponentSchema = UIComponent
|
||||
|
||||
Reference in New Issue
Block a user