Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 34 additions & 0 deletions apps/web/app/(main)/docs/api/react-native/page.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -149,6 +149,40 @@ Conditions in specs use the `VisibilityCondition` format with `$state` paths (e.
</ValidationProvider>
```

### JSONUIProvider

Convenience wrapper that combines `StateProvider`, `VisibilityProvider`, `ActionProvider`, and `ValidationProvider`. It accepts the same props as those providers, plus:

<table>
<thead>
<tr>
<th>Prop</th>
<th>Type</th>
<th>Description</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>functions</code></td>
<td><code>Record&lt;string, ComputedFunction&gt;</code></td>
<td>Named functions for <code>$computed</code> expressions in props</td>
</tr>
</tbody>
</table>

```tsx
<JSONUIProvider
registry={registry}
initialState={{}}
handlers={handlers}
functions={{ fullName: (args) => `${args.first} ${args.last}` }}
>
<Renderer spec={spec} registry={registry} />
</JSONUIProvider>
```

The `functions` prop is also available on `createRenderer`.

## defineRegistry

Create a type-safe component registry. Standard components are built-in; only register custom components.
Expand Down
27 changes: 27 additions & 0 deletions packages/react-native/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -195,6 +195,33 @@ Any prop value can be a dynamic expression resolved at render time:

See [@json-render/core](../core/README.md) for full expression syntax.

### `$template` and `$computed`

```json
{
"label": { "$template": "Hello, ${/user/name}!" },
"fullName": {
"$computed": "fullName",
"args": {
"first": { "$state": "/form/firstName" },
"last": { "$state": "/form/lastName" }
}
}
}
```

Register named functions via the `functions` prop on `JSONUIProvider` or `createRenderer` (same as [@json-render/react](../react/README.md)):

```tsx
<JSONUIProvider
registry={registry}
initialState={{}}
functions={{ fullName: (args) => `${args.first} ${args.last}` }}
>
<Renderer spec={spec} registry={registry} />
</JSONUIProvider>
```

## Tab Navigation Pattern

Combine `Pressable`, `setState`, visibility conditions, and dynamic props for functional tabs:
Expand Down
79 changes: 52 additions & 27 deletions packages/react-native/src/renderer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import type {
Catalog,
SchemaDefinition,
StateStore,
ComputedFunction,
} from "@json-render/core";
import {
resolveElementProps,
Expand Down Expand Up @@ -136,6 +137,19 @@ class ElementErrorBoundary extends React.Component<
}
}

// ---------------------------------------------------------------------------
// FunctionsContext – provides $computed functions to the element tree
// ---------------------------------------------------------------------------

const EMPTY_FUNCTIONS: Record<string, ComputedFunction> = {};

const FunctionsContext =
React.createContext<Record<string, ComputedFunction>>(EMPTY_FUNCTIONS);

function useFunctions(): Record<string, ComputedFunction> {
return React.useContext(FunctionsContext);
}

interface ElementRendererProps {
element: UIElement;
spec: Spec;
Expand All @@ -159,20 +173,21 @@ const ElementRenderer = React.memo(function ElementRenderer({
const { ctx } = useVisibility();
const { execute } = useActions();
const { getSnapshot } = useStateStore();

// Build context with repeat scope (used for both visibility and props)
const fullCtx: PropResolutionContext = useMemo(
() =>
repeatScope
? {
...ctx,
repeatItem: repeatScope.item,
repeatIndex: repeatScope.index,
repeatBasePath: repeatScope.basePath,
}
: ctx,
[ctx, repeatScope],
);
const functions = useFunctions();

// Build context with repeat scope and $computed functions (visibility + props)
const fullCtx: PropResolutionContext = useMemo(() => {
const base: PropResolutionContext = repeatScope
? {
...ctx,
repeatItem: repeatScope.item,
repeatIndex: repeatScope.index,
repeatBasePath: repeatScope.basePath,
}
: { ...ctx };
base.functions = functions;
return base;
}, [ctx, repeatScope, functions]);

// Evaluate visibility (now supports $item/$index inside repeat scopes)
const isVisible =
Expand Down Expand Up @@ -439,6 +454,8 @@ export interface JSONUIProviderProps {
string,
(value: unknown, args?: Record<string, unknown>) => boolean
>;
/** Named functions for `$computed` expressions in props */
functions?: Record<string, ComputedFunction>;
/** Callback when state changes (uncontrolled mode) */
onStateChange?: (changes: Array<{ path: string; value: unknown }>) => void;
children: ReactNode;
Expand All @@ -454,6 +471,7 @@ export function JSONUIProvider({
handlers,
navigate,
validationFunctions,
functions,
onStateChange,
children,
}: JSONUIProviderProps) {
Expand All @@ -465,10 +483,12 @@ export function JSONUIProvider({
>
<VisibilityProvider>
<ActionProvider handlers={handlers} navigate={navigate}>
<ValidationProvider customFunctions={validationFunctions}>
{children}
<ConfirmationDialogManager />
</ValidationProvider>
<FunctionsContext.Provider value={functions ?? EMPTY_FUNCTIONS}>
<ValidationProvider customFunctions={validationFunctions}>
{children}
<ConfirmationDialogManager />
</ValidationProvider>
</FunctionsContext.Provider>
</ActionProvider>
</VisibilityProvider>
</StateProvider>
Expand Down Expand Up @@ -663,6 +683,8 @@ export interface CreateRendererProps {
onAction?: (actionName: string, params?: Record<string, unknown>) => void;
/** Callback when state changes (uncontrolled mode) */
onStateChange?: (changes: Array<{ path: string; value: unknown }>) => void;
/** Named functions for `$computed` expressions in props */
functions?: Record<string, ComputedFunction>;
/** Whether the spec is currently loading/streaming */
loading?: boolean;
/** Fallback component for unknown types */
Expand Down Expand Up @@ -716,6 +738,7 @@ export function createRenderer<
state,
onAction,
onStateChange,
functions,
loading,
fallback,
}: CreateRendererProps) {
Expand Down Expand Up @@ -744,15 +767,17 @@ export function createRenderer<
>
<VisibilityProvider>
<ActionProvider handlers={actionHandlers}>
<ValidationProvider>
<Renderer
spec={spec}
registry={registry}
loading={loading}
fallback={fallback}
/>
<ConfirmationDialogManager />
</ValidationProvider>
<FunctionsContext.Provider value={functions ?? EMPTY_FUNCTIONS}>
<ValidationProvider>
<Renderer
spec={spec}
registry={registry}
loading={loading}
fallback={fallback}
/>
<ConfirmationDialogManager />
</ValidationProvider>
</FunctionsContext.Provider>
</ActionProvider>
</VisibilityProvider>
</StateProvider>
Expand Down
2 changes: 2 additions & 0 deletions skills/react-native/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,7 @@ Any prop value can be a data-driven expression resolved at render time:
- **`{ "$bindState": "/path" }`** - two-way binding: use on the natural value prop (value, checked, pressed, etc.) of form components.
- **`{ "$bindItem": "field" }`** - two-way binding to a repeat item field. Use inside repeat scopes.
- **`{ "$cond": <condition>, "$then": <value>, "$else": <value> }`** - conditional value
- **`{ "$computed": "name", "args": { ... } }`** - call a named function; register implementations with the `functions` prop on `JSONUIProvider` or `createRenderer` (same as `@json-render/react`)

```json
{
Expand Down Expand Up @@ -147,6 +148,7 @@ The `setState` action is handled automatically by `ActionProvider` and updates t
| `ActionProvider` | Handle actions dispatched from components |
| `VisibilityProvider` | Enable conditional rendering based on state |
| `ValidationProvider` | Form field validation |
| `JSONUIProvider` | Combines the above providers; also accepts `functions` for `$computed` props |

### External Store (Controlled Mode)

Expand Down