diff --git a/src/lib/json-ui/component-renderer.tsx b/src/lib/json-ui/component-renderer.tsx index d56b23d..672539c 100644 --- a/src/lib/json-ui/component-renderer.tsx +++ b/src/lib/json-ui/component-renderer.tsx @@ -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): unknown { - return resolveDataBinding(binding, data) +function resolveBinding( + binding: Binding, + data: Record, + context: Record, + state?: Record +): 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 } diff --git a/src/lib/json-ui/page-renderer.tsx b/src/lib/json-ui/page-renderer.tsx index 17b613c..41e4c86 100644 --- a/src/lib/json-ui/page-renderer.tsx +++ b/src/lib/json-ui/page-renderer.tsx @@ -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} /> ))} diff --git a/src/lib/json-ui/utils.ts b/src/lib/json-ui/utils.ts index 2ea68f8..8113ce2 100644 --- a/src/lib/json-ui/utils.ts +++ b/src/lib/json-ui/utils.ts @@ -1,23 +1,43 @@ type BindingTransform = string | ((data: unknown) => unknown) +interface BindingSourceOptions { + state?: Record + bindings?: Record +} + 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, context: Record = {}, + 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) diff --git a/src/schemas/page-schemas.ts b/src/schemas/page-schemas.ts new file mode 100644 index 0000000..46ceb50 --- /dev/null +++ b/src/schemas/page-schemas.ts @@ -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', + }, + }, + }, + ], + }, + ], + }, + ], +} diff --git a/src/types/json-ui.ts b/src/types/json-ui.ts index 8f0ec72..f3ce4c3 100644 --- a/src/types/json-ui.ts +++ b/src/types/json-ui.ts @@ -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 context?: Record + state?: Record onEvent?: (componentId: string, handler: EventHandler, eventData: unknown) => void }