@ea-lab/reactive-json-docs 2.8.0 → 2.11.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (38) hide show
  1. package/package.json +2 -2
  2. package/public/rjbuild/docs/core/element/form/CheckBoxField.md +142 -18
  3. package/public/rjbuild/docs/core/element/form/CheckBoxField.yaml +132 -7
  4. package/public/rjbuild/docs/core/element/form/SelectField.md +36 -10
  5. package/public/rjbuild/docs/core/element/form/SelectField.yaml +37 -3
  6. package/public/rjbuild/docs/core/element/form/formElementsCommon.md +32 -0
  7. package/public/rjbuild/docs/core/element/html/Html.yaml +47 -0
  8. package/public/rjbuild/docs/core/element/html/LabelFromValue.md +159 -0
  9. package/public/rjbuild/docs/core/element/html/LabelFromValue.yaml +163 -10
  10. package/public/rjbuild/docs/core/element/special/DataSync.md +41 -1
  11. package/public/rjbuild/docs/core/element/special/DataSync.yaml +41 -1
  12. package/public/rjbuild/docs/core/example/native-html-forms.md +19 -2
  13. package/public/rjbuild/docs/core/hook/index.md +3 -0
  14. package/public/rjbuild/docs/core/hook/index.yaml +3 -0
  15. package/public/rjbuild/docs/core/reaction/collectField.md +51 -0
  16. package/public/rjbuild/docs/core/reaction/collectField.yaml +81 -0
  17. package/public/rjbuild/docs/core/reaction/fetchData.md +21 -1
  18. package/public/rjbuild/docs/core/reaction/fetchData.yaml +2 -2
  19. package/public/rjbuild/docs/core/reaction/index.md +5 -0
  20. package/public/rjbuild/docs/core/reaction/index.yaml +5 -0
  21. package/public/rjbuild/docs/core/reaction/joinData.md +51 -0
  22. package/public/rjbuild/docs/core/reaction/joinData.yaml +87 -0
  23. package/public/rjbuild/docs/core/reaction/printPage.md +22 -0
  24. package/public/rjbuild/docs/core/reaction/printPage.yaml +29 -0
  25. package/public/rjbuild/docs/core/reaction/redirectNow.md +20 -2
  26. package/public/rjbuild/docs/core/reaction/redirectNow.yaml +24 -3
  27. package/public/rjbuild/docs/core/reaction/setClipboardData.md +35 -6
  28. package/public/rjbuild/docs/core/reaction/setClipboardData.yaml +18 -6
  29. package/public/rjbuild/docs/core/reaction/stringifyData.md +45 -0
  30. package/public/rjbuild/docs/core/reaction/stringifyData.yaml +58 -0
  31. package/public/rjbuild/docs/core/reaction/submitData.md +1 -1
  32. package/public/rjbuild/docs/core/reaction/submitData.yaml +4 -2
  33. package/public/rjbuild/docs/core/reaction/toggleData.md +72 -0
  34. package/public/rjbuild/docs/core/reaction/toggleData.yaml +77 -0
  35. package/public/rjbuild/docs/getting-started/reactions.md +5 -0
  36. package/public/rjbuild/docs/getting-started/reactions.yaml +5 -0
  37. package/public/rjbuild/docs/getting-started/rjbuild-structure.md +23 -1
  38. package/public/rjbuild/docs/getting-started/rjbuild-structure.yaml +25 -1
@@ -74,6 +74,53 @@ renderView:
74
74
  class: ""
75
75
  txt: Hello world!
76
76
 
77
+ - type: Markdown
78
+ content: |
79
+ ## URL attributes
80
+
81
+ The URL attributes (`href`, `src`, `action`, `formAction`, `poster`) accept an array of segments, in the format of the `src` of `additionalDataSource` (see *Dynamic URLs with `src` as Array* in [RjBuild structure](../../getting-started/rjbuild-structure)). The references are evaluated in the current template context, so each item of a `Switch` gets its own URL:
82
+
83
+ ```yaml
84
+ - type: a
85
+ attributes:
86
+ href:
87
+ - "/article/"
88
+ - ~.year
89
+ - "/"
90
+ - segment: ~.slug
91
+ required: true
92
+ content: ~.title
93
+ ```
94
+
95
+ When a required segment is empty, the attribute is omitted. An array in another attribute is evaluated as before (each item separately).
96
+
97
+ - type: Markdown
98
+ content: |
99
+ ## Form controls
100
+
101
+ `input`, `textarea` and `select` elements are rendered as **controlled** React elements: their `value` defaults to `""`.
102
+
103
+ A checkbox or radio `input` is controlled by its `checked` attribute, which defaults to `false`. Without a binding, the input never shows as checked, whatever its reactions do to the data. Bind it to the data:
104
+
105
+ - a boolean: `checked: ~.isSelected` (a falsy value leaves the input unchecked);
106
+ - a comparison or a list membership: an `attributeTransforms` entry setting `checked`, with a condition (`is`, `contains`...).
107
+
108
+ ```yaml
109
+ - type: input
110
+ attributes:
111
+ type: checkbox
112
+ value: ~.label
113
+ attributeTransforms:
114
+ - what: setAttributeValue
115
+ name: checked
116
+ mode: replace
117
+ value: true
118
+ when: ~~.lead.sessions
119
+ contains: ~.label
120
+ ```
121
+
122
+ A warning is logged when a checkbox or radio has event reactions (`on: change`...) but neither a `checked` attribute nor an attribute transform on `checked`. To store a value or a list of values, prefer [CheckBoxField](../form/CheckBoxField), which binds `checked` itself.
123
+
77
124
  - type: Markdown
78
125
  content: |
79
126
  ## Limitations
@@ -0,0 +1,159 @@
1
+ # LabelFromValue
2
+
3
+ The `LabelFromValue` component displays the **label** associated with a value, using an option-like structure as data source. It is the read-only counterpart of the form fields: the same `{label, value}` list feeding a [`SelectField`](../form/SelectField.md) or a [`CheckBoxField`](../form/CheckBoxField.md) can be reused to render the selected value as text.
4
+
5
+ ## Basic Syntax
6
+
7
+ ```yaml
8
+ - type: LabelFromValue
9
+ dataLocation: ~.status
10
+ options:
11
+ - label: "Active"
12
+ value: 1
13
+ - label: "Inactive"
14
+ value: 2
15
+ ```
16
+
17
+ With `status: 2` in the data, this renders `Inactive`.
18
+
19
+ ## Properties
20
+
21
+ - `dataLocation` (string, optional): Path to the value to resolve. Without it, the value is read from the template context under the component's `datafield`.
22
+ - `options` (array, optional): Array of `{label, value}` objects (both support template evaluation). Ignored when `dynamicOptions` is set.
23
+ - `dynamicOptions` (any, optional): Template reference resolving to the options array (e.g. `~~.allStages`, typically options loaded from a backend). Takes precedence over `options`. The items can be `{label, value}` objects, scalars (each scalar is used as both label and value), or objects of any shape mapped with `optionMapping`.
24
+ - `optionMapping` (object, optional): Builds each option from an item of the options list, exactly like in `SelectField` and `CheckBoxField`. It is evaluated once per item, **with the item as the template context** (`~.title` reads the item's `title`, `~~.` still reads the global data). The mapped keys override the item's keys.
25
+ - `defaultFieldValue` (any, optional): Value used when no data is present.
26
+ - `loose` (boolean, optional): Matches the options by `String()`-coerced equality instead of the default strict one (see [Loose matching](#loose-matching)). Omitted or `false` = strict equality (original behavior).
27
+ - `actions` (array, optional): Actions and conditional reactions to execute based on component state (hide, tooltip, …).
28
+
29
+ The label is rendered through the `View` engine, so it can be a plain string or a full component structure (`{type: strong, content: …}`).
30
+
31
+ ## Loose matching
32
+
33
+ By default an option matches the current value only on **strict** equality (`===`), so `1` (number) and `"1"` (string) are two different values and no label is found.
34
+
35
+ This bites when the JS type of the same identifier is not guaranteed across sources. A typical case is a decoupled backend that renders an entity reference id as a **string** when reading the entity, but echoes it back as a **number** in its save response: the label is correct on load, then disappears right after a save — until a full reload restores the original type.
36
+
37
+ Set `loose: true` to compare the values as strings instead:
38
+
39
+ ```yaml
40
+ - type: LabelFromValue
41
+ dataLocation: ~.stage
42
+ dynamicOptions: ~~.allStages
43
+ loose: true
44
+ ```
45
+
46
+ Nullish values are never coerced: an empty value (`null` / `undefined`) still matches nothing, so it cannot accidentally match an option whose value is the `"null"` string. `loose` is opt-in — omitted, the component behaves exactly as before, so existing usages are unaffected.
47
+
48
+ ## Examples
49
+
50
+ ### Display the label of a selected value
51
+
52
+ ```yaml
53
+ renderView:
54
+ - type: SelectField
55
+ dataLocation: ~.status
56
+ label: "Status"
57
+ options:
58
+ - label: "Active"
59
+ value: 1
60
+ - label: "Inactive"
61
+ value: 2
62
+ - label: "Pending"
63
+ value: 3
64
+ - type: div
65
+ content:
66
+ - type: strong
67
+ content: "Label: "
68
+ - type: LabelFromValue
69
+ dataLocation: ~.status
70
+ options:
71
+ - label: "Active"
72
+ value: 1
73
+ - label: "Inactive"
74
+ value: 2
75
+ - label: "Pending"
76
+ value: 3
77
+
78
+ data:
79
+ status: 3
80
+ ```
81
+
82
+ Renders: `Label: Pending`.
83
+
84
+ ### Options loaded from a backend, with loose matching
85
+
86
+ ```yaml
87
+ renderView:
88
+ - type: LabelFromValue
89
+ dataLocation: ~~.deal.data.stage
90
+ dynamicOptions: ~~.allStages
91
+ loose: true
92
+
93
+ data:
94
+ # allStages is typically fetched (additionalDataSource) as
95
+ # [{label: "Discovery", value: "12"}, {label: "Negotiation", value: "13"}, …]
96
+ deal:
97
+ data:
98
+ stage: 13
99
+ ```
100
+
101
+ The value is a number while the option values are strings: without `loose`, nothing would be displayed.
102
+
103
+ ### Options of any shape, with a mapping
104
+
105
+ ```yaml
106
+ renderView:
107
+ - type: LabelFromValue
108
+ dataLocation: ~~.deal.stage
109
+ dynamicOptions: ~~.stages
110
+ optionMapping:
111
+ label: ~.title
112
+ value: ~.id
113
+
114
+ data:
115
+ stages:
116
+ - id: "new"
117
+ title: "New"
118
+ - id: "won"
119
+ title: "Won"
120
+ deal:
121
+ stage: "won"
122
+ ```
123
+
124
+ Renders: `Won`. Use the same `optionMapping` as the `SelectField` editing the value.
125
+
126
+ ### Inside a template, against global data
127
+
128
+ ```yaml
129
+ templates:
130
+ userCard:
131
+ type: div
132
+ content:
133
+ - "Admin: "
134
+ - type: LabelFromValue
135
+ dataLocation: ~~.isAdmin
136
+ options:
137
+ - label: "Yes"
138
+ value: true
139
+ - label: "No"
140
+ value: false
141
+
142
+ renderView:
143
+ - load: userCard
144
+
145
+ data:
146
+ isAdmin: true
147
+ ```
148
+
149
+ ## Notes
150
+
151
+ - The first matching option wins; the options list is not deduplicated.
152
+ - When no option matches the value (or the matched option carries no `label`), nothing is displayed — the value itself is not used as a fallback label.
153
+ - An empty value (`null`, `undefined`, `""`) therefore renders nothing, unless an option explicitly declares that value.
154
+
155
+ ## Limitations
156
+
157
+ - No support for grouped options or nested option structures.
158
+ - No built-in formatting of the label (wrap the component if needed).
159
+ - Multi-value data (an array of values) is not supported: the whole array is compared to each option value.
@@ -3,7 +3,23 @@ renderView:
3
3
  content: |
4
4
  # LabelFromValue
5
5
 
6
- The `LabelFromValue` component displays the label associated with a value, using an option-like structure. It is useful for showing the display label of a value selected in a SelectField or any value/label mapping.
6
+ The `LabelFromValue` component displays the **label** associated with a value, using an option-like structure as data source. It is the read-only counterpart of the form fields: the same `{label, value}` list feeding a [`SelectField`](/docs/core/element/form/SelectField) or a [`CheckBoxField`](/docs/core/element/form/CheckBoxField) can be reused to render the selected value as text.
7
+
8
+ - type: Markdown
9
+ content: |
10
+ ## Basic Syntax
11
+
12
+ ```yaml
13
+ - type: LabelFromValue
14
+ dataLocation: ~.status
15
+ options:
16
+ - label: "Active"
17
+ value: 1
18
+ - label: "Inactive"
19
+ value: 2
20
+ ```
21
+
22
+ With `status: 2` in the data, this renders `Inactive`.
7
23
 
8
24
  ## Properties
9
25
 
@@ -11,22 +27,44 @@ renderView:
11
27
  content:
12
28
  - term:
13
29
  code: dataLocation
14
- after: "(string, required)"
15
- details: "Path to the value."
30
+ after: "(string, optional)"
31
+ details: "Path to the value to resolve. Without it, the value is read from the template context under the component's `datafield`."
16
32
  - term:
17
33
  code: options
18
34
  after: "(array, optional)"
19
35
  details:
20
36
  type: Markdown
21
- content: "Array of `{label, value}` objects."
37
+ content: "Array of `{label, value}` objects (both support template evaluation). Ignored when `dynamicOptions` is set."
22
38
  - term:
23
39
  code: dynamicOptions
24
40
  after: "(any, optional)"
25
- details: "Template value for dynamic options."
41
+ details:
42
+ type: Markdown
43
+ content: "Template reference resolving to the options array (e.g. `~~.allStages`, typically options loaded from a backend). Takes precedence over `options`. The items can be `{label, value}` objects, scalars (each scalar is used as both label and value), or objects of any shape mapped with `optionMapping`."
44
+ - term:
45
+ code: optionMapping
46
+ after: "(object, optional)"
47
+ details:
48
+ type: Markdown
49
+ content: "Builds each option from an item of the options list, exactly like in `SelectField` and `CheckBoxField`. It is evaluated once per item, **with the item as the template context** (`~.title` reads the item's `title`, `~~.` still reads the global data). The mapped keys override the item's keys."
26
50
  - term:
27
51
  code: defaultFieldValue
28
52
  after: "(any, optional)"
29
- details: "Default value if none is set."
53
+ details: "Value used when no data is present."
54
+ - term:
55
+ code: loose
56
+ after: "(boolean, optional)"
57
+ details:
58
+ type: Markdown
59
+ content: "Matches the options by `String()`-coerced equality instead of the default strict one. See [Loose matching](#loose-matching). Omitted or `false` = strict equality (original behavior)."
60
+ - term:
61
+ code: actions
62
+ after: "(array, optional)"
63
+ details: "Actions and conditional reactions to execute based on component state (hide, tooltip, …)."
64
+
65
+ - type: Markdown
66
+ content: |
67
+ The label is rendered through the `View` engine, so it can be a plain string or a full component structure (`{type: strong, content: …}`).
30
68
 
31
69
  - type: RjBuildDescriber
32
70
  title: "Interactive Example: Display label from value"
@@ -64,8 +102,123 @@ renderView:
64
102
 
65
103
  - type: Markdown
66
104
  content: |
105
+ ## Loose matching
106
+
107
+ By default an option matches the current value only on **strict** equality (`===`), so `1` (number) and `"1"` (string) are two different values and no label is found.
108
+
109
+ This bites when the JS type of the same identifier is not guaranteed across sources. A typical case is a decoupled backend that renders an entity reference id as a **string** when reading the entity, but echoes it back as a **number** in its save response: the label is correct on load, then disappears right after a save — until a full reload restores the original type.
110
+
111
+ Set `loose: true` to compare the values as strings instead:
112
+
113
+ ```yaml
114
+ - type: LabelFromValue
115
+ dataLocation: ~.stage
116
+ dynamicOptions: ~~.allStages
117
+ loose: true
118
+ ```
119
+
120
+ Nullish values are never coerced: an empty value (`null` / `undefined`) still matches nothing, so it cannot accidentally match an option whose value is the `"null"` string. `loose` is opt-in — omitted, the component behaves exactly as before.
121
+
122
+ - type: RjBuildDescriber
123
+ title: "Interactive Example: Loose matching"
124
+ description:
125
+ - type: Markdown
126
+ content: |
127
+ Both components read the same value, `"2"` (a **string**), against options whose values are **numbers**. Only the loose one finds the label.
128
+ toDescribe:
129
+ renderView:
130
+ - type: div
131
+ content:
132
+ - type: strong
133
+ content: "Strict (default): "
134
+ - type: LabelFromValue
135
+ dataLocation: ~.status
136
+ options:
137
+ - label: "Active"
138
+ value: 1
139
+ - label: "Inactive"
140
+ value: 2
141
+ - label: "Pending"
142
+ value: 3
143
+ - type: div
144
+ content:
145
+ - type: strong
146
+ content: "Loose: "
147
+ - type: LabelFromValue
148
+ dataLocation: ~.status
149
+ loose: true
150
+ options:
151
+ - label: "Active"
152
+ value: 1
153
+ - label: "Inactive"
154
+ value: 2
155
+ - label: "Pending"
156
+ value: 3
157
+ data:
158
+ status: "2"
159
+
160
+ - type: RjBuildDescriber
161
+ title: "Interactive Example: Options loaded from a backend"
162
+ description:
163
+ - type: Markdown
164
+ content: |
165
+ `dynamicOptions` reads the options from the data (here `allStages`, as an `additionalDataSource` would provide them). The value is a **number** while the option values are **strings**, hence `loose: true`.
166
+ toDescribe:
167
+ renderView:
168
+ - type: div
169
+ content:
170
+ - type: strong
171
+ content: "Stage: "
172
+ - type: LabelFromValue
173
+ dataLocation: ~~.deal.data.stage
174
+ dynamicOptions: ~~.allStages
175
+ loose: true
176
+ data:
177
+ allStages:
178
+ - label: "Discovery"
179
+ value: "12"
180
+ - label: "Negotiation"
181
+ value: "13"
182
+ - label: "Closed won"
183
+ value: "14"
184
+ deal:
185
+ data:
186
+ stage: 13
187
+
188
+ - type: RjBuildDescriber
189
+ title: "Interactive Example: Inside a template, against global data"
190
+ description:
191
+ - type: Markdown
192
+ content: |
193
+ Within a template, `~~.` reaches the global data while `~.` would stay in the template scope.
194
+ toDescribe:
195
+ renderView:
196
+ - load: userCard
197
+ templates:
198
+ userCard:
199
+ type: div
200
+ content:
201
+ - "Admin: "
202
+ - type: LabelFromValue
203
+ dataLocation: ~~.isAdmin
204
+ options:
205
+ - label: "Yes"
206
+ value: true
207
+ - label: "No"
208
+ value: false
209
+ data:
210
+ isAdmin: true
211
+
212
+ - type: Markdown
213
+ content: |
214
+ ## Notes
215
+
216
+ - The first matching option wins; the options list is not deduplicated.
217
+ - When no option matches the value (or the matched option carries no `label`), nothing is displayed — the value itself is not used as a fallback label.
218
+ - An empty value (`null`, `undefined`, `""`) therefore renders nothing, unless an option explicitly declares that value.
219
+
67
220
  ## Limitations
68
- - Only the first matching label is displayed; duplicate values in options are not supported.
69
- - No support for grouped options or nested structures.
70
- - No built-in formatting for the label (use a wrapper if needed).
71
- - If no options are provided, the raw value is shown.
221
+
222
+ - No support for grouped options or nested option structures.
223
+ - No built-in formatting of the label (wrap the component if needed).
224
+ - Multi-value data (an array of values) is not supported: the whole array is compared to each option value.
@@ -262,6 +262,46 @@ const plugins = mergeComponentCollections([
262
262
 
263
263
  The registry must be a **shared singleton**: every `DataSync` that resolves the same `dataSyncGroups` object joins the same groups. Provide one instance, not one per render.
264
264
 
265
+ ## Host-controlled saving (`useDataSyncPolicy`)
266
+
267
+ Some hosts decide *when* the content is saved, instead of each `DataSync`: an editor with a global **Save** button, for example. The same RjBuild can then autosave in one host and wait for the Save button in another, without any change to its YAML.
268
+
269
+ Register a `useDataSyncPolicy` hook under `plugins.hook`. It is called on every render of every `DataSync`, with the resolved store path of the syncable (without the `data.` prefix) and the `DataSync` props, and returns the policy of this syncable:
270
+
271
+ | Key | Type | Description |
272
+ |-----|------|-------------|
273
+ | `manual` | boolean | `true` holds the user edits instead of saving them. Default: `false` (the `mode` of the `DataSync` applies). |
274
+ | `flushSignal` | any | In manual mode, a change of this value saves the held edits. Its value at mount does not save anything. |
275
+ | `onDirty` | function | Called on each held edit, e.g. to enable a Save button or a "leave the page?" guard. |
276
+ | `defaultMergeKey` | function(syncable) | The group identity used when the `DataSync` has no `mergeKey`, e.g. `(syncable) => syncable?.item_url`. Return `null` to leave the syncable alone. |
277
+
278
+ ```js
279
+ import { useContext } from "react";
280
+
281
+ const useDataSyncPolicy = ({ path, props }) => {
282
+ // The host state, e.g. from a React context provided by the editor.
283
+ const editor = useContext(EditorContext);
284
+
285
+ return {
286
+ manual: Boolean(editor?.editing) && path === "content",
287
+ flushSignal: editor?.saveCount,
288
+ onDirty: editor?.markDirty,
289
+ defaultMergeKey: (syncable) => syncable?.item_url,
290
+ };
291
+ };
292
+
293
+ const plugins = mergeComponentCollections([{ hook: { useDataSyncPolicy } }]);
294
+ ```
295
+
296
+ Behavior in manual mode:
297
+ - An edit is held and `onDirty` is called. The `trigger` property still saves immediately.
298
+ - When `flushSignal` changes, each `DataSync` holding edits saves them once. In a `mergeKey` group, only the owner writes, with the latest shared data.
299
+ - The edits are still mirrored live to the other members of the group, so that every mount shows the same content, and so that the owner writes the edits made on the other mounts.
300
+
301
+ Without the hook, every `DataSync` follows its own `mode`, and only an explicit `mergeKey` groups syncables. The hook is a plugin: it applies to the roots (and subroots) that receive it, so an app can restrict it to an editor.
302
+
303
+ **Limitation**: in manual mode, the live mirror follows each store change. In a form updated on each keystroke (`TextField`), each keystroke is mirrored to the other members of the group.
304
+
265
305
  ## Using with Templates
266
306
 
267
307
  `DataSync` supports `TemplateContext`, so it can be used inside templates to sync individual items in a list:
@@ -362,6 +402,6 @@ renderView:
362
402
  - Only one sync can be active at a time per `DataSync` instance. Concurrent changes while a sync is in flight are not queued.
363
403
  - The entire Syncable Object is sent in the POST body, not just the `data` field.
364
404
  - Only POST requests are supported for submission. Custom HTTP methods are not configurable.
365
- - `item_url` and `delete_url` are part of the Syncable Object structure but are not used by the component itself. They are available for the backend or for other components to use.
405
+ - `delete_url` is part of the Syncable Object structure but is not used by the component itself. `item_url` is only used to recognize a syncable reloaded from the server (its data is then not posted back).
366
406
  - No built-in support for optimistic updates. The local data is replaced by the server response.
367
407
  - CORS errors are not retried (they are indistinguishable from network errors when the browser is online).
@@ -335,6 +335,46 @@ renderView:
335
335
 
336
336
  The registry must be a **shared singleton**: every `DataSync` that resolves the same `dataSyncGroups` object joins the same groups. Provide one instance, not one per render.
337
337
 
338
+ ## Host-controlled saving (`useDataSyncPolicy`)
339
+
340
+ Some hosts decide *when* the content is saved, instead of each `DataSync`: an editor with a global **Save** button, for example. The same RjBuild can then autosave in one host and wait for the Save button in another, without any change to its YAML.
341
+
342
+ Register a `useDataSyncPolicy` hook under `plugins.hook`. It is called on every render of every `DataSync`, with the resolved store path of the syncable (without the `data.` prefix) and the `DataSync` props, and returns the policy of this syncable:
343
+
344
+ | Key | Type | Description |
345
+ |-----|------|-------------|
346
+ | `manual` | boolean | `true` holds the user edits instead of saving them. Default: `false` (the `mode` of the `DataSync` applies). |
347
+ | `flushSignal` | any | In manual mode, a change of this value saves the held edits. Its value at mount does not save anything. |
348
+ | `onDirty` | function | Called on each held edit, e.g. to enable a Save button or a "leave the page?" guard. |
349
+ | `defaultMergeKey` | function(syncable) | The group identity used when the `DataSync` has no `mergeKey`, e.g. `(syncable) => syncable?.item_url`. Return `null` to leave the syncable alone. |
350
+
351
+ ```js
352
+ import { useContext } from "react";
353
+
354
+ const useDataSyncPolicy = ({ path, props }) => {
355
+ // The host state, e.g. from a React context provided by the editor.
356
+ const editor = useContext(EditorContext);
357
+
358
+ return {
359
+ manual: Boolean(editor?.editing) && path === "content",
360
+ flushSignal: editor?.saveCount,
361
+ onDirty: editor?.markDirty,
362
+ defaultMergeKey: (syncable) => syncable?.item_url,
363
+ };
364
+ };
365
+
366
+ const plugins = mergeComponentCollections([{ hook: { useDataSyncPolicy } }]);
367
+ ```
368
+
369
+ Behavior in manual mode:
370
+ - An edit is held and `onDirty` is called. The `trigger` property still saves immediately.
371
+ - When `flushSignal` changes, each `DataSync` holding edits saves them once. In a `mergeKey` group, only the owner writes, with the latest shared data.
372
+ - The edits are still mirrored live to the other members of the group, so that every mount shows the same content, and so that the owner writes the edits made on the other mounts.
373
+
374
+ Without the hook, every `DataSync` follows its own `mode`, and only an explicit `mergeKey` groups syncables. The hook is a plugin: it applies to the roots (and subroots) that receive it, so an app can restrict it to an editor.
375
+
376
+ **Limitation**: in manual mode, the live mirror follows each store change. In a form updated on each keystroke (`TextField`), each keystroke is mirrored to the other members of the group.
377
+
338
378
  ## Using with Templates
339
379
 
340
380
  `DataSync` supports `TemplateContext`, so it can be used inside templates to sync individual items in a list:
@@ -382,6 +422,6 @@ renderView:
382
422
  - Only one sync can be active at a time per `DataSync` instance. Concurrent changes while a sync is in flight are not queued.
383
423
  - The entire Syncable Object is sent in the POST body, not just the `data` field.
384
424
  - Only POST requests are supported for submission. Custom HTTP methods are not configurable.
385
- - `item_url` and `delete_url` are part of the Syncable Object structure but are not used by the component itself. They are available for the backend or for other components to use.
425
+ - `delete_url` is part of the Syncable Object structure but is not used by the component itself. `item_url` is only used to recognize a syncable reloaded from the server (its data is then not posted back).
386
426
  - No built-in support for optimistic updates. The local data is replaced by the server response.
387
427
  - CORS errors are not retried (they are indistinguishable from network errors when the browser is online).
@@ -146,13 +146,22 @@ Use the `for` attribute to associate labels with form elements for accessibility
146
146
  ```
147
147
 
148
148
  ### Radio Buttons
149
+
150
+ Each radio must be checked only when the stored value is its own. `checked: ~.selectedSize` would check every radio as soon as a size is selected, so compare the value with an attribute transform:
151
+
149
152
  ```yaml
150
153
  - type: input
151
154
  attributes:
152
155
  type: "radio"
153
156
  name: "size"
154
157
  value: "small"
155
- checked: ~.selectedSize
158
+ attributeTransforms:
159
+ - what: setAttributeValue
160
+ name: checked
161
+ mode: replace
162
+ value: true
163
+ when: ~.selectedSize
164
+ is: "small"
156
165
  actions:
157
166
  - what: setData
158
167
  on: change
@@ -164,7 +173,13 @@ Use the `for` attribute to associate labels with form elements for accessibility
164
173
  type: "radio"
165
174
  name: "size"
166
175
  value: "large"
167
- checked: ~.selectedSize
176
+ attributeTransforms:
177
+ - what: setAttributeValue
178
+ name: checked
179
+ mode: replace
180
+ value: true
181
+ when: ~.selectedSize
182
+ is: "large"
168
183
  actions:
169
184
  - what: setData
170
185
  on: change
@@ -172,6 +187,8 @@ Use the `for` attribute to associate labels with form elements for accessibility
172
187
  value: "large"
173
188
  ```
174
189
 
190
+ A checkbox or radio without a `checked` binding (attribute or attribute transform) is forced to unchecked and never shows a user's click. [CheckBoxField](../element/form/CheckBoxField.md) with `controlType: radio` handles all of this by itself.
191
+
175
192
  ## Advanced Patterns
176
193
 
177
194
  ### Form Validation with Conditional Actions
@@ -12,6 +12,9 @@ Hooks in Reactive-JSON are reusable React functions that encapsulate common comp
12
12
  ### Attribute Processing
13
13
  - **[useTransformedAttributes](./useTransformedAttributes.md)**: Applies attribute transformations based on configured transformers
14
14
 
15
+ ### Hooks provided by the application
16
+ - **useDataSyncPolicy**: Decides how each `DataSync` saves (automatic or held until a flush signal) and how the syncables are grouped. The core has no default implementation: without it, every `DataSync` follows its own `mode`. See [DataSync: Host-controlled saving](../element/special/DataSync.md#host-controlled-saving-usedatasyncpolicy).
17
+
15
18
  ## Architecture
16
19
 
17
20
  Reactive-JSON hooks follow these core principles:
@@ -15,6 +15,9 @@ renderView:
15
15
  ### Attribute Processing
16
16
  - **[useTransformedAttributes](./useTransformedAttributes)**: Applies attribute transformations based on configured transformers
17
17
 
18
+ ### Hooks provided by the application
19
+ - **useDataSyncPolicy**: Decides how each `DataSync` saves (automatic or held until a flush signal) and how the syncables are grouped. The core has no default implementation: without it, every `DataSync` follows its own `mode`. See *Host-controlled saving* in [DataSync](../element/special/DataSync).
20
+
18
21
  ## Architecture
19
22
 
20
23
  Reactive-JSON hooks follow these core principles:
@@ -0,0 +1,51 @@
1
+ # Reaction: collectField
2
+
3
+ The `collectField` reaction writes the values of one field of every item of a collection, as an array. Typical use: a "select all" checkbox that writes the identifiers of the listed items.
4
+
5
+ ## Properties
6
+
7
+ - `from` (string, required): The collection (template syntax), an array of objects.
8
+ - `field` (string, required): The name of the field to read in each item. It is evaluated.
9
+ - `path` (string, required): The location receiving the array (template syntax).
10
+
11
+ ## Behavior
12
+
13
+ - The array of the `field` values is written at `path`, in the order of the collection.
14
+ - The items that are not objects, and the items whose field is `null`, `undefined` or an empty string, are skipped.
15
+ - A collection that is not an array writes an empty array.
16
+ - The collection is read from the complete data, even inside a `DataFilter`.
17
+
18
+ ## Example
19
+
20
+ ### Select all / deselect all
21
+
22
+ ```yaml
23
+ renderView:
24
+ - type: button
25
+ content: "Select all"
26
+ actions:
27
+ - what: collectField
28
+ on: click
29
+ from: ~~.results
30
+ field: nid
31
+ path: ~~.selection
32
+ - type: button
33
+ content: "Deselect all"
34
+ actions:
35
+ - what: setData
36
+ on: click
37
+ path: ~~.selection
38
+ value: []
39
+
40
+ data:
41
+ results:
42
+ - nid: 4
43
+ title: "First"
44
+ - nid: 9
45
+ title: "Second"
46
+ selection: []
47
+ ```
48
+
49
+ ## Limitations
50
+
51
+ - Only a direct field of the items is read (no nested path such as `author.id`).