From 67f2c26f104b74ef7b4298310387701d3923f0e1 Mon Sep 17 00:00:00 2001 From: johndoe6345789 Date: Sun, 18 Jan 2026 13:35:00 +0000 Subject: [PATCH] Add nested target path updates --- JSON_EXPRESSION_SYSTEM.md | 14 +++++ src/hooks/data/use-data-sources.ts | 15 ++---- src/hooks/ui/use-action-executor.ts | 82 +++++++++++++++++++++++------ src/lib/json-ui/page-renderer.tsx | 1 + src/types/json-ui.ts | 1 + 5 files changed, 84 insertions(+), 29 deletions(-) diff --git a/JSON_EXPRESSION_SYSTEM.md b/JSON_EXPRESSION_SYSTEM.md index 8fd9d9a..40dabfe 100644 --- a/JSON_EXPRESSION_SYSTEM.md +++ b/JSON_EXPRESSION_SYSTEM.md @@ -96,6 +96,20 @@ Update a data source with a new value. } ``` +**Target-path convention:** +To update nested values inside a data source, use a dotted `target` where the prefix is the data source ID and the remainder is the nested path: + +```json +{ + "id": "set-city", + "type": "set-value", + "target": "profile.address.city", + "expression": "event.target.value" +} +``` + +This dotted `target` format works with `set-value`, `update`, `toggle-value`, `increment`, and `decrement`. + ### create Add a new item to an array data source. diff --git a/src/hooks/data/use-data-sources.ts b/src/hooks/data/use-data-sources.ts index 72cdcd5..10bd204 100644 --- a/src/hooks/data/use-data-sources.ts +++ b/src/hooks/data/use-data-sources.ts @@ -1,6 +1,7 @@ import { useState, useCallback, useEffect } from 'react' import { useKV } from '@/hooks/use-kv' import { DataSource } from '@/types/json-ui' +import { setNestedValue } from '@/lib/json-ui/utils' export function useDataSources(dataSources: DataSource[]) { const [data, setData] = useState>({}) @@ -86,18 +87,8 @@ export function useDataSources(dataSources: DataSource[]) { return prev } - const pathParts = path.split('.') - const newData = { ...sourceData } - let current: any = newData - - for (let i = 0; i < pathParts.length - 1; i++) { - if (!(pathParts[i] in current)) { - current[pathParts[i]] = {} - } - current = current[pathParts[i]] - } - - current[pathParts[pathParts.length - 1]] = value + const newData = Array.isArray(sourceData) ? [...sourceData] : { ...sourceData } + setNestedValue(newData, path, value) if (source.type === 'kv') { const kvIndex = kvSources.indexOf(source) diff --git a/src/hooks/ui/use-action-executor.ts b/src/hooks/ui/use-action-executor.ts index 528a5c8..5765ec8 100644 --- a/src/hooks/ui/use-action-executor.ts +++ b/src/hooks/ui/use-action-executor.ts @@ -2,9 +2,17 @@ import { useCallback } from 'react' import { toast } from 'sonner' import { Action, JSONUIContext } from '@/types/json-ui' import { evaluateExpression, evaluateTemplate } from '@/lib/json-ui/expression-evaluator' +import { getNestedValue } from '@/lib/json-ui/utils' export function useActionExecutor(context: JSONUIContext) { - const { data, updateData, executeAction: contextExecute } = context + const { data, updateData, updatePath, executeAction: contextExecute } = context + + const getTargetParts = (target?: string) => { + if (!target) return null + const [sourceId, ...pathParts] = target.split('.') + const path = pathParts.join('.') + return { sourceId, path: path || undefined } + } const executeAction = useCallback(async (action: Action, event?: any) => { try { @@ -35,7 +43,8 @@ export function useActionExecutor(context: JSONUIContext) { } case 'update': { - if (!action.target) return + const targetParts = getTargetParts(action.target) + if (!targetParts) return let newValue if (action.compute) { @@ -47,8 +56,12 @@ export function useActionExecutor(context: JSONUIContext) { } else { newValue = action.value } - - updateData(action.target, newValue) + + if (targetParts.path) { + updatePath(targetParts.sourceId, targetParts.path, newValue) + } else { + updateData(targetParts.sourceId, newValue) + } break } @@ -66,7 +79,8 @@ export function useActionExecutor(context: JSONUIContext) { } case 'set-value': { - if (!action.target) return + const targetParts = getTargetParts(action.target) + if (!targetParts) return let newValue if (action.compute) { @@ -78,31 +92,65 @@ export function useActionExecutor(context: JSONUIContext) { } else { newValue = action.value } - - updateData(action.target, newValue) + + if (targetParts.path) { + updatePath(targetParts.sourceId, targetParts.path, newValue) + } else { + updateData(targetParts.sourceId, newValue) + } break } case 'toggle-value': { - if (!action.target) return - const currentValue = data[action.target] - updateData(action.target, !currentValue) + const targetParts = getTargetParts(action.target) + if (!targetParts) return + + const currentValue = targetParts.path + ? getNestedValue(data[targetParts.sourceId], targetParts.path) + : data[targetParts.sourceId] + const nextValue = !currentValue + + if (targetParts.path) { + updatePath(targetParts.sourceId, targetParts.path, nextValue) + } else { + updateData(targetParts.sourceId, nextValue) + } break } case 'increment': { - if (!action.target) return - const currentValue = data[action.target] || 0 + const targetParts = getTargetParts(action.target) + if (!targetParts) return + + const currentValue = targetParts.path + ? getNestedValue(data[targetParts.sourceId], targetParts.path) + : data[targetParts.sourceId] const amount = action.value || 1 - updateData(action.target, currentValue + amount) + const nextValue = (currentValue || 0) + amount + + if (targetParts.path) { + updatePath(targetParts.sourceId, targetParts.path, nextValue) + } else { + updateData(targetParts.sourceId, nextValue) + } break } case 'decrement': { - if (!action.target) return - const currentValue = data[action.target] || 0 + const targetParts = getTargetParts(action.target) + if (!targetParts) return + + const currentValue = targetParts.path + ? getNestedValue(data[targetParts.sourceId], targetParts.path) + : data[targetParts.sourceId] const amount = action.value || 1 - updateData(action.target, currentValue - amount) + const nextValue = (currentValue || 0) - amount + + if (targetParts.path) { + updatePath(targetParts.sourceId, targetParts.path, nextValue) + } else { + updateData(targetParts.sourceId, nextValue) + } break } @@ -145,7 +193,7 @@ export function useActionExecutor(context: JSONUIContext) { console.error('Action execution failed:', error) toast.error('Action failed') } - }, [data, updateData, contextExecute]) + }, [data, updateData, updatePath, contextExecute]) const executeActions = useCallback(async (actions: Action[], event?: any) => { for (const action of actions) { diff --git a/src/lib/json-ui/page-renderer.tsx b/src/lib/json-ui/page-renderer.tsx index 41e4c86..13d589e 100644 --- a/src/lib/json-ui/page-renderer.tsx +++ b/src/lib/json-ui/page-renderer.tsx @@ -17,6 +17,7 @@ export function PageRenderer({ schema, onCustomAction }: PageRendererProps) { const context = { data, updateData, + updatePath, executeAction: onCustomAction || (async () => {}), } diff --git a/src/types/json-ui.ts b/src/types/json-ui.ts index 7e1cd9b..7ea572f 100644 --- a/src/types/json-ui.ts +++ b/src/types/json-ui.ts @@ -147,6 +147,7 @@ export interface PageSchema { export interface JSONUIContext { data: Record updateData: (sourceId: string, value: any) => void + updatePath: (sourceId: string, path: string, value: any) => void executeAction: (action: Action, event?: any) => Promise }