@ea-lab/reactive-json-docs 1.3.0 → 1.4.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/attributeTransformer/setAttributeValue.md +107 -14
- package/public/rjbuild/docs/core/attributeTransformer/setAttributeValue.yaml +124 -12
- package/public/rjbuild/docs/core/example/DataFilter-example-direct-array.md +211 -0
- package/public/rjbuild/docs/core/example/DataFilter-example-direct-array.yaml +323 -0
- package/public/rjbuild/docs/core/example/bulk-actions.yaml +2 -2
- package/public/rjbuild/docs/core/example/button-wrapper-pattern.md +99 -0
- package/public/rjbuild/docs/core/example/button-wrapper-pattern.yaml +132 -0
- package/public/rjbuild/docs/core/example/conditional-field-with-dual-update.md +209 -0
- package/public/rjbuild/docs/core/example/conditional-field-with-dual-update.yaml +74 -0
|
@@ -0,0 +1,323 @@
|
|
|
1
|
+
renderView:
|
|
2
|
+
- type: Markdown
|
|
3
|
+
content: |
|
|
4
|
+
# DataFilter Example: Filtering Direct Array Items
|
|
5
|
+
|
|
6
|
+
This example demonstrates how to use `DataFilter` with arrays where each item is a direct object (not wrapped in a namespace property). This is useful when working with data from APIs that return arrays of objects directly.
|
|
7
|
+
|
|
8
|
+
## Use Case
|
|
9
|
+
|
|
10
|
+
When your data structure is a direct array of objects like:
|
|
11
|
+
```yaml
|
|
12
|
+
data:
|
|
13
|
+
rows:
|
|
14
|
+
- id: 1
|
|
15
|
+
label: "Operation 1"
|
|
16
|
+
done: "done"
|
|
17
|
+
operation: "create"
|
|
18
|
+
- id: 2
|
|
19
|
+
label: "Operation 2"
|
|
20
|
+
done: "pending"
|
|
21
|
+
operation: "update"
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
Instead of:
|
|
25
|
+
```yaml
|
|
26
|
+
data:
|
|
27
|
+
rows:
|
|
28
|
+
- item:
|
|
29
|
+
id: 1
|
|
30
|
+
label: "Operation 1"
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
## Solution
|
|
34
|
+
|
|
35
|
+
Use an existing property that is present in all items (like `id`) as the `subjectsWithProperty` namespace. In `whenFilterableData`, reference properties directly without the namespace prefix.
|
|
36
|
+
|
|
37
|
+
**Important**: When using string values for filtering (like status), use a special value like `"all"` to represent "no filter" instead of an empty string, as empty strings can cause issues with select elements.
|
|
38
|
+
|
|
39
|
+
- type: RjBuildDescriber
|
|
40
|
+
title: Complete Example
|
|
41
|
+
description: |
|
|
42
|
+
Example showing how to filter a direct array using DataFilter with string-based select filters and text search filters.
|
|
43
|
+
toDescribe:
|
|
44
|
+
renderView:
|
|
45
|
+
- type: div
|
|
46
|
+
attributes:
|
|
47
|
+
class: "mb-3"
|
|
48
|
+
content:
|
|
49
|
+
- type: div
|
|
50
|
+
attributes:
|
|
51
|
+
class: "mb-2"
|
|
52
|
+
content:
|
|
53
|
+
- type: label
|
|
54
|
+
attributes:
|
|
55
|
+
style:
|
|
56
|
+
display: "block"
|
|
57
|
+
marginBottom: "4px"
|
|
58
|
+
content: "Filter by Status:"
|
|
59
|
+
- type: select
|
|
60
|
+
attributes:
|
|
61
|
+
value: ~~._filters.done
|
|
62
|
+
style:
|
|
63
|
+
padding: "4px 8px"
|
|
64
|
+
border: "1px solid #ccc"
|
|
65
|
+
borderRadius: "4px"
|
|
66
|
+
content:
|
|
67
|
+
- type: option
|
|
68
|
+
attributes:
|
|
69
|
+
value: "all"
|
|
70
|
+
content: "All"
|
|
71
|
+
- type: option
|
|
72
|
+
attributes:
|
|
73
|
+
value: "done"
|
|
74
|
+
content: "Done"
|
|
75
|
+
- type: option
|
|
76
|
+
attributes:
|
|
77
|
+
value: "pending"
|
|
78
|
+
content: "Pending"
|
|
79
|
+
actions:
|
|
80
|
+
- what: setData
|
|
81
|
+
on: change
|
|
82
|
+
path: ~~._filters.done
|
|
83
|
+
value: <reactive-json:event-new-value>
|
|
84
|
+
- type: div
|
|
85
|
+
attributes:
|
|
86
|
+
class: "mb-2"
|
|
87
|
+
content:
|
|
88
|
+
- type: label
|
|
89
|
+
attributes:
|
|
90
|
+
style:
|
|
91
|
+
display: "block"
|
|
92
|
+
marginBottom: "4px"
|
|
93
|
+
content: "Filter by Operation:"
|
|
94
|
+
- type: input
|
|
95
|
+
attributes:
|
|
96
|
+
type: "text"
|
|
97
|
+
value: ~~._filters.operation
|
|
98
|
+
placeholder: "Enter operation type"
|
|
99
|
+
style:
|
|
100
|
+
padding: "4px 8px"
|
|
101
|
+
border: "1px solid #ccc"
|
|
102
|
+
borderRadius: "4px"
|
|
103
|
+
width: "100%"
|
|
104
|
+
actions:
|
|
105
|
+
- what: setData
|
|
106
|
+
on: input
|
|
107
|
+
path: ~~._filters.operation
|
|
108
|
+
value: <reactive-json:event-new-value>
|
|
109
|
+
- type: div
|
|
110
|
+
attributes:
|
|
111
|
+
class: "mb-2"
|
|
112
|
+
content:
|
|
113
|
+
- type: label
|
|
114
|
+
attributes:
|
|
115
|
+
style:
|
|
116
|
+
display: "block"
|
|
117
|
+
marginBottom: "4px"
|
|
118
|
+
content: "Search Label:"
|
|
119
|
+
- type: input
|
|
120
|
+
attributes:
|
|
121
|
+
type: "text"
|
|
122
|
+
value: ~~._filters.label
|
|
123
|
+
placeholder: "Search in labels"
|
|
124
|
+
style:
|
|
125
|
+
padding: "4px 8px"
|
|
126
|
+
border: "1px solid #ccc"
|
|
127
|
+
borderRadius: "4px"
|
|
128
|
+
width: "100%"
|
|
129
|
+
actions:
|
|
130
|
+
- what: setData
|
|
131
|
+
on: input
|
|
132
|
+
path: ~~._filters.label
|
|
133
|
+
value: <reactive-json:event-new-value>
|
|
134
|
+
- type: table
|
|
135
|
+
attributes:
|
|
136
|
+
class: "table table-striped"
|
|
137
|
+
content:
|
|
138
|
+
- type: thead
|
|
139
|
+
content:
|
|
140
|
+
- type: tr
|
|
141
|
+
content:
|
|
142
|
+
- type: th
|
|
143
|
+
content: "ID"
|
|
144
|
+
- type: th
|
|
145
|
+
content: "Label"
|
|
146
|
+
- type: th
|
|
147
|
+
content: "Status"
|
|
148
|
+
- type: th
|
|
149
|
+
content: "Operation"
|
|
150
|
+
- type: tbody
|
|
151
|
+
content:
|
|
152
|
+
- type: DataFilter
|
|
153
|
+
context: global
|
|
154
|
+
filters:
|
|
155
|
+
- subjectsWithProperty: id
|
|
156
|
+
andConditions:
|
|
157
|
+
# Filter by status (string)
|
|
158
|
+
- orConditions:
|
|
159
|
+
- when: ~~._filters.done
|
|
160
|
+
is: "all"
|
|
161
|
+
- whenFilterableData: done
|
|
162
|
+
is: ~~._filters.done
|
|
163
|
+
|
|
164
|
+
# Filter by operation type (text search with contains)
|
|
165
|
+
- orConditions:
|
|
166
|
+
- when: ~~._filters.operation
|
|
167
|
+
is: ""
|
|
168
|
+
- andConditions:
|
|
169
|
+
- whenFilterableData: operation
|
|
170
|
+
isNotEmpty: true
|
|
171
|
+
- when: ~~._filters.operation
|
|
172
|
+
isNotEmpty: true
|
|
173
|
+
- whenFilterableData: operation
|
|
174
|
+
contains: ~~._filters.operation
|
|
175
|
+
|
|
176
|
+
# Filter by label (text search with contains)
|
|
177
|
+
- orConditions:
|
|
178
|
+
- when: ~~._filters.label
|
|
179
|
+
is: ""
|
|
180
|
+
- andConditions:
|
|
181
|
+
- whenFilterableData: label
|
|
182
|
+
isNotEmpty: true
|
|
183
|
+
- when: ~~._filters.label
|
|
184
|
+
isNotEmpty: true
|
|
185
|
+
- whenFilterableData: label
|
|
186
|
+
contains: ~~._filters.label
|
|
187
|
+
content:
|
|
188
|
+
- type: Switch
|
|
189
|
+
content: ~~.rows
|
|
190
|
+
singleOption:
|
|
191
|
+
load: operationRow
|
|
192
|
+
|
|
193
|
+
templates:
|
|
194
|
+
operationRow:
|
|
195
|
+
- type: tr
|
|
196
|
+
content:
|
|
197
|
+
- type: td
|
|
198
|
+
content: ~.id
|
|
199
|
+
- type: td
|
|
200
|
+
content: ~.label
|
|
201
|
+
- type: td
|
|
202
|
+
content:
|
|
203
|
+
- type: span
|
|
204
|
+
attributes:
|
|
205
|
+
class: "badge"
|
|
206
|
+
style:
|
|
207
|
+
backgroundColor: "#28a745"
|
|
208
|
+
color: "white"
|
|
209
|
+
padding: "4px 8px"
|
|
210
|
+
borderRadius: "4px"
|
|
211
|
+
content: "Done"
|
|
212
|
+
actions:
|
|
213
|
+
- what: hide
|
|
214
|
+
when: ~.done
|
|
215
|
+
isNot: "done"
|
|
216
|
+
- type: span
|
|
217
|
+
attributes:
|
|
218
|
+
class: "badge"
|
|
219
|
+
style:
|
|
220
|
+
backgroundColor: "#ffc107"
|
|
221
|
+
color: "white"
|
|
222
|
+
padding: "4px 8px"
|
|
223
|
+
borderRadius: "4px"
|
|
224
|
+
content: "Pending"
|
|
225
|
+
actions:
|
|
226
|
+
- what: hide
|
|
227
|
+
when: ~.done
|
|
228
|
+
isNot: "pending"
|
|
229
|
+
- type: td
|
|
230
|
+
content: ~.operation
|
|
231
|
+
|
|
232
|
+
data:
|
|
233
|
+
rows:
|
|
234
|
+
- id: 1
|
|
235
|
+
label: "Operation 1"
|
|
236
|
+
done: "done"
|
|
237
|
+
operation: "create"
|
|
238
|
+
- id: 2
|
|
239
|
+
label: "Operation 2"
|
|
240
|
+
done: "pending"
|
|
241
|
+
operation: "update"
|
|
242
|
+
- id: 3
|
|
243
|
+
label: "Operation 3"
|
|
244
|
+
done: "done"
|
|
245
|
+
operation: "create"
|
|
246
|
+
_filters:
|
|
247
|
+
done: "all"
|
|
248
|
+
label: ""
|
|
249
|
+
operation: ""
|
|
250
|
+
|
|
251
|
+
- type: Markdown
|
|
252
|
+
content: |
|
|
253
|
+
## Key Points
|
|
254
|
+
|
|
255
|
+
1. **Namespace Selection**: Choose a property that exists in all items (e.g., `id`, `name`, `key`). This property acts as the identifier for DataFilter to recognize filterable items.
|
|
256
|
+
|
|
257
|
+
2. **Direct Property Access**: In `whenFilterableData`, reference properties directly:
|
|
258
|
+
- ✅ `whenFilterableData: label` (correct)
|
|
259
|
+
- ❌ `whenFilterableData: id.label` (incorrect - don't use namespace prefix)
|
|
260
|
+
|
|
261
|
+
3. **String-based Filtering**: When using select elements for filtering, use a special value like `"all"` to represent "no filter" instead of an empty string:
|
|
262
|
+
```yaml
|
|
263
|
+
- orConditions:
|
|
264
|
+
- when: ~~._filters.done
|
|
265
|
+
is: "all" # Shows all items
|
|
266
|
+
- whenFilterableData: done
|
|
267
|
+
is: ~~._filters.done # Filters by exact match
|
|
268
|
+
```
|
|
269
|
+
This avoids issues where select elements might not properly handle empty string values.
|
|
270
|
+
|
|
271
|
+
4. **Text Search**: For text search with `contains`, ensure both the filter value and the data property are not empty:
|
|
272
|
+
```yaml
|
|
273
|
+
- orConditions:
|
|
274
|
+
- when: ~~._filters.label
|
|
275
|
+
is: "" # Shows all when empty
|
|
276
|
+
- andConditions:
|
|
277
|
+
- whenFilterableData: label
|
|
278
|
+
isNotEmpty: true
|
|
279
|
+
- when: ~~._filters.label
|
|
280
|
+
isNotEmpty: true
|
|
281
|
+
- whenFilterableData: label
|
|
282
|
+
contains: ~~._filters.label
|
|
283
|
+
```
|
|
284
|
+
The `andConditions` wrapper ensures that both the data property and filter value are not empty before attempting the `contains` comparison.
|
|
285
|
+
|
|
286
|
+
5. **Template Access**: In templates, access properties directly without namespace:
|
|
287
|
+
- ✅ `~.label`, `~.done`, `~.operation`
|
|
288
|
+
- ❌ `~.id.label` (incorrect)
|
|
289
|
+
|
|
290
|
+
## Filter Types Demonstrated
|
|
291
|
+
|
|
292
|
+
- **Select Filter with "All" option**: Using `is: "all"` to show all items when a special "all" value is selected
|
|
293
|
+
- **Exact String Match**: Using `is` for exact string matching (e.g., status filtering)
|
|
294
|
+
- **Text Search**: Using `contains` for substring matching (case-insensitive) with proper empty checks
|
|
295
|
+
|
|
296
|
+
## Additional Template Techniques
|
|
297
|
+
|
|
298
|
+
**Conditional Display in Templates**: The example uses `hide` actions to conditionally display badges based on data values. This is a common pattern for showing different UI elements based on data state, but it's not part of the DataFilter pattern itself - it's just a way to enhance the visual display of filtered data:
|
|
299
|
+
|
|
300
|
+
```yaml
|
|
301
|
+
- type: span
|
|
302
|
+
content: "Done"
|
|
303
|
+
actions:
|
|
304
|
+
- what: hide
|
|
305
|
+
when: ~.done
|
|
306
|
+
isNot: "done" # Hide if status is not "done"
|
|
307
|
+
- type: span
|
|
308
|
+
content: "Pending"
|
|
309
|
+
actions:
|
|
310
|
+
- what: hide
|
|
311
|
+
when: ~.done
|
|
312
|
+
isNot: "pending" # Hide if status is not "pending"
|
|
313
|
+
```
|
|
314
|
+
|
|
315
|
+
## Notes
|
|
316
|
+
|
|
317
|
+
- The `subjectsWithProperty` value (`id` in this example) must exist in every item of the array
|
|
318
|
+
- DataFilter filters the data before rendering, so no `hide` actions are needed in templates for filtering (but they can be used for conditional display within templates)
|
|
319
|
+
- All filter conditions use `orConditions` with an empty/"all" check first, allowing "show all" when filters are empty or set to "all"
|
|
320
|
+
- For select elements, prefer using a special value like `"all"` instead of empty strings to avoid UI issues
|
|
321
|
+
- For text inputs, empty strings (`""`) work fine for the "show all" condition
|
|
322
|
+
- When using `contains` for text search, always wrap the condition in `andConditions` with `isNotEmpty` checks to avoid errors with empty values
|
|
323
|
+
|
|
@@ -256,7 +256,7 @@ renderView:
|
|
|
256
256
|
fileRow:
|
|
257
257
|
- type: tr
|
|
258
258
|
attributes:
|
|
259
|
-
class: "
|
|
259
|
+
class: "border-b"
|
|
260
260
|
actions:
|
|
261
261
|
- what: setAttributeValue
|
|
262
262
|
name: "data-selected"
|
|
@@ -416,7 +416,7 @@ renderView:
|
|
|
416
416
|
content: |
|
|
417
417
|
- type: tr
|
|
418
418
|
attributes:
|
|
419
|
-
class: "
|
|
419
|
+
class: "border-b"
|
|
420
420
|
actions:
|
|
421
421
|
- what: setAttributeValue
|
|
422
422
|
name: "class"
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
# Adding Behaviors to Reusable Components via Wrapper Pattern
|
|
2
|
+
|
|
3
|
+
This pattern demonstrates how to add behaviors (actions, event handlers, conditional logic) to reusable components loaded via `ReactiveJsonSubroot` without modifying the component itself. This is particularly useful when you need to attach context-specific behaviors to shared components.
|
|
4
|
+
|
|
5
|
+
## Why Use This Pattern?
|
|
6
|
+
|
|
7
|
+
When working with reusable components loaded via `ReactiveJsonSubroot`, you might encounter situations where:
|
|
8
|
+
|
|
9
|
+
- **The component is shared**: You can't modify it directly because it's used in multiple places
|
|
10
|
+
- **Context-specific behavior needed**: Different instances need different behaviors (click handlers, data updates, conditional logic)
|
|
11
|
+
- **Separation of concerns**: You want to keep the component generic and add behavior at the usage site
|
|
12
|
+
|
|
13
|
+
The wrapper pattern solves this by wrapping the component in an element that can handle events and execute actions, while the component itself remains unchanged.
|
|
14
|
+
|
|
15
|
+
## How It Works
|
|
16
|
+
|
|
17
|
+
The pattern works by leveraging **event propagation** in the DOM:
|
|
18
|
+
|
|
19
|
+
1. **Wrapper element**: A DOM element (like `div`) wraps the component and has actions attached
|
|
20
|
+
2. **Component inside**: The reusable component is loaded via `ReactiveJsonSubroot` inside the wrapper
|
|
21
|
+
3. **Event bubbling**: When a user interacts with the component (e.g., clicks a button), the event bubbles up to the wrapper
|
|
22
|
+
4. **Action execution**: The wrapper's actions execute, allowing you to add behavior without modifying the component
|
|
23
|
+
|
|
24
|
+
**Important**: This works because DOM events naturally bubble up from child elements to parent elements. The wrapper captures these events and executes its actions.
|
|
25
|
+
|
|
26
|
+
## Example: Adding Click Behavior to a Button Component
|
|
27
|
+
|
|
28
|
+
In this example, we have a reusable button component that we want to use in multiple places. Instead of modifying the button component itself, we wrap it and add the behavior we need:
|
|
29
|
+
|
|
30
|
+
```yaml
|
|
31
|
+
renderView:
|
|
32
|
+
- type: div
|
|
33
|
+
actions:
|
|
34
|
+
- what: setData
|
|
35
|
+
on: click
|
|
36
|
+
path: ~~.lastAction
|
|
37
|
+
value: "Button was clicked!"
|
|
38
|
+
content:
|
|
39
|
+
- type: ReactiveJsonSubroot
|
|
40
|
+
rjOptions:
|
|
41
|
+
maybeRawAppRjBuild:
|
|
42
|
+
renderView:
|
|
43
|
+
- type: button
|
|
44
|
+
attributes:
|
|
45
|
+
style: ~~.buttonStyle
|
|
46
|
+
content: ~~.content
|
|
47
|
+
dataOverride:
|
|
48
|
+
content: "Click me"
|
|
49
|
+
buttonStyle:
|
|
50
|
+
color: "white"
|
|
51
|
+
backgroundColor: "#2563eb"
|
|
52
|
+
padding: "8px 16px"
|
|
53
|
+
border: "none"
|
|
54
|
+
borderRadius: "4px"
|
|
55
|
+
cursor: "pointer"
|
|
56
|
+
|
|
57
|
+
data:
|
|
58
|
+
lastAction: "No action yet"
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
## Key Concepts
|
|
62
|
+
|
|
63
|
+
### Event Propagation
|
|
64
|
+
|
|
65
|
+
When a user interacts with the button (clicks it), the click event bubbles up through the DOM hierarchy. The wrapper `div` captures this event and executes its actions. This is why the wrapper pattern works - it leverages the natural behavior of DOM events.
|
|
66
|
+
|
|
67
|
+
### Component Independence
|
|
68
|
+
|
|
69
|
+
The button component inside the wrapper doesn't need to know about the wrapper's actions. It remains a generic, reusable component. The behavior is added at the usage site, not in the component definition.
|
|
70
|
+
|
|
71
|
+
### DataOverride for Customization
|
|
72
|
+
|
|
73
|
+
Use `dataOverride` to pass different props to the component for different instances. This allows the same component to be customized without modification.
|
|
74
|
+
|
|
75
|
+
## When to Use This Pattern
|
|
76
|
+
|
|
77
|
+
This pattern is ideal when:
|
|
78
|
+
|
|
79
|
+
- ✅ You have reusable components that need different behaviors in different contexts
|
|
80
|
+
- ✅ You want to add event handlers without modifying the component
|
|
81
|
+
- ✅ You need to attach conditional logic or data updates to shared components
|
|
82
|
+
- ✅ You want to keep components generic and add behavior at the usage site
|
|
83
|
+
|
|
84
|
+
**Not suitable when**:
|
|
85
|
+
- ❌ The component itself needs to handle the event (attach actions directly to the component)
|
|
86
|
+
- ❌ You need to prevent event propagation (use `stopPropagation` in actions if needed)
|
|
87
|
+
- ❌ You're using `Phantom` wrapper for DOM events (it doesn't render DOM, so events can't bubble to it)
|
|
88
|
+
|
|
89
|
+
**Note about Phantom**: While `Phantom` cannot handle DOM events, it can still be useful for other purposes like passing values via `dataOverride`, applying non-event actions (tooltips, conditional logic), or when you only need to customize component appearance without event handling.
|
|
90
|
+
|
|
91
|
+
## Limitations and Considerations
|
|
92
|
+
|
|
93
|
+
- **DOM element required for events**: The wrapper must be a DOM element (like `div`) for DOM events to bubble. `Phantom` won't work for DOM events since it doesn't render anything in the DOM. However, `Phantom` can still be useful in other scenarios:
|
|
94
|
+
- **Passing values**: When you only need to pass data via `dataOverride` without event handling
|
|
95
|
+
- **Styling/esthetics**: When you want to apply actions that don't require DOM events (like tooltips, conditional logic, or data manipulations)
|
|
96
|
+
- **Non-event actions**: For actions that don't rely on DOM event listeners
|
|
97
|
+
- **Event propagation**: Events bubble up naturally. If you need to stop propagation, you can use `stopPropagation: true` in your actions.
|
|
98
|
+
- **No sharedUpdates needed**: If the component doesn't need to update parent data, `sharedUpdates` is optional. Only use it if the component needs to modify data that the parent is watching.
|
|
99
|
+
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
renderView:
|
|
2
|
+
- type: Markdown
|
|
3
|
+
content: |
|
|
4
|
+
# Adding Behaviors to Reusable Components via Wrapper Pattern
|
|
5
|
+
|
|
6
|
+
This pattern demonstrates how to add behaviors (actions, event handlers, conditional logic) to reusable components loaded via `ReactiveJsonSubroot` without modifying the component itself. This is particularly useful when you need to attach context-specific behaviors to shared components.
|
|
7
|
+
|
|
8
|
+
- type: Markdown
|
|
9
|
+
content: |
|
|
10
|
+
## Why Use This Pattern?
|
|
11
|
+
|
|
12
|
+
When working with reusable components loaded via `ReactiveJsonSubroot`, you might encounter situations where:
|
|
13
|
+
|
|
14
|
+
- **The component is shared**: You can't modify it directly because it's used in multiple places
|
|
15
|
+
- **Context-specific behavior needed**: Different instances need different behaviors (click handlers, data updates, conditional logic)
|
|
16
|
+
- **Separation of concerns**: You want to keep the component generic and add behavior at the usage site
|
|
17
|
+
|
|
18
|
+
The wrapper pattern solves this by wrapping the component in an element that can handle events and execute actions, while the component itself remains unchanged.
|
|
19
|
+
|
|
20
|
+
- type: Markdown
|
|
21
|
+
content: |
|
|
22
|
+
## How It Works
|
|
23
|
+
|
|
24
|
+
The pattern works by leveraging **event propagation** in the DOM:
|
|
25
|
+
|
|
26
|
+
1. **Wrapper element**: A DOM element (like `div`) wraps the component and has actions attached
|
|
27
|
+
2. **Component inside**: The reusable component is loaded via `ReactiveJsonSubroot` inside the wrapper
|
|
28
|
+
3. **Event bubbling**: When a user interacts with the component (e.g., clicks a button), the event bubbles up to the wrapper
|
|
29
|
+
4. **Action execution**: The wrapper's actions execute, allowing you to add behavior without modifying the component
|
|
30
|
+
|
|
31
|
+
**Important**: This works because DOM events naturally bubble up from child elements to parent elements. The wrapper captures these events and executes its actions.
|
|
32
|
+
|
|
33
|
+
- type: Markdown
|
|
34
|
+
content: |
|
|
35
|
+
## Example: Adding Click Behavior to a Button Component
|
|
36
|
+
|
|
37
|
+
In this example, we have a reusable button component that we want to use in multiple places. Instead of modifying the button component itself, we wrap it and add the behavior we need.
|
|
38
|
+
|
|
39
|
+
- type: RjBuildDescriber
|
|
40
|
+
title: "Example: Button with wrapper actions"
|
|
41
|
+
description: |
|
|
42
|
+
A reusable button component is wrapped in a div that adds click behavior. When the button is clicked, the wrapper's action executes, updating data without modifying the button component.
|
|
43
|
+
toDescribe:
|
|
44
|
+
renderView:
|
|
45
|
+
- type: div
|
|
46
|
+
attributes:
|
|
47
|
+
style:
|
|
48
|
+
display: "inline-block"
|
|
49
|
+
actions:
|
|
50
|
+
- what: setData
|
|
51
|
+
on: click
|
|
52
|
+
path: ~~.lastAction
|
|
53
|
+
value: "Button was clicked!"
|
|
54
|
+
content:
|
|
55
|
+
- type: ReactiveJsonSubroot
|
|
56
|
+
rjOptions:
|
|
57
|
+
maybeRawAppRjBuild:
|
|
58
|
+
renderView:
|
|
59
|
+
- type: button
|
|
60
|
+
attributes:
|
|
61
|
+
style: ~~.buttonStyle
|
|
62
|
+
content: ~~.content
|
|
63
|
+
dataOverride:
|
|
64
|
+
content: "Click me"
|
|
65
|
+
buttonStyle:
|
|
66
|
+
color: "white"
|
|
67
|
+
backgroundColor: "#2563eb"
|
|
68
|
+
padding: "8px 16px"
|
|
69
|
+
border: "none"
|
|
70
|
+
borderRadius: "4px"
|
|
71
|
+
cursor: "pointer"
|
|
72
|
+
- type: div
|
|
73
|
+
attributes:
|
|
74
|
+
style:
|
|
75
|
+
marginTop: "16px"
|
|
76
|
+
padding: "8px"
|
|
77
|
+
borderRadius: "4px"
|
|
78
|
+
content:
|
|
79
|
+
- type: span
|
|
80
|
+
content: ~~.lastAction
|
|
81
|
+
data:
|
|
82
|
+
lastAction: "No action yet"
|
|
83
|
+
|
|
84
|
+
- type: Markdown
|
|
85
|
+
content: |
|
|
86
|
+
## Key Concepts
|
|
87
|
+
|
|
88
|
+
### Event Propagation
|
|
89
|
+
|
|
90
|
+
When a user interacts with the button (clicks it), the click event bubbles up through the DOM hierarchy. The wrapper `div` captures this event and executes its actions. This is why the wrapper pattern works - it leverages the natural behavior of DOM events.
|
|
91
|
+
|
|
92
|
+
### Component Independence
|
|
93
|
+
|
|
94
|
+
The button component inside the wrapper doesn't need to know about the wrapper's actions. It remains a generic, reusable component. The behavior is added at the usage site, not in the component definition.
|
|
95
|
+
|
|
96
|
+
### DataOverride for Customization
|
|
97
|
+
|
|
98
|
+
Use `dataOverride` to pass different props to the component for different instances. This allows the same component to be customized without modification.
|
|
99
|
+
|
|
100
|
+
- type: Markdown
|
|
101
|
+
content: |
|
|
102
|
+
## When to Use This Pattern
|
|
103
|
+
|
|
104
|
+
This pattern is ideal when:
|
|
105
|
+
|
|
106
|
+
- ✅ You have reusable components that need different behaviors in different contexts
|
|
107
|
+
- ✅ You want to add event handlers without modifying the component
|
|
108
|
+
- ✅ You need to attach conditional logic or data updates to shared components
|
|
109
|
+
- ✅ You want to keep components generic and add behavior at the usage site
|
|
110
|
+
|
|
111
|
+
**Not suitable when**:
|
|
112
|
+
- ❌ The component itself needs to handle the event (attach actions directly to the component)
|
|
113
|
+
- ❌ You need to prevent event propagation (use `stopPropagation` in actions if needed)
|
|
114
|
+
- ❌ You're using `Phantom` wrapper for DOM events (it doesn't render DOM, so events can't bubble to it)
|
|
115
|
+
|
|
116
|
+
**Note about Phantom**: While `Phantom` cannot handle DOM events, it can still be useful for other purposes like passing values via `dataOverride`, applying non-event actions (tooltips, conditional logic), or when you only need to customize component appearance without event handling.
|
|
117
|
+
|
|
118
|
+
- type: Markdown
|
|
119
|
+
content: |
|
|
120
|
+
## Limitations and Considerations
|
|
121
|
+
|
|
122
|
+
- **DOM element required for events**: The wrapper must be a DOM element (like `div`) for DOM events to bubble. `Phantom` won't work for DOM events since it doesn't render anything in the DOM. However, `Phantom` can still be useful in other scenarios:
|
|
123
|
+
- **Passing values**: When you only need to pass data via `dataOverride` without event handling
|
|
124
|
+
- **Styling/esthetics**: When you want to apply actions that don't require DOM events (like tooltips, conditional logic, or data manipulations)
|
|
125
|
+
- **Non-event actions**: For actions that don't rely on DOM event listeners
|
|
126
|
+
- **Event propagation**: Events bubble up naturally. If you need to stop propagation, you can use `stopPropagation: true` in your actions.
|
|
127
|
+
- **No sharedUpdates needed**: If the component doesn't need to update parent data, `sharedUpdates` is optional. Only use it if the component needs to modify data that the parent is watching.
|
|
128
|
+
|
|
129
|
+
templates:
|
|
130
|
+
|
|
131
|
+
data:
|
|
132
|
+
|