mirror of
https://github.com/johndoe6345789/low-code-react-app-b.git
synced 2026-04-24 13:44:54 +00:00
Merge pull request #111 from johndoe6345789/codex/extend-json-schema-types-for-bindings
Add state-aware bindings for JSON renderer
This commit is contained in:
@@ -3,11 +3,16 @@ import { UIComponent, Binding, ComponentRendererProps, EventHandler, JSONEventDe
|
||||
import { getUIComponent } from './component-registry'
|
||||
import { resolveDataBinding, evaluateCondition } from './utils'
|
||||
|
||||
function resolveBinding(binding: Binding, data: Record<string, unknown>): unknown {
|
||||
return resolveDataBinding(binding, data)
|
||||
function resolveBinding(
|
||||
binding: Binding,
|
||||
data: Record<string, unknown>,
|
||||
context: Record<string, unknown>,
|
||||
state?: Record<string, unknown>
|
||||
): unknown {
|
||||
return resolveDataBinding(binding, data, context, { state, bindings: context })
|
||||
}
|
||||
|
||||
export function ComponentRenderer({ component, data, context = {}, onEvent }: ComponentRendererProps) {
|
||||
export function ComponentRenderer({ component, data, context = {}, state, onEvent }: ComponentRendererProps) {
|
||||
const mergedData = useMemo(() => ({ ...data, ...context }), [data, context])
|
||||
const resolvedEventHandlers = useMemo(() => {
|
||||
const normalizeEventName = (eventName: string) =>
|
||||
@@ -77,12 +82,12 @@ export function ComponentRenderer({ component, data, context = {}, onEvent }: Co
|
||||
|
||||
if (component.bindings) {
|
||||
Object.entries(component.bindings).forEach(([propName, binding]) => {
|
||||
resolved[propName] = resolveBinding(binding, mergedData)
|
||||
resolved[propName] = resolveBinding(binding, data, context, state)
|
||||
})
|
||||
}
|
||||
|
||||
if (component.dataBinding) {
|
||||
const boundData = resolveDataBinding(component.dataBinding, mergedData)
|
||||
const boundData = resolveDataBinding(component.dataBinding, data, context, { state, bindings: context })
|
||||
if (boundData !== undefined) {
|
||||
resolved.value = boundData
|
||||
resolved.data = boundData
|
||||
@@ -114,7 +119,7 @@ export function ComponentRenderer({ component, data, context = {}, onEvent }: Co
|
||||
}
|
||||
|
||||
return resolved
|
||||
}, [component, mergedData, onEvent])
|
||||
}, [component, data, context, state, mergedData, onEvent])
|
||||
|
||||
const Component = getUIComponent(component.type)
|
||||
|
||||
@@ -141,6 +146,7 @@ export function ComponentRenderer({ component, data, context = {}, onEvent }: Co
|
||||
component={child}
|
||||
data={data}
|
||||
context={renderContext}
|
||||
state={state}
|
||||
onEvent={onEvent}
|
||||
/>
|
||||
)}
|
||||
@@ -166,6 +172,7 @@ export function ComponentRenderer({ component, data, context = {}, onEvent }: Co
|
||||
component={child}
|
||||
data={data}
|
||||
context={renderContext}
|
||||
state={state}
|
||||
onEvent={onEvent}
|
||||
/>
|
||||
)}
|
||||
@@ -177,6 +184,7 @@ export function ComponentRenderer({ component, data, context = {}, onEvent }: Co
|
||||
component={branch}
|
||||
data={data}
|
||||
context={renderContext}
|
||||
state={state}
|
||||
onEvent={onEvent}
|
||||
/>
|
||||
)
|
||||
@@ -198,7 +206,7 @@ export function ComponentRenderer({ component, data, context = {}, onEvent }: Co
|
||||
}
|
||||
|
||||
if (component.loop) {
|
||||
const items = resolveDataBinding(component.loop.source, mergedData) || []
|
||||
const items = resolveDataBinding(component.loop.source, data, context, { state, bindings: context }) || []
|
||||
const loopChildren = items.map((item: unknown, index: number) => {
|
||||
const loopContext = {
|
||||
...context,
|
||||
@@ -216,7 +224,7 @@ export function ComponentRenderer({ component, data, context = {}, onEvent }: Co
|
||||
}
|
||||
|
||||
if (component.condition) {
|
||||
const conditionValue = resolveBinding(component.condition, { ...data, ...loopContext })
|
||||
const conditionValue = resolveBinding(component.condition, data, loopContext, state)
|
||||
if (!conditionValue) {
|
||||
return null
|
||||
}
|
||||
@@ -240,7 +248,7 @@ export function ComponentRenderer({ component, data, context = {}, onEvent }: Co
|
||||
}
|
||||
|
||||
if (component.condition) {
|
||||
const conditionValue = resolveBinding(component.condition, mergedData)
|
||||
const conditionValue = resolveBinding(component.condition, data, context, state)
|
||||
if (!conditionValue) {
|
||||
return null
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ import { useCallback } from 'react'
|
||||
import { PageSchema } from '@/types/json-ui'
|
||||
import { useDataSources } from '@/hooks/data/use-data-sources'
|
||||
import { useActionExecutor } from '@/hooks/ui/use-action-executor'
|
||||
import { useAppSelector } from '@/store'
|
||||
import { ComponentRenderer } from './component-renderer'
|
||||
|
||||
interface PageRendererProps {
|
||||
@@ -11,6 +12,7 @@ interface PageRendererProps {
|
||||
|
||||
export function PageRenderer({ schema, onCustomAction }: PageRendererProps) {
|
||||
const { data, updateData, updatePath } = useDataSources(schema.dataSources)
|
||||
const state = useAppSelector((rootState) => rootState)
|
||||
|
||||
const context = {
|
||||
data,
|
||||
@@ -32,6 +34,7 @@ export function PageRenderer({ schema, onCustomAction }: PageRendererProps) {
|
||||
key={component.id || index}
|
||||
component={component}
|
||||
data={data}
|
||||
state={state}
|
||||
onEvent={handleEvent}
|
||||
/>
|
||||
))}
|
||||
|
||||
@@ -1,23 +1,43 @@
|
||||
type BindingTransform = string | ((data: unknown) => unknown)
|
||||
|
||||
interface BindingSourceOptions {
|
||||
state?: Record<string, any>
|
||||
bindings?: Record<string, any>
|
||||
}
|
||||
|
||||
export function resolveDataBinding(
|
||||
binding: string | { source: string; path?: string; transform?: BindingTransform },
|
||||
binding: string | { source: string; sourceType?: 'data' | 'bindings' | 'state'; path?: string; transform?: BindingTransform },
|
||||
dataMap: Record<string, any>,
|
||||
context: Record<string, any> = {},
|
||||
options: BindingSourceOptions = {},
|
||||
): any {
|
||||
const mergedContext = { ...dataMap, ...context }
|
||||
const stateSource = options.state ?? {}
|
||||
const bindingsSource = options.bindings ?? context
|
||||
|
||||
if (typeof binding === 'string') {
|
||||
if (binding.startsWith('state.')) {
|
||||
return getNestedValue(stateSource, binding.slice('state.'.length))
|
||||
}
|
||||
if (binding.startsWith('bindings.')) {
|
||||
return getNestedValue(bindingsSource, binding.slice('bindings.'.length))
|
||||
}
|
||||
if (binding.includes('.')) {
|
||||
return getNestedValue(mergedContext, binding)
|
||||
}
|
||||
return mergedContext[binding]
|
||||
}
|
||||
|
||||
const { source, path, transform } = binding
|
||||
const { source, sourceType, path, transform } = binding
|
||||
const sourceContext =
|
||||
sourceType === 'state'
|
||||
? stateSource
|
||||
: sourceType === 'bindings'
|
||||
? bindingsSource
|
||||
: mergedContext
|
||||
const sourceValue = source.includes('.')
|
||||
? getNestedValue(mergedContext, source)
|
||||
: mergedContext[source]
|
||||
? getNestedValue(sourceContext, source)
|
||||
: sourceContext[source]
|
||||
const resolvedValue = path ? getNestedValue(sourceValue, path) : sourceValue
|
||||
|
||||
return applyTransform(resolvedValue, transform)
|
||||
|
||||
75
src/schemas/page-schemas.ts
Normal file
75
src/schemas/page-schemas.ts
Normal file
@@ -0,0 +1,75 @@
|
||||
import type { PageSchema } from '@/types/json-ui'
|
||||
|
||||
export const stateBindingsDemoSchema: PageSchema = {
|
||||
id: 'state-bindings-demo',
|
||||
name: 'State & Bindings Demo',
|
||||
layout: {
|
||||
type: 'single',
|
||||
},
|
||||
dataSources: [
|
||||
{
|
||||
id: 'statusItems',
|
||||
type: 'static',
|
||||
defaultValue: ['KV Ready', 'Components Loaded', 'Sync Enabled'],
|
||||
},
|
||||
],
|
||||
components: [
|
||||
{
|
||||
id: 'state-demo-root',
|
||||
type: 'div',
|
||||
props: {
|
||||
className: 'space-y-4 rounded-lg border border-border bg-card p-6',
|
||||
},
|
||||
children: [
|
||||
{
|
||||
id: 'state-demo-title',
|
||||
type: 'Heading',
|
||||
props: {
|
||||
className: 'text-xl font-semibold',
|
||||
children: 'Renderer State Binding Demo',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'state-demo-theme',
|
||||
type: 'Text',
|
||||
props: {
|
||||
className: 'text-sm text-muted-foreground',
|
||||
},
|
||||
bindings: {
|
||||
children: {
|
||||
sourceType: 'state',
|
||||
source: 'settings',
|
||||
path: 'settings.theme',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'state-demo-list',
|
||||
type: 'div',
|
||||
props: {
|
||||
className: 'space-y-2',
|
||||
},
|
||||
loop: {
|
||||
source: 'statusItems',
|
||||
itemVar: 'statusItem',
|
||||
},
|
||||
children: [
|
||||
{
|
||||
id: 'state-demo-list-item',
|
||||
type: 'Text',
|
||||
props: {
|
||||
className: 'text-sm',
|
||||
},
|
||||
bindings: {
|
||||
children: {
|
||||
sourceType: 'bindings',
|
||||
source: 'statusItem',
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
}
|
||||
@@ -18,6 +18,9 @@ export type ActionType =
|
||||
export type DataSourceType =
|
||||
| 'kv' | 'computed' | 'static'
|
||||
|
||||
export type BindingSourceType =
|
||||
| 'data' | 'bindings' | 'state'
|
||||
|
||||
export interface DataSource {
|
||||
id: string
|
||||
type: DataSourceType
|
||||
@@ -46,6 +49,7 @@ export interface Action {
|
||||
|
||||
export interface Binding {
|
||||
source: string
|
||||
sourceType?: BindingSourceType
|
||||
path?: string
|
||||
transform?: string | ((value: any) => any)
|
||||
}
|
||||
@@ -124,6 +128,7 @@ export interface ComponentRendererProps {
|
||||
component: UIComponent
|
||||
data: Record<string, unknown>
|
||||
context?: Record<string, unknown>
|
||||
state?: Record<string, unknown>
|
||||
onEvent?: (componentId: string, handler: EventHandler, eventData: unknown) => void
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user