Skip to content

Commit 1bac9e8

Browse files
bloveclaude
andcommitted
docs(render): specs page — inert schema fields flagged; every fence parses; the demo's spec quoted verbatim
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
1 parent 6a0a77b commit 1bac9e8

1 file changed

Lines changed: 38 additions & 20 deletions

File tree

  • apps/website/content/docs/render/guides

apps/website/content/docs/render/guides/specs.mdx

Lines changed: 38 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -4,21 +4,21 @@ description: How the spec rendering example streams a JSON spec into a live comp
44

55
# Specs
66

7-
A spec is the JSON object that describes an entire UI tree. It names a root element, holds every element in one flat map, and says for each element which component renders it, what props it receives, which children it owns, and when it is visible. The running example streams three specs in one character at a time and renders each one as it arrives, so the format on this page is the format you can watch being parsed.
7+
A spec is the JSON object that describes an entire UI tree. It names a root element, holds every element in one flat map, and says for each element which component renders it, what props it receives, which children it owns, and when it is visible. The running example streams three specs one character at a time and renders each one as it arrives, so the format on this page is the format you can watch being parsed.
88

99
## What the demo does
1010

1111
The Run tab is split in two. The left side is the live render output, the right side is the raw JSON as it streams, and the bar along the bottom is a transport: play and pause, a track you can scrub to any character, a character count, and 1x, 2x and 4x speeds.
1212

13-
Three tabs across the top pick the spec. "Heading + Text" is a heading with one text child. "Card + Badge" is a card holding a badge and a paragraph. "Nested Layout" is a heading over two cards, each with its own text child. Picking a tab restarts the stream against that spec, and elements appear as soon as their props arrive, with skeleton placeholders standing in until they do.
13+
Three tabs across the top pick the spec. "Heading + Text" is a heading with one text child. "Card + Badge" is a card holding a badge and a paragraph. "Nested Layout" is a heading over two cards, each with its own text child. Picking a tab restarts the stream against that spec, and elements appear as soon as their props arrive. The Text, Heading and Card components draw a skeleton placeholder while they wait; the Badge component has none, so a badge simply appears once its label arrives.
1414

1515
## How it is built
1616

17-
The example is one Angular file plus an application config. Open the Code tab to read them in place.
17+
The Code tab holds the component and the application config. Open it to read them in place.
1818

1919
### The spec the demo streams
2020

21-
The three specs are plain JSON strings the component ships with. This is the first one, and it is the whole format in miniature: a `root` key, a flat `elements` map, and a `children` array that holds keys rather than nested objects.
21+
The three specs live in `specs.ts`, beside the component, as plain JSON strings. This is the first one, and it is the whole format in miniature: a `root` key, a flat `elements` map, and a `children` array that holds keys rather than nested objects.
2222

2323
```json
2424
{
@@ -31,7 +31,9 @@ The three specs are plain JSON strings the component ships with. This is the fir
3131
},
3232
"desc": {
3333
"type": "Text",
34-
"props": { "content": "This UI is rendered entirely from a JSON specification." }
34+
"props": {
35+
"content": "This UI is rendered entirely from a JSON specification. Each element maps to a registered Angular component."
36+
}
3537
}
3638
}
3739
}
@@ -71,7 +73,7 @@ The text component in the same file declares `childKeys` and never loops over it
7173

7274
### A partial spec still renders
7375

74-
The spec does not arrive whole. A partial JSON parser consumes the stream character by character and materializes whatever is complete so far, so the value handed to `<render-spec>` grows one element and one prop at a time.
76+
The spec does not arrive whole. `StreamingSimulator`, a shared helper that sits beside the examples rather than in the block below, is the piece that feeds characters into an incremental JSON parser and materializes whatever is complete so far, so the value handed to `<render-spec>` grows one element and one prop at a time.
7577

7678
<ExampleCode file="spec-rendering.component.ts" region="streaming-source" title="spec-rendering.component.ts — the streaming source" />
7779

@@ -108,7 +110,7 @@ const spec: Spec = {
108110
|----------|------|-------------|
109111
| `root` | `string` | The key in `elements` that rendering starts from |
110112
| `elements` | `Record<string, UIElement>` | A flat map of every element definition, keyed by a unique string |
111-
| `state` | `Record<string, unknown>` | Optional. Seeds the internal state store that `<render-spec>` creates when no store is supplied |
113+
| `state` | `Record<string, unknown>` | Optional. Seeds the internal state store that `<render-spec>` creates when neither a `[store]` input nor a `provideRender({ store })` is supplied. The internal store is created once and is not re-seeded by a later spec |
112114

113115
<Callout type="info" title="Flat element map">
114116
Elements are stored in a flat map rather than a nested tree. Parent-child relationships are expressed through the `children` property, which holds keys pointing at other entries in the same map. That keeps lookups constant-time and makes a spec easy for a model to emit and to patch one element at a time.
@@ -126,6 +128,8 @@ interface UIElement {
126128
visible?: VisibilityCondition;
127129
repeat?: { statePath: string; key?: string };
128130
on?: Record<string, ActionBinding | ActionBinding[]>;
131+
// Part of the spec schema; the Angular renderer does not act on it today
132+
// (use `on` bindings or an effect over the store).
129133
watch?: Record<string, ActionBinding | ActionBinding[]>;
130134
}
131135
```
@@ -136,9 +140,9 @@ interface UIElement {
136140
| `props` | `Record<string, unknown>` | Inputs for the component. Static values or expressions |
137141
| `children` | `string[]` | Keys of child elements in the same `elements` map |
138142
| `visible` | `VisibilityCondition` | Visibility condition, evaluated against the store |
139-
| `repeat` | `{ statePath: string; key?: string }` | Render this element once per item in a state array |
143+
| `repeat` | `{ statePath: string; key?: string }` | Render this element once per item in a state array. `key` is schema-level and is not used by the Angular renderer |
140144
| `on` | `Record<string, ActionBinding \| ActionBinding[]>` | Event name to the action or actions it fires |
141-
| `watch` | `Record<string, ActionBinding \| ActionBinding[]>` | State path to the actions that fire when the value there changes |
145+
| `watch` | `Record<string, ActionBinding \| ActionBinding[]>` | State path to the actions that fire when the value there changes. Part of the spec schema; the Angular renderer does not act on it today (use `on` bindings or an effect over the store) |
142146

143147
## Prop expressions
144148

@@ -162,9 +166,9 @@ Arrays and plain objects are walked, so an expression nested inside a prop objec
162166
`$bindState` resolves like `$state` and additionally populates the component's `bindings` input with the path, so the component can write back:
163167

164168
```typescript
165-
props: {
169+
const props = {
166170
value: { $bindState: '/form/email' },
167-
}
171+
};
168172
// The component receives:
169173
// value = the current value at /form/email
170174
// bindings = { value: '/form/email' }
@@ -185,9 +189,9 @@ const functions: Record<string, ComputedFunction> = {
185189
```
186190

187191
```typescript
188-
props: {
192+
const props = {
189193
label: { $computed: 'uppercase', args: { text: { $state: '/name' } } },
190-
}
194+
};
191195
```
192196

193197
Pass the map through the `[functions]` input on `<render-spec>`, or register it once with [`provideRender()`](/docs/render/api/provide-render). An unregistered name resolves to `undefined` and logs a warning.
@@ -197,13 +201,25 @@ Pass the map through the `[functions]` input on `<render-spec>`, or register it
197201
The `visible` property decides whether an element mounts. When it evaluates to false the element and everything under it stays out of the DOM. Omitting it means visible.
198202

199203
```typescript
200-
{ type: 'Text', props: { content: 'Never shown' }, visible: false }
204+
import type { UIElement } from '@json-render/core';
205+
206+
const never: UIElement = {
207+
type: 'Text',
208+
props: { content: 'Never shown' },
209+
visible: false,
210+
};
201211

202-
{ type: 'Text', props: { content: 'Shown when the flag is truthy' },
203-
visible: { $state: '/showMessage' } }
212+
const whenFlag: UIElement = {
213+
type: 'Text',
214+
props: { content: 'Shown when the flag is truthy' },
215+
visible: { $state: '/showMessage' },
216+
};
204217

205-
{ type: 'Text', props: { content: 'Shown when the count is over five' },
206-
visible: { $state: '/count', gt: 5 } }
218+
const whenOverFive: UIElement = {
219+
type: 'Text',
220+
props: { content: 'Shown when the count is over five' },
221+
visible: { $state: '/count', gt: 5 },
222+
};
207223
```
208224

209225
A single condition reads `$state`, `$item` or `$index` and applies at most one comparison operator: `eq`, `neq`, `gt`, `gte`, `lt` or `lte`. With no operator it checks truthiness, and `not: true` inverts the result. An array of conditions is an implicit AND; `{ $and: [...] }` and `{ $or: [...] }` are the explicit forms and may nest.
@@ -213,14 +229,16 @@ A single condition reads `$state`, `$item` or `$index` and applies at most one c
213229
`repeat` renders one copy of the element for each item in a state array:
214230

215231
```typescript
216-
{
232+
import type { UIElement } from '@json-render/core';
233+
234+
const item: UIElement = {
217235
type: 'ListItem',
218236
props: {
219237
label: { $item: 'name' },
220238
position: { $index: true },
221239
},
222240
repeat: { statePath: '/todos' },
223-
}
241+
};
224242
```
225243

226244
For each item the renderer builds a repeat scope holding the item, its index and its base path (`/todos/0`, `/todos/1`, and so on), provides that scope through a child injector, and resolves the element's props inside it, so `$item`, `$bindItem` and `$index` mean something different in every copy.

0 commit comments

Comments
 (0)