mirror of
https://github.com/johndoe6345789/low-code-react-app-b.git
synced 2026-04-24 13:44:54 +00:00
Merge branch 'main' into codex/align-event/action-shape-in-renderer
This commit is contained in:
@@ -1,55 +1,50 @@
|
||||
import { createElement, useMemo } from 'react'
|
||||
import { UIComponent, Binding, ComponentRendererProps } from '@/types/json-ui'
|
||||
import { getUIComponent } from './component-registry'
|
||||
import { transformData } from './utils'
|
||||
|
||||
function resolveBinding(binding: Binding, data: Record<string, unknown>): unknown {
|
||||
let value: unknown = data[binding.source]
|
||||
const sourceValue = binding.source.includes('.')
|
||||
? getNestedValue(data, binding.source)
|
||||
: data[binding.source]
|
||||
let value: unknown = sourceValue
|
||||
|
||||
if (binding.path) {
|
||||
const keys = binding.path.split('.')
|
||||
for (const key of keys) {
|
||||
if (value && typeof value === 'object') {
|
||||
value = (value as Record<string, unknown>)[key]
|
||||
} else {
|
||||
value = undefined
|
||||
break
|
||||
}
|
||||
}
|
||||
value = getNestedValue(value, binding.path)
|
||||
}
|
||||
|
||||
if (binding.transform) {
|
||||
value = binding.transform(value)
|
||||
value = typeof binding.transform === 'string'
|
||||
? transformData(value, binding.transform)
|
||||
: binding.transform(value)
|
||||
}
|
||||
|
||||
return value
|
||||
}
|
||||
|
||||
export function ComponentRenderer({ component, data, onEvent }: ComponentRendererProps) {
|
||||
export function ComponentRenderer({ component, data, context = {}, onEvent }: ComponentRendererProps) {
|
||||
const mergedData = useMemo(() => ({ ...data, ...context }), [data, context])
|
||||
const resolvedProps = useMemo(() => {
|
||||
const resolved: Record<string, unknown> = { ...component.props }
|
||||
|
||||
if (component.bindings) {
|
||||
Object.entries(component.bindings).forEach(([propName, binding]) => {
|
||||
resolved[propName] = resolveBinding(binding, data)
|
||||
resolved[propName] = resolveBinding(binding, mergedData)
|
||||
})
|
||||
}
|
||||
|
||||
if (component.events && onEvent) {
|
||||
component.events.forEach(handler => {
|
||||
const eventName = handler.event
|
||||
const propName = eventName.startsWith('on')
|
||||
? eventName
|
||||
: `on${eventName.charAt(0).toUpperCase()}${eventName.slice(1)}`
|
||||
resolved[propName] = (e: unknown) => {
|
||||
if (!handler.condition || handler.condition(data)) {
|
||||
onEvent(component.id, handler, e)
|
||||
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)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
return resolved
|
||||
}, [component, data, onEvent])
|
||||
}, [component, mergedData, onEvent])
|
||||
|
||||
const Component = getUIComponent(component.type)
|
||||
|
||||
@@ -59,7 +54,7 @@ export function ComponentRenderer({ component, data, onEvent }: ComponentRendere
|
||||
}
|
||||
|
||||
if (component.condition) {
|
||||
const conditionValue = resolveBinding(component.condition, data)
|
||||
const conditionValue = resolveBinding(component.condition, mergedData)
|
||||
if (!conditionValue) {
|
||||
return null
|
||||
}
|
||||
@@ -70,6 +65,7 @@ export function ComponentRenderer({ component, data, onEvent }: ComponentRendere
|
||||
key={child.id || index}
|
||||
component={child}
|
||||
data={data}
|
||||
context={context}
|
||||
onEvent={onEvent}
|
||||
/>
|
||||
))
|
||||
|
||||
@@ -10,32 +10,6 @@ export function JSONUIRenderer({
|
||||
onAction,
|
||||
context = {}
|
||||
}: JSONUIRendererProps) {
|
||||
|
||||
if (component.conditional) {
|
||||
const conditionMet = evaluateCondition(component.conditional.if, { ...dataMap, ...context })
|
||||
if (conditionMet) {
|
||||
if (component.conditional.then) {
|
||||
return (
|
||||
<JSONUIRenderer
|
||||
component={component.conditional.then as UIComponent}
|
||||
dataMap={dataMap}
|
||||
onAction={onAction}
|
||||
context={context}
|
||||
/>
|
||||
)
|
||||
}
|
||||
} else {
|
||||
return component.conditional.else ? (
|
||||
<JSONUIRenderer
|
||||
component={component.conditional.else as UIComponent}
|
||||
dataMap={dataMap}
|
||||
onAction={onAction}
|
||||
context={context}
|
||||
/>
|
||||
) : null
|
||||
}
|
||||
}
|
||||
|
||||
const renderChildren = (
|
||||
children: UIComponent[] | string | undefined,
|
||||
renderContext: Record<string, unknown>
|
||||
@@ -57,8 +31,55 @@ export function JSONUIRenderer({
|
||||
))
|
||||
}
|
||||
|
||||
const renderConditionalBranch = (
|
||||
branch: UIComponent | UIComponent[] | string | undefined,
|
||||
renderContext: Record<string, unknown>
|
||||
) => {
|
||||
if (branch === undefined) return null
|
||||
|
||||
if (typeof branch === 'string' || Array.isArray(branch)) {
|
||||
return renderChildren(branch, renderContext)
|
||||
}
|
||||
|
||||
return (
|
||||
<JSONUIRenderer
|
||||
component={branch as UIComponent}
|
||||
dataMap={dataMap}
|
||||
onAction={onAction}
|
||||
context={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) => {
|
||||
@@ -67,9 +88,45 @@ export function JSONUIRenderer({
|
||||
[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}`}>
|
||||
{renderChildren(component.children, loopContext)}
|
||||
{typeof Component === 'string'
|
||||
? React.createElement(Component, props, renderChildren(component.children, loopContext))
|
||||
: (
|
||||
<Component {...props}>
|
||||
{renderChildren(component.children, loopContext)}
|
||||
</Component>
|
||||
)}
|
||||
</React.Fragment>
|
||||
)
|
||||
})}
|
||||
@@ -86,6 +143,12 @@ export function JSONUIRenderer({
|
||||
|
||||
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) {
|
||||
|
||||
@@ -60,6 +60,7 @@ export const UIComponentSchema: any = z.object({
|
||||
props: z.record(z.string(), z.any()).optional(),
|
||||
className: z.string().optional(),
|
||||
style: z.record(z.string(), z.any()).optional(),
|
||||
bindings: z.record(z.string(), DataBindingSchema).optional(),
|
||||
children: z.union([
|
||||
z.string(),
|
||||
z.array(z.lazy(() => UIComponentSchema)),
|
||||
|
||||
@@ -49,7 +49,7 @@ export interface Action {
|
||||
export interface Binding {
|
||||
source: string
|
||||
path?: string
|
||||
transform?: (value: any) => any
|
||||
transform?: string | ((value: any) => any)
|
||||
}
|
||||
|
||||
export interface EventHandler {
|
||||
@@ -99,7 +99,8 @@ export interface JSONUIContext {
|
||||
export interface ComponentRendererProps {
|
||||
component: UIComponent
|
||||
data: Record<string, unknown>
|
||||
onEvent?: (componentId: string, handler: EventHandler, eventData: unknown) => void
|
||||
context?: Record<string, unknown>
|
||||
onEvent?: (componentId: string, event: string, eventData: unknown) => void
|
||||
}
|
||||
|
||||
export type ComponentSchema = UIComponent
|
||||
|
||||
Reference in New Issue
Block a user