@ea-lab/reactive-json 0.0.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 (66) hide show
  1. package/README.md +83 -0
  2. package/dist/reactive-json.css +5 -0
  3. package/dist/reactive-json.js +56303 -0
  4. package/dist/reactive-json.umd.cjs +382 -0
  5. package/lib/component/action/HashChangeListener.jsx +66 -0
  6. package/lib/component/action/Hide.jsx +14 -0
  7. package/lib/component/action/MessageListener.jsx +62 -0
  8. package/lib/component/action/Popover.jsx +53 -0
  9. package/lib/component/action/ReactOnEvent.jsx +118 -0
  10. package/lib/component/action/Redirect.jsx +26 -0
  11. package/lib/component/action/Tooltip.jsx +27 -0
  12. package/lib/component/action/VisuallyHide.jsx +15 -0
  13. package/lib/component/element/chart/BarChart.jsx +40 -0
  14. package/lib/component/element/chart/DoughnutChart.jsx +32 -0
  15. package/lib/component/element/chart/LineChart.jsx +40 -0
  16. package/lib/component/element/chart/PolarAreaChart.jsx +32 -0
  17. package/lib/component/element/form/CheckBoxField.jsx +215 -0
  18. package/lib/component/element/form/DateField.jsx +42 -0
  19. package/lib/component/element/form/NumberField.jsx +29 -0
  20. package/lib/component/element/form/SelectField.jsx +130 -0
  21. package/lib/component/element/form/TextAreaField.jsx +48 -0
  22. package/lib/component/element/form/TextField.jsx +65 -0
  23. package/lib/component/element/form/formElementsCommon.jsx +54 -0
  24. package/lib/component/element/html/AccordionItem.jsx +42 -0
  25. package/lib/component/element/html/FolderSortableTree.jsx +307 -0
  26. package/lib/component/element/html/FormatNumeral.jsx +118 -0
  27. package/lib/component/element/html/Html.jsx +107 -0
  28. package/lib/component/element/html/LabelFromValue.jsx +89 -0
  29. package/lib/component/element/html/Modal.jsx +77 -0
  30. package/lib/component/element/html/ModalForm.jsx +30 -0
  31. package/lib/component/element/html/Paragraph.jsx +10 -0
  32. package/lib/component/element/html/PreformattedMarkup.jsx +54 -0
  33. package/lib/component/element/html/SortableTreeItemCollapseButton.jsx +20 -0
  34. package/lib/component/element/html/Tabs.jsx +55 -0
  35. package/lib/component/element/special/BootstrapElement.jsx +32 -0
  36. package/lib/component/element/special/Count.jsx +46 -0
  37. package/lib/component/element/special/DataFilter.jsx +156 -0
  38. package/lib/component/element/special/DelayedActions.jsx +119 -0
  39. package/lib/component/element/special/PageControls.jsx +19 -0
  40. package/lib/component/element/special/Phantom.jsx +25 -0
  41. package/lib/component/element/special/Switch.jsx +131 -0
  42. package/lib/component/hook/usePagination.jsx +184 -0
  43. package/lib/component/reaction/addData.jsx +23 -0
  44. package/lib/component/reaction/fetchData.jsx +83 -0
  45. package/lib/component/reaction/moveData.jsx +52 -0
  46. package/lib/component/reaction/postMessage.jsx +43 -0
  47. package/lib/component/reaction/redirectNow.jsx +17 -0
  48. package/lib/component/reaction/removeData.jsx +48 -0
  49. package/lib/component/reaction/setClipboardData.jsx +20 -0
  50. package/lib/component/reaction/setData.jsx +23 -0
  51. package/lib/component/reaction/submitData.jsx +136 -0
  52. package/lib/component/reaction/triggerEvent.jsx +62 -0
  53. package/lib/component/utility/formatString.jsx +59 -0
  54. package/lib/engine/Actions.jsx +392 -0
  55. package/lib/engine/EventDispatcherContext.jsx +16 -0
  56. package/lib/engine/EventDispatcherProvider.jsx +80 -0
  57. package/lib/engine/GlobalDataContext.jsx +13 -0
  58. package/lib/engine/GlobalDataContextProvider.jsx +33 -0
  59. package/lib/engine/PaginationContext.jsx +10 -0
  60. package/lib/engine/PaginationProvider.jsx +61 -0
  61. package/lib/engine/ReactiveJsonRoot.jsx +315 -0
  62. package/lib/engine/TemplateContext.jsx +13 -0
  63. package/lib/engine/TemplateSystem.jsx +302 -0
  64. package/lib/engine/View.jsx +240 -0
  65. package/lib/main.jsx +41 -0
  66. package/package.json +72 -0
@@ -0,0 +1,136 @@
1
+ import axios from "axios";
2
+ import {evaluateTemplateValue} from "../../engine/TemplateSystem";
3
+
4
+ /**
5
+ * Submits the current state of this app data.
6
+ *
7
+ * Will reload the app content if refreshAppOnResponse is true.
8
+ *
9
+ * @param {{args: {data, httpMethod, refreshAppOnResponse, submitSilently, url}, event, globalDataContext, templateContext}} props Reaction function props.
10
+ */
11
+ export const submitData = (props) => {
12
+ // Prevent multiple submits.
13
+ const reactionEvent = props?.event;
14
+
15
+ // Check in realtime if we are already submitting.
16
+ // With this system, only 1 submit can be made concurrently for all roots.
17
+ const body = document.body;
18
+
19
+ if (body.dataset.htmlBuilderIsSubmitting === "true") {
20
+ return;
21
+ }
22
+
23
+ // This will block any attempts to resubmit until receiving the response.
24
+ body.dataset.htmlBuilderIsSubmitting = "true";
25
+
26
+ if (props?.args?.submitSilently) {
27
+ // This will prevent CSS from visually disabling the fields if true.
28
+ body.dataset.htmlBuilderIsSubmittingSilently = "true";
29
+ } else {
30
+ delete body.dataset.htmlBuilderIsSubmittingSilently;
31
+ }
32
+
33
+ const currentTarget = reactionEvent?.currentTarget;
34
+
35
+ if (currentTarget?.dataset) {
36
+ // Useful for styling.
37
+ currentTarget.dataset.isSubmitting = "true";
38
+ }
39
+
40
+ const {globalDataContext: _globalDataContext, templateContext} = props;
41
+
42
+ // Use the root context when submitting data,
43
+ // not the maybe-filtered one that the DataFilter component may have edited.
44
+ // This could be made configurable if ever needed.
45
+ const globalDataContext = _globalDataContext.getRootContext ? _globalDataContext.getRootContext() : _globalDataContext;
46
+
47
+ /**
48
+ * Tells if the response content will replace the current app content.
49
+ *
50
+ * @type {boolean}
51
+ */
52
+ const refreshAppOnResponse = props?.args?.refreshAppOnResponse ?? true;
53
+
54
+ const url = evaluateTemplateValue({
55
+ valueToEvaluate: props?.args?.url, globalDataContext, templateContext
56
+ });
57
+
58
+ if (!url) {
59
+ return;
60
+ }
61
+
62
+ let payload = {};
63
+
64
+ if (props?.args?.hasOwnProperty("data")) {
65
+ payload = props.args.data;
66
+
67
+ // Evaluate the data on the first level.
68
+ function applyFilter(value, filterFn) {
69
+ if (Array.isArray(value)) {
70
+ return value.map(filterFn);
71
+ } else if (typeof value === 'object' && value !== null) {
72
+ const entries = Object.entries(value).map(([key, val]) => {
73
+ return [key, filterFn(val)];
74
+ });
75
+
76
+ return Object.fromEntries(
77
+ entries
78
+ );
79
+ } else {
80
+ return filterFn(value) ? value : null;
81
+ }
82
+ }
83
+
84
+ payload = applyFilter(payload, (value) => {
85
+ return evaluateTemplateValue({valueToEvaluate: value, globalDataContext, templateContext})
86
+ });
87
+
88
+ if (globalDataContext.templateData.__state !== undefined) {
89
+ // Append the special data.__state value.
90
+ payload.__state = globalDataContext.templateData.__state;
91
+ }
92
+ } else {
93
+ payload.data = globalDataContext.templateData;
94
+
95
+ if (globalDataContext.templateData.__state !== undefined) {
96
+ // Append the special data.__state value.
97
+ payload.data.__state = globalDataContext.templateData.__state;
98
+ }
99
+ }
100
+
101
+ const headers = globalDataContext.headersForData ?? {};
102
+
103
+ const {setRawAppData} = globalDataContext;
104
+
105
+ const config = {
106
+ method: props?.args?.httpMethod ?? "post",
107
+ url: url,
108
+ data: payload,
109
+ };
110
+
111
+ if (headers) {
112
+ // Override headers only when explicitly set.
113
+ config.headers = headers;
114
+ }
115
+
116
+ axios(config)
117
+ .then((value) => {
118
+ if (!refreshAppOnResponse) {
119
+ return;
120
+ }
121
+
122
+ // This will trigger a complete re-render.
123
+ setRawAppData(value.data);
124
+ })
125
+ .catch((reason) => {
126
+ console.log("reactionFunction:submitData : Could not submit. Reason: " + reason.message);
127
+ })
128
+ .finally(() => {
129
+ delete body.dataset.htmlBuilderIsSubmitting;
130
+ delete body.dataset.htmlBuilderIsSubmittingSilently;
131
+
132
+ if (currentTarget?.dataset) {
133
+ delete currentTarget.dataset.isSubmitting;
134
+ }
135
+ });
136
+ };
@@ -0,0 +1,62 @@
1
+ /**
2
+ * Reaction function which will trigger an event on an element identified by the given selector.
3
+ *
4
+ * @param {{args:{selector, selectorBase, eventName}, event}} props Reaction function props.
5
+ */
6
+ export const triggerEvent = (props) => {
7
+ const selector = props?.args?.selector;
8
+
9
+ if (!selector || typeof selector !== "string" || selector.length === 0) {
10
+ return;
11
+ }
12
+
13
+ const selectorBase = props?.args?.selectorBase;
14
+ let selectorBase_real;
15
+
16
+ if (typeof selectorBase === "undefined") {
17
+ selectorBase_real = document;
18
+ } else if (selectorBase === "currentEventTarget") {
19
+ selectorBase_real = props?.event?.target;
20
+ } else {
21
+ // Find the closest element matching the selectorBase as selector.
22
+ selectorBase_real = props?.event?.target?.closest(selectorBase);
23
+ }
24
+
25
+ if (!selectorBase_real) {
26
+ return;
27
+ }
28
+
29
+ const eventName = props?.args?.eventName;
30
+
31
+ if (!eventName || typeof eventName !== "string" || eventName.length === 0) {
32
+ return;
33
+ }
34
+
35
+ const elements = selectorBase_real.querySelectorAll(selector);
36
+
37
+ // Prepare the events in a promise system to handle successive synchronous events.
38
+ // If not doing this, only the last synchronous event will fire.
39
+ const elements_asArray = Object.entries(elements);
40
+
41
+ const consumeSingleEvent = () => {
42
+ const element = elements_asArray.splice(0, 1)?.[0]?.[1] ?? undefined;
43
+
44
+ if (!element) {
45
+ return;
46
+ }
47
+
48
+ const event = new Event(eventName, {
49
+ bubbles: true,
50
+ cancelable: false,
51
+ });
52
+
53
+ element.dispatchEvent(event);
54
+
55
+ // We use promises to make multiple clicks on different targets work.
56
+ // Without this, only the last will receive the event (tested on Firefox).
57
+ // This is undocumented online.
58
+ Promise.resolve().then(consumeSingleEvent);
59
+ };
60
+
61
+ consumeSingleEvent();
62
+ };
@@ -0,0 +1,59 @@
1
+ import {evaluateTemplateValue} from "../../engine/TemplateSystem";
2
+
3
+ /**
4
+ * Pseudo sprintf implementation.
5
+ *
6
+ * Taken from https://stackoverflow.com/a/43718864
7
+ *
8
+ * @licence CC-BY-SA 3.0
9
+ * @author https://stackoverflow.com/users/6314667/7vujy0f0hy and community.
10
+ *
11
+ * @param {string} str The string to format.
12
+ * @param {...string} argv The replacement arguments.
13
+ *
14
+ * @returns {*}
15
+ */
16
+ const sprintf = (str, ...argv) => !argv.length
17
+ ? str
18
+ : sprintf(str = str.replace(sprintf.token || "$token", argv.shift()), ...argv);
19
+
20
+ /**
21
+ * Formats the given string with replacement arguments.
22
+ *
23
+ * @param {{globalDataContext: {}, templateContext: {}}} templateContexts Template contexts.
24
+ * @param {string} toFormat The string to format.
25
+ * @param {...string} argv The replacement arguments.
26
+ */
27
+ export const formatString = ({templateContexts}, toFormat, ...argv) => {
28
+ const evaluatedArgs = argv.map(
29
+ (toEvaluate) => {
30
+ return evaluateTemplateValue({
31
+ globalDataContext: templateContexts.globalDataContext,
32
+ templateContext: templateContexts.templateContext,
33
+ valueToEvaluate: toEvaluate
34
+ });
35
+ }
36
+ )
37
+ return sprintf(toFormat, ...evaluatedArgs);
38
+ };
39
+
40
+ /**
41
+ * Formats the given data with replacement arguments if possible.
42
+ *
43
+ * @param {{templateContexts: {globalDataContext: {}, templateContext: {}}}} options Template contexts.
44
+ * @param {*} toFormat The data to format.
45
+ */
46
+ export const maybeFormatString = (options, toFormat) => {
47
+ if (typeof toFormat === "string") {
48
+ return toFormat;
49
+ }
50
+
51
+ if (Array.isArray(toFormat)) {
52
+ const [toFormatReal, ...args] = toFormat;
53
+
54
+ return formatString(options, toFormatReal, ...args);
55
+ }
56
+
57
+ // Unsupported data to format.
58
+ return "";
59
+ };
@@ -0,0 +1,392 @@
1
+ import {useContext} from "react";
2
+ import GlobalDataContext from "./GlobalDataContext";
3
+ import TemplateContext from "./TemplateContext";
4
+ import {evaluateTemplateValue, isTemplateValue} from "./TemplateSystem";
5
+
6
+ import HashChangeListener from "../component/action/HashChangeListener";
7
+ import Hide from "../component/action/Hide";
8
+ import MessageListener from "../component/action/MessageListener";
9
+ import Popover from "../component/action/Popover";
10
+ import ReactOnEvent, {reactionFunctions} from "../component/action/ReactOnEvent";
11
+ import Redirect from "../component/action/Redirect";
12
+ import Tooltip from "../component/action/Tooltip";
13
+ import VisuallyHide from "../component/action/VisuallyHide";
14
+ import {isEqual} from "lodash";
15
+ import JSONPath from "jsonpath";
16
+
17
+ /**
18
+ * Contains the list of available actions in config.
19
+ * @type {{}}
20
+ */
21
+ const actionsToEvaluate = {
22
+ hide: Hide,
23
+ popover: Popover,
24
+ redirect: Redirect,
25
+ tooltip: Tooltip,
26
+ visuallyHide: VisuallyHide,
27
+ };
28
+
29
+ /**
30
+ * Capitalizes the first letter.
31
+ *
32
+ * @param str
33
+ * @returns {string}
34
+ */
35
+ const capitalizeFirstLetter = (str) => str && str.charAt(0).toUpperCase() + str.slice(1);
36
+
37
+ /**
38
+ * Checks whether the conditions are fulfilled.
39
+ * @param {{andConditions, containedBy, containedByNot, contains, containsNot, is, isNot, orConditions, when, whenDataCountOf, ">", "<", ">=", "<=", compareAsDates}} condition
40
+ * @param {object} templateContexts
41
+ * @param {Map} additionalConditionHandlers
42
+ * @returns {*}
43
+ */
44
+ export const isValid = (condition, templateContexts, additionalConditionHandlers) => {
45
+ const {globalDataContext, templateContext} = templateContexts;
46
+
47
+ if (Array.isArray(condition.andConditions)) {
48
+ return condition.andConditions.reduce(
49
+ (acc, cur) => {
50
+ // All sub conditions must be true.
51
+ return acc && isValid(cur, templateContexts, additionalConditionHandlers);
52
+ },
53
+ true
54
+ );
55
+ }
56
+
57
+ if (Array.isArray(condition.orConditions)) {
58
+ const orConditions = condition.orConditions;
59
+
60
+ for (const condition of orConditions) {
61
+ if (isValid(condition, templateContexts, additionalConditionHandlers)) {
62
+ // A single condition is enough to validate.
63
+ return true;
64
+ }
65
+ }
66
+
67
+ return false;
68
+ }
69
+
70
+ if (condition.when !== undefined && !isTemplateValue(condition.when)) {
71
+ return false;
72
+ }
73
+
74
+ const evaluateTemplateValueLocal = (toEvaluate) => {
75
+ return evaluateTemplateValue({
76
+ globalDataContext: globalDataContext,
77
+ templateContext: templateContext,
78
+ valueToEvaluate: toEvaluate,
79
+ });
80
+ }
81
+
82
+ let valueToCompare;
83
+
84
+ if (condition.when === undefined && condition.hasOwnProperty("whenDataCountOf")) {
85
+ // Select where to count.
86
+ let countFinalTemplateData;
87
+
88
+ switch (condition.inContext) {
89
+ case "root":
90
+ countFinalTemplateData = globalDataContext.getRootContext().templateData;
91
+ break;
92
+
93
+ case "template":
94
+ countFinalTemplateData = templateContext.templateData;
95
+ break;
96
+
97
+ default:
98
+ countFinalTemplateData = globalDataContext.templateData;
99
+ break;
100
+ }
101
+
102
+ valueToCompare = JSONPath.query(countFinalTemplateData, condition.whenDataCountOf).length;
103
+ } else {
104
+ // Other code may inject additional condition handler which can return the value to compare.
105
+ valueToCompare = (() => {
106
+ if (additionalConditionHandlers) {
107
+ for (const [handlerId, additionalConditionHandler] of additionalConditionHandlers) {
108
+ if (condition.hasOwnProperty(handlerId)) {
109
+ // This additional condition handler will compute the value to compare.
110
+ return additionalConditionHandler({
111
+ condition,
112
+ templateContexts,
113
+ evaluateAgainstTemplates: (value) => {
114
+ return evaluateTemplateValueLocal(value);
115
+ }
116
+ });
117
+ }
118
+ }
119
+ }
120
+
121
+ // Compute using the usual method.
122
+ return evaluateTemplateValueLocal(condition.when);
123
+ })();
124
+ }
125
+
126
+ if (condition.hasOwnProperty("isEmpty")) {
127
+ const maybeInvert = val => condition.isEmpty === "not" ? !val : val;
128
+
129
+ if (valueToCompare === null) {
130
+ return maybeInvert(true);
131
+ }
132
+
133
+ switch (typeof valueToCompare) {
134
+ case "undefined":
135
+ return maybeInvert(true);
136
+
137
+ case "string":
138
+ return maybeInvert(valueToCompare.length === 0);
139
+
140
+ case "object":
141
+ return maybeInvert(Object.keys(valueToCompare).length === 0);
142
+
143
+ default:
144
+ return maybeInvert(!valueToCompare);
145
+ }
146
+ }
147
+
148
+ // This can be useful when two differently formatted dates must be compared.
149
+ const compareAsDates = condition.compareAsDates;
150
+
151
+ if (condition.hasOwnProperty("isNot")) {
152
+ return maybeDate(compareAsDates, valueToCompare) !== maybeDate(compareAsDates, evaluateTemplateValueLocal(condition.isNot));
153
+ }
154
+
155
+ if (condition.hasOwnProperty("is")) {
156
+ return maybeDate(compareAsDates, valueToCompare) === maybeDate(compareAsDates, evaluateTemplateValueLocal(condition.is));
157
+ }
158
+
159
+ if (condition.hasOwnProperty("containsNot") || condition.hasOwnProperty("contains")) {
160
+ // True if containsNot.
161
+ const invertedMode = condition.hasOwnProperty("containsNot");
162
+
163
+ const needleSource = invertedMode ? condition.containsNot : condition.contains;
164
+
165
+ const needle = maybeDate(compareAsDates, evaluateTemplateValueLocal(needleSource));
166
+
167
+ if (typeof valueToCompare === "string") {
168
+ // Check as a string. Do a case-insensitive check.
169
+ if (typeof needle !== "string") {
170
+ // The types do not match.
171
+ return invertedMode;
172
+ }
173
+
174
+ const result = valueToCompare.toLowerCase().indexOf(needle.toLowerCase()) !== -1;
175
+
176
+ return invertedMode ? !result : result;
177
+ } else {
178
+ for (const item of Object.values(valueToCompare)) {
179
+ if (isEqual(maybeDate(compareAsDates, item), needle)) {
180
+ // The needle has been found. When in inverted mode (containsNot), return false.
181
+ return !invertedMode;
182
+ }
183
+ }
184
+ }
185
+
186
+ // The needle has not been found. When in inverted mode (containsNot), return true.
187
+ return invertedMode;
188
+ }
189
+
190
+ if (condition.hasOwnProperty("containedByNot") || condition.hasOwnProperty("containedBy")) {
191
+ // True if containedByNot.
192
+ const invertedMode = condition.hasOwnProperty("containedByNot");
193
+
194
+ const needleSource = invertedMode ? condition.containedByNot : condition.containedBy;
195
+
196
+ const container = maybeDate(compareAsDates, evaluateTemplateValueLocal(needleSource));
197
+
198
+ if (typeof valueToCompare === "string") {
199
+ // Check as a string. Do a case-insensitive check.
200
+ if (typeof container !== "string") {
201
+ // The types do not match.
202
+ return invertedMode;
203
+ }
204
+
205
+ const result = container.toLowerCase().indexOf(valueToCompare.toLowerCase()) !== -1;
206
+
207
+ return invertedMode ? !result : result;
208
+ } else {
209
+ for (const item of Object.values(container)) {
210
+ if (isEqual(maybeDate(compareAsDates, item), valueToCompare)) {
211
+ // The needle has been found. When in inverted mode (containsNot), return false.
212
+ return !invertedMode;
213
+ }
214
+ }
215
+ }
216
+
217
+ // The needle has not been found. When in inverted mode (containsNot), return true.
218
+ return invertedMode;
219
+ }
220
+
221
+ if (condition.hasOwnProperty(">")) {
222
+ return maybeDate(compareAsDates, valueToCompare) > maybeDate(compareAsDates, evaluateTemplateValueLocal(condition[">"]));
223
+ }
224
+
225
+ if (condition.hasOwnProperty(">=")) {
226
+ return maybeDate(compareAsDates, valueToCompare) >= maybeDate(compareAsDates, evaluateTemplateValueLocal(condition[">="]));
227
+ }
228
+
229
+ if (condition.hasOwnProperty("<")) {
230
+ return maybeDate(compareAsDates, valueToCompare) < maybeDate(compareAsDates, evaluateTemplateValueLocal(condition["<"]));
231
+ }
232
+
233
+ if (condition.hasOwnProperty("<=")) {
234
+ return maybeDate(compareAsDates, valueToCompare) <= maybeDate(compareAsDates, evaluateTemplateValueLocal(condition["<="]));
235
+ }
236
+
237
+ // No condition means always valid.
238
+ return true;
239
+ };
240
+
241
+ /**
242
+ * Gets the actions to execute for the current component.
243
+ * @param {Array} actions
244
+ * @param {object} templateContexts
245
+ * @returns {*[]}
246
+ */
247
+ const getActionsToExecute = (actions, templateContexts) => {
248
+ const result = [];
249
+
250
+ let requiresReactionComponent = false;
251
+
252
+ const reactionFunctionProps = {};
253
+
254
+ if (!Array.isArray(actions)) {
255
+ // Not a supported actions structure.
256
+ // Dev note: we may also allow objects in the future, to allow specific overrides.
257
+ return result;
258
+ }
259
+
260
+ // The index is useful to build the data path for the components created by the actions.
261
+ // This is a requirement of the View components.
262
+ // Dev note: could it be like the components, where we can specify string keys,
263
+ // thus allowing objects in the actions definitions?
264
+ for (const [index, item] of actions.entries()) {
265
+ const what = item?.what ?? undefined;
266
+
267
+ if (!what) {
268
+ continue;
269
+ }
270
+
271
+ const Component = actionsToEvaluate[what];
272
+ let reactionFunction = undefined;
273
+
274
+ if (!Component) {
275
+ // This is not a component. Maybe it's a reaction function...
276
+ reactionFunction = reactionFunctions[what] ?? undefined;
277
+
278
+ if (!reactionFunction) {
279
+ // The component is unknown or not registered,
280
+ // and it's not a reaction function.
281
+ continue;
282
+ }
283
+
284
+ // This is a reaction function.
285
+ if (item.on === undefined) {
286
+ // Reaction functions are called only in response to events.
287
+ // Dev note: evaluate if we can execute reaction functions on component mount,
288
+ // with empty "on".
289
+ continue;
290
+ }
291
+
292
+ if (!isValid(item, templateContexts)) {
293
+ continue;
294
+ }
295
+
296
+ if (item.on === "message") {
297
+ // "message" has a special handling. It adds the special MessageListener action component.
298
+ // This is because the message event can only be listened to on the window object,
299
+ // so it adds event listeners on the window object (not the current component).
300
+ result.push({ActionComponent: MessageListener, actionProps: item, actionIndex: index});
301
+ continue;
302
+ }
303
+
304
+ if (item.on === "hashchange") {
305
+ // "hashchange" works in the same way than "message": it must be added on the window object.
306
+ result.push({ActionComponent: HashChangeListener, actionProps: item, actionIndex: index});
307
+ continue;
308
+ }
309
+
310
+ requiresReactionComponent = true;
311
+
312
+ const normalizedEventName = "on" + capitalizeFirstLetter(item.on);
313
+
314
+ if (!Array.isArray(reactionFunctionProps[normalizedEventName])) {
315
+ // Initialize the key.
316
+ reactionFunctionProps[normalizedEventName] = [];
317
+ }
318
+
319
+ // Append the reaction function definition that will be read
320
+ // later by the ReactOnEvent action component.
321
+ reactionFunctionProps[normalizedEventName].push(item);
322
+
323
+ // Do not add the ReactOnEvent component yet in the result array.
324
+ // It will be added at the end of the actions chain.
325
+ continue;
326
+ }
327
+
328
+ if (!isValid(item, templateContexts)) {
329
+ continue;
330
+ }
331
+
332
+ result.push({ActionComponent: Component, actionProps: item, actionIndex: index});
333
+ }
334
+
335
+ if (requiresReactionComponent) {
336
+ // Add the final component for reaction.
337
+ // It's added at the end because it will collect all definitions
338
+ // and apply the reaction function properties on the real rendered element.
339
+ // TODO: evaluate if the _reactOnEvent actionIndex may create issues.
340
+ result.push({ActionComponent: ReactOnEvent, actionProps: reactionFunctionProps, actionIndex: "_reactOnEvent"});
341
+ }
342
+
343
+ return result;
344
+ };
345
+
346
+ /**
347
+ * Maybe converts the given value to a date representation.
348
+ *
349
+ * Needed for date comparisons.
350
+ *
351
+ * @param {boolean} shouldCompareAsDates Tells if the value should be converted.
352
+ * @param {any} a The value to maybe convert.
353
+ *
354
+ * @returns {Date|*} The converted or same value.
355
+ */
356
+ const maybeDate = (shouldCompareAsDates, a) => {
357
+ return shouldCompareAsDates ? new Date(a) : a;
358
+ };
359
+
360
+ /**
361
+ * Component that executes actions before rendering its children.
362
+ *
363
+ * Actions include a system to hide.
364
+ *
365
+ * @param props The props that served to build an action dependant component.
366
+ * @returns {*}
367
+ * @constructor
368
+ */
369
+ const ActionDependant = (props) => {
370
+ const globalDataContext = useContext(GlobalDataContext);
371
+ const templateContext = useContext(TemplateContext);
372
+
373
+ if (!props) {
374
+ // There is nothing to render or evaluate, so don't even bother checking the actions.
375
+ return null;
376
+ }
377
+
378
+ const result = getActionsToExecute(props?.actions ?? [], {globalDataContext, templateContext});
379
+
380
+ // Encapsulate into actions.
381
+ return result.reverse().reduce((acc, {ActionComponent, actionProps, actionIndex}) => {
382
+ // actionProps contains only the info that is related to the action.
383
+ return <ActionComponent
384
+ componentProps={props}
385
+ actionProps={actionProps}
386
+ actionIndex={actionIndex}>
387
+ {acc}
388
+ </ActionComponent>;
389
+ }, props?.children ?? null);
390
+ };
391
+
392
+ export default ActionDependant;
@@ -0,0 +1,16 @@
1
+ import {createContext} from "react";
2
+
3
+ /**
4
+ * A context for "globalizing" event dispatchers.
5
+ *
6
+ * This will help reducing the count of event listeners added to the DOM,
7
+ * which would slow down the render if each listener was appended individually.
8
+ *
9
+ * @type {React.Context<{removeEventListener: removeEventListener, addEventListener: addEventListener}>}
10
+ */
11
+ const EventDispatcherContext = createContext({
12
+ addEventListener: null,
13
+ removeEventListener: null,
14
+ });
15
+
16
+ export default EventDispatcherContext;