From 0021cee3386ad00a88641c8fc4e5aacff4681fdf Mon Sep 17 00:00:00 2001 From: karankalsi Date: Fri, 24 Jul 2026 15:12:05 +0530 Subject: [PATCH 1/4] feat(react-native): support custom directives --- packages/react-native/README.md | 23 +++++++++++++++++ packages/react-native/src/renderer.tsx | 35 +++++++++++++++++++++----- skills/react-native/SKILL.md | 10 ++++++++ 3 files changed, 62 insertions(+), 6 deletions(-) diff --git a/packages/react-native/README.md b/packages/react-native/README.md index 3c09c67f..745c1a4e 100644 --- a/packages/react-native/README.md +++ b/packages/react-native/README.md @@ -195,6 +195,29 @@ Any prop value can be a dynamic expression resolved at render time: See [@json-render/core](../core/README.md) for full expression syntax. +### Custom directives + +Register custom directives through `JSONUIProvider` to resolve user-defined `$`-prefixed values in component props: + +```tsx +import { defineDirective, resolvePropValue } from "@json-render/core"; +import { z } from "zod"; + +const uppercase = defineDirective({ + name: "$uppercase", + schema: z.object({ $uppercase: z.unknown() }), + resolve(value, ctx) { + return String(resolvePropValue(value.$uppercase, ctx)).toUpperCase(); + }, +}); + + + +; +``` + +Directives can wrap built-in expressions such as `{ "$uppercase": { "$state": "/message" } }`. See the [directives documentation](https://json-render.dev/docs/directives) for more details. + ## Tab Navigation Pattern Combine `Pressable`, `setState`, visibility conditions, and dynamic props for functional tabs: diff --git a/packages/react-native/src/renderer.tsx b/packages/react-native/src/renderer.tsx index f9601a8b..e1f3b6ca 100644 --- a/packages/react-native/src/renderer.tsx +++ b/packages/react-native/src/renderer.tsx @@ -12,6 +12,8 @@ import type { Catalog, SchemaDefinition, StateStore, + DirectiveDefinition, + DirectiveRegistry, } from "@json-render/core"; import { resolveElementProps, @@ -19,6 +21,7 @@ import { resolveActionParam, evaluateVisibility, getByPath, + createDirectiveRegistry, type PropResolutionContext, type VisibilityContext as CoreVisibilityContext, } from "@json-render/core"; @@ -40,6 +43,14 @@ import { ConfirmDialog } from "./contexts/actions"; import { standardComponents } from "./components/standard"; import { RepeatScopeProvider, useRepeatScope } from "./contexts/repeat-scope"; +const DirectivesContext = React.createContext( + undefined, +); + +function useDirectives(): DirectiveRegistry | undefined { + return React.useContext(DirectivesContext); +} + /** * Props passed to component renderers */ @@ -159,6 +170,7 @@ const ElementRenderer = React.memo(function ElementRenderer({ const { ctx } = useVisibility(); const { execute } = useActions(); const { getSnapshot } = useStateStore(); + const directives = useDirectives(); // Build context with repeat scope (used for both visibility and props) const fullCtx: PropResolutionContext = useMemo( @@ -169,9 +181,10 @@ const ElementRenderer = React.memo(function ElementRenderer({ repeatItem: repeatScope.item, repeatIndex: repeatScope.index, repeatBasePath: repeatScope.basePath, + directives, } - : ctx, - [ctx, repeatScope], + : { ...ctx, directives }, + [ctx, repeatScope, directives], ); // Evaluate visibility (now supports $item/$index inside repeat scopes) @@ -439,6 +452,8 @@ export interface JSONUIProviderProps { string, (value: unknown, args?: Record) => boolean >; + /** Custom directives for user-defined `$`-prefixed dynamic values */ + directives?: DirectiveDefinition[]; /** Callback when state changes (uncontrolled mode) */ onStateChange?: (changes: Array<{ path: string; value: unknown }>) => void; children: ReactNode; @@ -454,9 +469,15 @@ export function JSONUIProvider({ handlers, navigate, validationFunctions, + directives, onStateChange, children, }: JSONUIProviderProps) { + const directiveRegistry = useMemo( + () => (directives ? createDirectiveRegistry(directives) : undefined), + [directives], + ); + return ( - - {children} - - + + + {children} + + + diff --git a/skills/react-native/SKILL.md b/skills/react-native/SKILL.md index f43134b8..70bd1f34 100644 --- a/skills/react-native/SKILL.md +++ b/skills/react-native/SKILL.md @@ -131,6 +131,16 @@ Any prop value can be a data-driven expression resolved at render time: Components do not use a `statePath` prop for two-way binding. Use `{ "$bindState": "/path" }` on the natural value prop instead. +### Custom directives + +Pass custom directive definitions to `JSONUIProvider` with the `directives` prop. Their resolvers can compose with built-in dynamic values such as `$state`: + +```tsx + + + +``` + ## Built-in Actions The `setState` action is handled automatically by `ActionProvider` and updates the state model directly, which re-evaluates visibility conditions and dynamic prop expressions: From d3b2116238f5e60f1a5e95dee4500efafcee9875 Mon Sep 17 00:00:00 2001 From: karankalsi Date: Fri, 24 Jul 2026 15:24:35 +0530 Subject: [PATCH 2/4] feat(react-native): support directives in createRenderer --- packages/react-native/README.md | 2 +- packages/react-native/src/renderer.tsx | 28 +++++++++++++++++--------- skills/react-native/SKILL.md | 2 +- 3 files changed, 21 insertions(+), 11 deletions(-) diff --git a/packages/react-native/README.md b/packages/react-native/README.md index 745c1a4e..bf1065e2 100644 --- a/packages/react-native/README.md +++ b/packages/react-native/README.md @@ -197,7 +197,7 @@ See [@json-render/core](../core/README.md) for full expression syntax. ### Custom directives -Register custom directives through `JSONUIProvider` to resolve user-defined `$`-prefixed values in component props: +Register custom directives through `JSONUIProvider` or a component returned by `createRenderer` to resolve user-defined `$`-prefixed values in component props: ```tsx import { defineDirective, resolvePropValue } from "@json-render/core"; diff --git a/packages/react-native/src/renderer.tsx b/packages/react-native/src/renderer.tsx index e1f3b6ca..c2825106 100644 --- a/packages/react-native/src/renderer.tsx +++ b/packages/react-native/src/renderer.tsx @@ -686,6 +686,8 @@ export interface CreateRendererProps { onAction?: (actionName: string, params?: Record) => void; /** Callback when state changes (uncontrolled mode) */ onStateChange?: (changes: Array<{ path: string; value: unknown }>) => void; + /** Custom directives for user-defined `$`-prefixed dynamic values */ + directives?: DirectiveDefinition[]; /** Whether the spec is currently loading/streaming */ loading?: boolean; /** Fallback component for unknown types */ @@ -739,9 +741,15 @@ export function createRenderer< state, onAction, onStateChange, + directives, loading, fallback, }: CreateRendererProps) { + const directiveRegistry = useMemo( + () => (directives ? createDirectiveRegistry(directives) : undefined), + [directives], + ); + // Wrap onAction with a Proxy so any action name routes to the callback const actionHandlers = onAction ? new Proxy( @@ -767,15 +775,17 @@ export function createRenderer< > - - - - + + + + + + diff --git a/skills/react-native/SKILL.md b/skills/react-native/SKILL.md index 70bd1f34..801d0301 100644 --- a/skills/react-native/SKILL.md +++ b/skills/react-native/SKILL.md @@ -133,7 +133,7 @@ Components do not use a `statePath` prop for two-way binding. Use `{ "$bindState ### Custom directives -Pass custom directive definitions to `JSONUIProvider` with the `directives` prop. Their resolvers can compose with built-in dynamic values such as `$state`: +Pass custom directive definitions to `JSONUIProvider` or a component returned by `createRenderer` with the `directives` prop. Their resolvers can compose with built-in dynamic values such as `$state`: ```tsx From 4b1f1038757c52d7047cd1a1a36912c5202fa8ac Mon Sep 17 00:00:00 2001 From: karankalsi Date: Fri, 24 Jul 2026 15:33:53 +0530 Subject: [PATCH 3/4] feat(react-native): align computed function support --- packages/react-native/README.md | 12 ++++++ packages/react-native/src/renderer.tsx | 60 ++++++++++++++++++-------- skills/react-native/SKILL.md | 4 ++ 3 files changed, 57 insertions(+), 19 deletions(-) diff --git a/packages/react-native/README.md b/packages/react-native/README.md index bf1065e2..e054d878 100644 --- a/packages/react-native/README.md +++ b/packages/react-native/README.md @@ -195,6 +195,18 @@ Any prop value can be a dynamic expression resolved at render time: See [@json-render/core](../core/README.md) for full expression syntax. +### Computed functions + +Register named functions through `JSONUIProvider` or a component returned by `createRenderer` to resolve `$computed` expressions: + +```tsx + `${args.first} ${args.last}` }} +> + + +``` + ### Custom directives Register custom directives through `JSONUIProvider` or a component returned by `createRenderer` to resolve user-defined `$`-prefixed values in component props: diff --git a/packages/react-native/src/renderer.tsx b/packages/react-native/src/renderer.tsx index c2825106..3601918c 100644 --- a/packages/react-native/src/renderer.tsx +++ b/packages/react-native/src/renderer.tsx @@ -12,6 +12,7 @@ import type { Catalog, SchemaDefinition, StateStore, + ComputedFunction, DirectiveDefinition, DirectiveRegistry, } from "@json-render/core"; @@ -43,6 +44,15 @@ import { ConfirmDialog } from "./contexts/actions"; import { standardComponents } from "./components/standard"; import { RepeatScopeProvider, useRepeatScope } from "./contexts/repeat-scope"; +const EMPTY_FUNCTIONS: Record = {}; + +const FunctionsContext = + React.createContext>(EMPTY_FUNCTIONS); + +function useFunctions(): Record { + return React.useContext(FunctionsContext); +} + const DirectivesContext = React.createContext( undefined, ); @@ -170,6 +180,7 @@ const ElementRenderer = React.memo(function ElementRenderer({ const { ctx } = useVisibility(); const { execute } = useActions(); const { getSnapshot } = useStateStore(); + const functions = useFunctions(); const directives = useDirectives(); // Build context with repeat scope (used for both visibility and props) @@ -181,10 +192,11 @@ const ElementRenderer = React.memo(function ElementRenderer({ repeatItem: repeatScope.item, repeatIndex: repeatScope.index, repeatBasePath: repeatScope.basePath, + functions, directives, } - : { ...ctx, directives }, - [ctx, repeatScope, directives], + : { ...ctx, functions, directives }, + [ctx, repeatScope, functions, directives], ); // Evaluate visibility (now supports $item/$index inside repeat scopes) @@ -452,6 +464,8 @@ export interface JSONUIProviderProps { string, (value: unknown, args?: Record) => boolean >; + /** Named functions for `$computed` expressions in props */ + functions?: Record; /** Custom directives for user-defined `$`-prefixed dynamic values */ directives?: DirectiveDefinition[]; /** Callback when state changes (uncontrolled mode) */ @@ -469,6 +483,7 @@ export function JSONUIProvider({ handlers, navigate, validationFunctions, + functions, directives, onStateChange, children, @@ -486,12 +501,14 @@ export function JSONUIProvider({ > - - - {children} - - - + + + + {children} + + + + @@ -686,6 +703,8 @@ export interface CreateRendererProps { onAction?: (actionName: string, params?: Record) => void; /** Callback when state changes (uncontrolled mode) */ onStateChange?: (changes: Array<{ path: string; value: unknown }>) => void; + /** Named functions for `$computed` expressions in props */ + functions?: Record; /** Custom directives for user-defined `$`-prefixed dynamic values */ directives?: DirectiveDefinition[]; /** Whether the spec is currently loading/streaming */ @@ -741,6 +760,7 @@ export function createRenderer< state, onAction, onStateChange, + functions, directives, loading, fallback, @@ -775,17 +795,19 @@ export function createRenderer< > - - - - - - + + + + + + + + diff --git a/skills/react-native/SKILL.md b/skills/react-native/SKILL.md index 801d0301..b31ad49d 100644 --- a/skills/react-native/SKILL.md +++ b/skills/react-native/SKILL.md @@ -131,6 +131,10 @@ Any prop value can be a data-driven expression resolved at render time: Components do not use a `statePath` prop for two-way binding. Use `{ "$bindState": "/path" }` on the natural value prop instead. +### Computed functions + +Pass named functions to `JSONUIProvider` or a component returned by `createRenderer` with the `functions` prop. These functions resolve `$computed` expressions in element props. + ### Custom directives Pass custom directive definitions to `JSONUIProvider` or a component returned by `createRenderer` with the `directives` prop. Their resolvers can compose with built-in dynamic values such as `$state`: From 1984339bba594c109e4554194daad33b834216d8 Mon Sep 17 00:00:00 2001 From: karankalsi Date: Fri, 24 Jul 2026 15:36:56 +0530 Subject: [PATCH 4/4] docs(react-native): show composed directives --- packages/react-native/README.md | 47 ++++++++++++++++++++++++++++----- 1 file changed, 41 insertions(+), 6 deletions(-) diff --git a/packages/react-native/README.md b/packages/react-native/README.md index e054d878..9b1cd585 100644 --- a/packages/react-native/README.md +++ b/packages/react-native/README.md @@ -215,20 +215,55 @@ Register custom directives through `JSONUIProvider` or a component returned by ` import { defineDirective, resolvePropValue } from "@json-render/core"; import { z } from "zod"; -const uppercase = defineDirective({ - name: "$uppercase", - schema: z.object({ $uppercase: z.unknown() }), +const math = defineDirective({ + name: "$math", + schema: z.object({ + $math: z.literal("multiply"), + a: z.unknown(), + b: z.unknown(), + }), resolve(value, ctx) { - return String(resolvePropValue(value.$uppercase, ctx)).toUpperCase(); + return Number(resolvePropValue(value.a, ctx)) * + Number(resolvePropValue(value.b, ctx)); }, }); - +const format = defineDirective({ + name: "$format", + schema: z.object({ + $format: z.literal("currency"), + value: z.unknown(), + currency: z.string(), + }), + resolve(value, ctx) { + const amount = Number(resolvePropValue(value.value, ctx)); + return new Intl.NumberFormat("en-US", { + style: "currency", + currency: value.currency, + }).format(amount); + }, +}); + + ; ``` -Directives can wrap built-in expressions such as `{ "$uppercase": { "$state": "/message" } }`. See the [directives documentation](https://json-render.dev/docs/directives) for more details. +The directives compose in props, so `$format` can format a `$math` result that reads its operands from state: + +```json +{ + "$format": "currency", + "value": { + "$math": "multiply", + "a": { "$state": "/price" }, + "b": { "$state": "/qty" } + }, + "currency": "USD" +} +``` + +See the [directives documentation](https://json-render.dev/docs/directives) for more details. ## Tab Navigation Pattern