@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.
- package/package.json +2 -2
- package/public/rjbuild/docs/core/element/form/CheckBoxField.md +142 -18
- package/public/rjbuild/docs/core/element/form/CheckBoxField.yaml +132 -7
- package/public/rjbuild/docs/core/element/form/SelectField.md +36 -10
- package/public/rjbuild/docs/core/element/form/SelectField.yaml +37 -3
- package/public/rjbuild/docs/core/element/form/formElementsCommon.md +32 -0
- package/public/rjbuild/docs/core/element/html/Html.yaml +47 -0
- package/public/rjbuild/docs/core/element/html/LabelFromValue.md +159 -0
- package/public/rjbuild/docs/core/element/html/LabelFromValue.yaml +163 -10
- package/public/rjbuild/docs/core/element/special/DataSync.md +41 -1
- package/public/rjbuild/docs/core/element/special/DataSync.yaml +41 -1
- package/public/rjbuild/docs/core/example/native-html-forms.md +19 -2
- package/public/rjbuild/docs/core/hook/index.md +3 -0
- package/public/rjbuild/docs/core/hook/index.yaml +3 -0
- package/public/rjbuild/docs/core/reaction/collectField.md +51 -0
- package/public/rjbuild/docs/core/reaction/collectField.yaml +81 -0
- package/public/rjbuild/docs/core/reaction/fetchData.md +21 -1
- package/public/rjbuild/docs/core/reaction/fetchData.yaml +2 -2
- package/public/rjbuild/docs/core/reaction/index.md +5 -0
- package/public/rjbuild/docs/core/reaction/index.yaml +5 -0
- package/public/rjbuild/docs/core/reaction/joinData.md +51 -0
- package/public/rjbuild/docs/core/reaction/joinData.yaml +87 -0
- package/public/rjbuild/docs/core/reaction/printPage.md +22 -0
- package/public/rjbuild/docs/core/reaction/printPage.yaml +29 -0
- package/public/rjbuild/docs/core/reaction/redirectNow.md +20 -2
- package/public/rjbuild/docs/core/reaction/redirectNow.yaml +24 -3
- package/public/rjbuild/docs/core/reaction/setClipboardData.md +35 -6
- package/public/rjbuild/docs/core/reaction/setClipboardData.yaml +18 -6
- package/public/rjbuild/docs/core/reaction/stringifyData.md +45 -0
- package/public/rjbuild/docs/core/reaction/stringifyData.yaml +58 -0
- package/public/rjbuild/docs/core/reaction/submitData.md +1 -1
- package/public/rjbuild/docs/core/reaction/submitData.yaml +4 -2
- package/public/rjbuild/docs/core/reaction/toggleData.md +72 -0
- package/public/rjbuild/docs/core/reaction/toggleData.yaml +77 -0
- package/public/rjbuild/docs/getting-started/reactions.md +5 -0
- package/public/rjbuild/docs/getting-started/reactions.yaml +5 -0
- package/public/rjbuild/docs/getting-started/rjbuild-structure.md +23 -1
- 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
|
|
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,
|
|
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:
|
|
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: "
|
|
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
|
-
|
|
69
|
-
- No support for grouped options or nested structures.
|
|
70
|
-
- No built-in formatting
|
|
71
|
-
-
|
|
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
|
-
- `
|
|
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
|
-
- `
|
|
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
|
-
|
|
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
|
-
|
|
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`).
|