@player-ui/data-change-listener-plugin 0.0.1-next.1

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.
@@ -0,0 +1,127 @@
1
+ 'use strict';
2
+
3
+ Object.defineProperty(exports, '__esModule', { value: true });
4
+
5
+ const LISTENER_TYPES = {
6
+ dataChange: "dataChange."
7
+ };
8
+ const WILDCARD_REGEX = /\._\.|\._$/;
9
+ function replaceExpressionIndexes(exp, indexes) {
10
+ if (indexes.length === 0) {
11
+ return exp;
12
+ }
13
+ if (typeof exp === "object" && exp !== null) {
14
+ return Object.values(exp).map((subExp) => replaceExpressionIndexes(subExp, indexes));
15
+ }
16
+ let workingExp = String(exp);
17
+ for (let replacementIndex = 0; replacementIndex < indexes.length; replacementIndex += 1) {
18
+ const regex = new RegExp(`_index${replacementIndex === 0 ? "" : replacementIndex.toString()}_`, "g");
19
+ workingExp = workingExp.replace(regex, indexes[replacementIndex].toString());
20
+ }
21
+ return workingExp;
22
+ }
23
+ function createWildcardHandler(listenerBinding, listenerExp, bindingParser) {
24
+ const wildCardIndex = listenerBinding.search(WILDCARD_REGEX);
25
+ const parsedListenerBinding = bindingParser.parse(listenerBinding);
26
+ const topLevelBinding = bindingParser.parse(listenerBinding.substr(0, wildCardIndex));
27
+ const getUpdatedExpressionToRun = (updatedBinding) => {
28
+ const indexes = [];
29
+ for (let bindingPartIndex = 0; bindingPartIndex < parsedListenerBinding.asArray().length; bindingPartIndex += 1) {
30
+ const listenerBindingPart = parsedListenerBinding.asArray()[bindingPartIndex];
31
+ const updatedBindingPart = updatedBinding.asArray()[bindingPartIndex];
32
+ if (listenerBindingPart === "_") {
33
+ indexes.push(updatedBindingPart);
34
+ } else if (updatedBindingPart !== listenerBindingPart) {
35
+ return;
36
+ }
37
+ }
38
+ return replaceExpressionIndexes(listenerExp, indexes);
39
+ };
40
+ return (context, binding) => {
41
+ if (topLevelBinding.contains(binding)) {
42
+ const expToRun = getUpdatedExpressionToRun(binding);
43
+ if (expToRun) {
44
+ context.expressionEvaluator.evaluate(expToRun);
45
+ }
46
+ }
47
+ };
48
+ }
49
+ function extractDataChangeListeners(view, bindingParser) {
50
+ if (!(view == null ? void 0 : view.listeners)) {
51
+ return [];
52
+ }
53
+ const { listeners } = view;
54
+ return Object.entries(listeners).reduce((allListeners, [listenerKey, listenerExp]) => {
55
+ if (typeof listenerKey !== "string" || !listenerKey.startsWith(LISTENER_TYPES.dataChange)) {
56
+ return allListeners;
57
+ }
58
+ const listenerRawBinding = listenerKey.slice(LISTENER_TYPES.dataChange.length);
59
+ if (listenerKey.match(WILDCARD_REGEX)) {
60
+ return [
61
+ ...allListeners,
62
+ createWildcardHandler(listenerRawBinding, listenerExp, bindingParser)
63
+ ];
64
+ }
65
+ const parsedOriginalBinding = bindingParser.parse(listenerRawBinding);
66
+ return [
67
+ ...allListeners,
68
+ (context, binding) => {
69
+ if (parsedOriginalBinding.contains(binding)) {
70
+ context.expressionEvaluator.evaluate(listenerExp);
71
+ }
72
+ }
73
+ ];
74
+ }, []);
75
+ }
76
+ class DataChangeListenerPlugin {
77
+ constructor() {
78
+ this.name = "data-change-listener-plugin";
79
+ }
80
+ apply(player) {
81
+ let expressionEvaluator;
82
+ let dataChangeListeners = [];
83
+ player.hooks.expressionEvaluator.tap(this.name, (expEvaluator) => {
84
+ expressionEvaluator = expEvaluator;
85
+ });
86
+ const onFieldUpdateHandler = (updates) => {
87
+ if (updates.length === 0 || !expressionEvaluator || dataChangeListeners.length === 0) {
88
+ return;
89
+ }
90
+ updates.forEach((binding) => {
91
+ dataChangeListeners.forEach((handler) => {
92
+ handler({
93
+ expressionEvaluator
94
+ }, binding);
95
+ });
96
+ });
97
+ };
98
+ player.hooks.dataController.tap(this.name, (dc) => dc.hooks.onUpdate.tap(this.name, (updates) => {
99
+ onFieldUpdateHandler(updates.map((t) => t.binding));
100
+ }));
101
+ const resolveViewInterceptor = {
102
+ context: false,
103
+ call: (view) => {
104
+ const playerState = player.getState();
105
+ if (playerState.status !== "in-progress") {
106
+ return;
107
+ }
108
+ dataChangeListeners = extractDataChangeListeners(view, playerState.controllers.binding);
109
+ }
110
+ };
111
+ player.hooks.viewController.tap(this.name, (viewController) => {
112
+ viewController.hooks.resolveView.intercept(resolveViewInterceptor);
113
+ });
114
+ player.hooks.flowController.tap(this.name, (flowController) => {
115
+ flowController.hooks.flow.tap(this.name, (flow) => {
116
+ flow.hooks.transition.tap(this.name, (from, to) => {
117
+ if (to.value.state_type !== "VIEW") {
118
+ dataChangeListeners = [];
119
+ }
120
+ });
121
+ });
122
+ });
123
+ }
124
+ }
125
+
126
+ exports.DataChangeListenerPlugin = DataChangeListenerPlugin;
127
+ //# sourceMappingURL=index.cjs.js.map
@@ -0,0 +1,17 @@
1
+ import { PlayerPlugin, Player } from '@player-ui/player';
2
+ import { ExpressionEvaluator } from '@player-ui/expressions';
3
+ import { BindingInstance } from '@player-ui/binding';
4
+
5
+ declare type ViewListenerHandler = (context: {
6
+ /** a means of evaluating an expression */
7
+ expressionEvaluator: ExpressionEvaluator;
8
+ }, binding: BindingInstance) => void;
9
+ /**
10
+ * this plugin processes the view level dataChange and evaluates custom expressions.
11
+ */
12
+ declare class DataChangeListenerPlugin implements PlayerPlugin {
13
+ name: string;
14
+ apply(player: Player): void;
15
+ }
16
+
17
+ export { DataChangeListenerPlugin, ViewListenerHandler };
@@ -0,0 +1,123 @@
1
+ const LISTENER_TYPES = {
2
+ dataChange: "dataChange."
3
+ };
4
+ const WILDCARD_REGEX = /\._\.|\._$/;
5
+ function replaceExpressionIndexes(exp, indexes) {
6
+ if (indexes.length === 0) {
7
+ return exp;
8
+ }
9
+ if (typeof exp === "object" && exp !== null) {
10
+ return Object.values(exp).map((subExp) => replaceExpressionIndexes(subExp, indexes));
11
+ }
12
+ let workingExp = String(exp);
13
+ for (let replacementIndex = 0; replacementIndex < indexes.length; replacementIndex += 1) {
14
+ const regex = new RegExp(`_index${replacementIndex === 0 ? "" : replacementIndex.toString()}_`, "g");
15
+ workingExp = workingExp.replace(regex, indexes[replacementIndex].toString());
16
+ }
17
+ return workingExp;
18
+ }
19
+ function createWildcardHandler(listenerBinding, listenerExp, bindingParser) {
20
+ const wildCardIndex = listenerBinding.search(WILDCARD_REGEX);
21
+ const parsedListenerBinding = bindingParser.parse(listenerBinding);
22
+ const topLevelBinding = bindingParser.parse(listenerBinding.substr(0, wildCardIndex));
23
+ const getUpdatedExpressionToRun = (updatedBinding) => {
24
+ const indexes = [];
25
+ for (let bindingPartIndex = 0; bindingPartIndex < parsedListenerBinding.asArray().length; bindingPartIndex += 1) {
26
+ const listenerBindingPart = parsedListenerBinding.asArray()[bindingPartIndex];
27
+ const updatedBindingPart = updatedBinding.asArray()[bindingPartIndex];
28
+ if (listenerBindingPart === "_") {
29
+ indexes.push(updatedBindingPart);
30
+ } else if (updatedBindingPart !== listenerBindingPart) {
31
+ return;
32
+ }
33
+ }
34
+ return replaceExpressionIndexes(listenerExp, indexes);
35
+ };
36
+ return (context, binding) => {
37
+ if (topLevelBinding.contains(binding)) {
38
+ const expToRun = getUpdatedExpressionToRun(binding);
39
+ if (expToRun) {
40
+ context.expressionEvaluator.evaluate(expToRun);
41
+ }
42
+ }
43
+ };
44
+ }
45
+ function extractDataChangeListeners(view, bindingParser) {
46
+ if (!(view == null ? void 0 : view.listeners)) {
47
+ return [];
48
+ }
49
+ const { listeners } = view;
50
+ return Object.entries(listeners).reduce((allListeners, [listenerKey, listenerExp]) => {
51
+ if (typeof listenerKey !== "string" || !listenerKey.startsWith(LISTENER_TYPES.dataChange)) {
52
+ return allListeners;
53
+ }
54
+ const listenerRawBinding = listenerKey.slice(LISTENER_TYPES.dataChange.length);
55
+ if (listenerKey.match(WILDCARD_REGEX)) {
56
+ return [
57
+ ...allListeners,
58
+ createWildcardHandler(listenerRawBinding, listenerExp, bindingParser)
59
+ ];
60
+ }
61
+ const parsedOriginalBinding = bindingParser.parse(listenerRawBinding);
62
+ return [
63
+ ...allListeners,
64
+ (context, binding) => {
65
+ if (parsedOriginalBinding.contains(binding)) {
66
+ context.expressionEvaluator.evaluate(listenerExp);
67
+ }
68
+ }
69
+ ];
70
+ }, []);
71
+ }
72
+ class DataChangeListenerPlugin {
73
+ constructor() {
74
+ this.name = "data-change-listener-plugin";
75
+ }
76
+ apply(player) {
77
+ let expressionEvaluator;
78
+ let dataChangeListeners = [];
79
+ player.hooks.expressionEvaluator.tap(this.name, (expEvaluator) => {
80
+ expressionEvaluator = expEvaluator;
81
+ });
82
+ const onFieldUpdateHandler = (updates) => {
83
+ if (updates.length === 0 || !expressionEvaluator || dataChangeListeners.length === 0) {
84
+ return;
85
+ }
86
+ updates.forEach((binding) => {
87
+ dataChangeListeners.forEach((handler) => {
88
+ handler({
89
+ expressionEvaluator
90
+ }, binding);
91
+ });
92
+ });
93
+ };
94
+ player.hooks.dataController.tap(this.name, (dc) => dc.hooks.onUpdate.tap(this.name, (updates) => {
95
+ onFieldUpdateHandler(updates.map((t) => t.binding));
96
+ }));
97
+ const resolveViewInterceptor = {
98
+ context: false,
99
+ call: (view) => {
100
+ const playerState = player.getState();
101
+ if (playerState.status !== "in-progress") {
102
+ return;
103
+ }
104
+ dataChangeListeners = extractDataChangeListeners(view, playerState.controllers.binding);
105
+ }
106
+ };
107
+ player.hooks.viewController.tap(this.name, (viewController) => {
108
+ viewController.hooks.resolveView.intercept(resolveViewInterceptor);
109
+ });
110
+ player.hooks.flowController.tap(this.name, (flowController) => {
111
+ flowController.hooks.flow.tap(this.name, (flow) => {
112
+ flow.hooks.transition.tap(this.name, (from, to) => {
113
+ if (to.value.state_type !== "VIEW") {
114
+ dataChangeListeners = [];
115
+ }
116
+ });
117
+ });
118
+ });
119
+ }
120
+ }
121
+
122
+ export { DataChangeListenerPlugin };
123
+ //# sourceMappingURL=index.esm.js.map
package/package.json ADDED
@@ -0,0 +1,17 @@
1
+ {
2
+ "name": "@player-ui/data-change-listener-plugin",
3
+ "version": "0.0.1-next.1",
4
+ "private": false,
5
+ "publishConfig": {
6
+ "registry": "https://registry.npmjs.org"
7
+ },
8
+ "peerDependencies": {
9
+ "@player-ui/binding-grammar": "0.0.1-next.1"
10
+ },
11
+ "dependencies": {
12
+ "@babel/runtime": "7.15.4"
13
+ },
14
+ "main": "dist/index.cjs.js",
15
+ "module": "dist/index.esm.js",
16
+ "typings": "dist/index.d.ts"
17
+ }
package/src/index.ts ADDED
@@ -0,0 +1,277 @@
1
+ import type {
2
+ Player,
3
+ DataController,
4
+ PlayerPlugin,
5
+ View,
6
+ ViewController,
7
+ } from '@player-ui/player';
8
+ import type {
9
+ ExpressionType,
10
+ ExpressionEvaluator,
11
+ } from '@player-ui/expressions';
12
+ import type { BindingInstance, BindingParser } from '@player-ui/binding';
13
+
14
+ const LISTENER_TYPES = {
15
+ dataChange: 'dataChange.',
16
+ };
17
+
18
+ const WILDCARD_REGEX = /\._\.|\._$/;
19
+
20
+ /** View with view level listeners that can do arbitrary expression */
21
+ interface ViewWithListener extends View {
22
+ /** a list of listeners */
23
+ listeners?: {
24
+ /** the key specifies what type of listener */
25
+ [key: string]: ExpressionType;
26
+ };
27
+ }
28
+
29
+ export type ViewListenerHandler = (
30
+ context: {
31
+ /** a means of evaluating an expression */
32
+ expressionEvaluator: ExpressionEvaluator;
33
+ },
34
+ binding: BindingInstance
35
+ ) => void;
36
+
37
+ /** Sub out any _index_ refs with the ones from the supplied list */
38
+ function replaceExpressionIndexes(
39
+ exp: ExpressionType,
40
+ indexes: Array<string | number>
41
+ ): ExpressionType {
42
+ if (indexes.length === 0) {
43
+ return exp;
44
+ }
45
+
46
+ if (typeof exp === 'object' && exp !== null) {
47
+ return Object.values(exp).map((subExp) =>
48
+ replaceExpressionIndexes(subExp, indexes)
49
+ );
50
+ }
51
+
52
+ let workingExp = String(exp);
53
+
54
+ for (
55
+ let replacementIndex = 0;
56
+ replacementIndex < indexes.length;
57
+ replacementIndex += 1
58
+ ) {
59
+ const regex = new RegExp(
60
+ `_index${replacementIndex === 0 ? '' : replacementIndex.toString()}_`,
61
+ 'g'
62
+ );
63
+
64
+ workingExp = workingExp.replace(
65
+ regex,
66
+ indexes[replacementIndex].toString()
67
+ );
68
+ }
69
+
70
+ return workingExp;
71
+ }
72
+
73
+ /**
74
+ * Create a handler for view listeners with wildcard (._.) placeholders
75
+ */
76
+ function createWildcardHandler(
77
+ listenerBinding: string,
78
+ listenerExp: ExpressionType,
79
+ bindingParser: BindingParser
80
+ ): ViewListenerHandler {
81
+ // The index of the start of the wildcard placeholder (foo._.bar)
82
+ const wildCardIndex = listenerBinding.search(WILDCARD_REGEX);
83
+ const parsedListenerBinding = bindingParser.parse(listenerBinding);
84
+
85
+ // The top binding that we care about
86
+ const topLevelBinding = bindingParser.parse(
87
+ listenerBinding.substr(0, wildCardIndex)
88
+ );
89
+
90
+ /** Compute an updated expression (resolving _index_'s), or nothing if the binding update doesn't match */
91
+ const getUpdatedExpressionToRun = (
92
+ updatedBinding: BindingInstance
93
+ ): ExpressionType | undefined => {
94
+ // what to replace _index_, _index1_, etc.
95
+ const indexes: Array<number | string> = [];
96
+
97
+ // walk down both bindings, and match up the ._. substitutions.
98
+ // If we hit a placeholder, sub it out with the right value from the _actual_ binding
99
+ // If we hit a non-placeholder, make sure the keys match up
100
+
101
+ for (
102
+ let bindingPartIndex = 0;
103
+ bindingPartIndex < parsedListenerBinding.asArray().length;
104
+ bindingPartIndex += 1
105
+ ) {
106
+ const listenerBindingPart =
107
+ parsedListenerBinding.asArray()[bindingPartIndex];
108
+ const updatedBindingPart = updatedBinding.asArray()[bindingPartIndex];
109
+
110
+ if (listenerBindingPart === '_') {
111
+ indexes.push(updatedBindingPart);
112
+ } else if (updatedBindingPart !== listenerBindingPart) {
113
+ // We are listening for a binding that isn't this one
114
+ // foo._.bar vs. foo._.baz
115
+ // bail out
116
+ return;
117
+ }
118
+ }
119
+
120
+ // sub out all of the _index_ values in the expression with our real ones.
121
+ return replaceExpressionIndexes(listenerExp, indexes);
122
+ };
123
+
124
+ return (context, binding) => {
125
+ if (topLevelBinding.contains(binding)) {
126
+ // Check if the sub-listener is also a match
127
+ const expToRun = getUpdatedExpressionToRun(binding);
128
+
129
+ if (expToRun) {
130
+ context.expressionEvaluator.evaluate(expToRun);
131
+ }
132
+ }
133
+ };
134
+ }
135
+
136
+ /**
137
+ * Extract data change listener from the view
138
+ *
139
+ * @param view - initial view with optional listener
140
+ */
141
+ function extractDataChangeListeners(
142
+ view: ViewWithListener,
143
+ bindingParser: BindingParser
144
+ ): Array<ViewListenerHandler> {
145
+ if (!view?.listeners) {
146
+ return [];
147
+ }
148
+
149
+ const { listeners } = view;
150
+
151
+ return Object.entries(listeners).reduce<Array<ViewListenerHandler>>(
152
+ (allListeners, [listenerKey, listenerExp]) => {
153
+ if (
154
+ typeof listenerKey !== 'string' ||
155
+ !listenerKey.startsWith(LISTENER_TYPES.dataChange)
156
+ ) {
157
+ return allListeners;
158
+ }
159
+
160
+ const listenerRawBinding = listenerKey.slice(
161
+ LISTENER_TYPES.dataChange.length
162
+ );
163
+
164
+ if (listenerKey.match(WILDCARD_REGEX)) {
165
+ return [
166
+ ...allListeners,
167
+ createWildcardHandler(listenerRawBinding, listenerExp, bindingParser),
168
+ ];
169
+ }
170
+
171
+ const parsedOriginalBinding = bindingParser.parse(listenerRawBinding);
172
+
173
+ return [
174
+ ...allListeners,
175
+ (context, binding) => {
176
+ if (parsedOriginalBinding.contains(binding)) {
177
+ context.expressionEvaluator.evaluate(listenerExp);
178
+ }
179
+ },
180
+ ];
181
+ },
182
+ []
183
+ );
184
+ }
185
+
186
+ /**
187
+ * this plugin processes the view level dataChange and evaluates custom expressions.
188
+ */
189
+ export class DataChangeListenerPlugin implements PlayerPlugin {
190
+ name = 'data-change-listener-plugin';
191
+
192
+ apply(player: Player) {
193
+ let expressionEvaluator: ExpressionEvaluator;
194
+ let dataChangeListeners: Array<ViewListenerHandler> = [];
195
+
196
+ player.hooks.expressionEvaluator.tap(
197
+ this.name,
198
+ (expEvaluator: ExpressionEvaluator) => {
199
+ expressionEvaluator = expEvaluator;
200
+ }
201
+ );
202
+
203
+ /**
204
+ * This function handles checking if the updated field requires an expression to be evaluated,
205
+ * One of the view-level listeners is attached to the field
206
+ *
207
+ * @param updates - field updates
208
+ */
209
+ const onFieldUpdateHandler = (updates: Array<BindingInstance>) => {
210
+ if (
211
+ updates.length === 0 ||
212
+ !expressionEvaluator ||
213
+ dataChangeListeners.length === 0
214
+ ) {
215
+ return;
216
+ }
217
+
218
+ updates.forEach((binding) => {
219
+ dataChangeListeners.forEach((handler) => {
220
+ handler(
221
+ {
222
+ expressionEvaluator,
223
+ },
224
+ binding
225
+ );
226
+ });
227
+ });
228
+ };
229
+
230
+ player.hooks.dataController.tap(this.name, (dc: DataController) =>
231
+ dc.hooks.onUpdate.tap(this.name, (updates) => {
232
+ onFieldUpdateHandler(updates.map((t) => t.binding));
233
+ })
234
+ );
235
+
236
+ /**
237
+ * Adding an interceptor instead of tapping to make intention clear. This plugin is not going to change the resolution of a view
238
+ * so do not want to tap into the resolveView hook. This will just intercept and extract required dependencies
239
+ * There are other hooks that can be used:
240
+ * 1) view -> onUpdate : the update object is the updated assets but this gets called upon every data update
241
+ * 2) view -> parser -> onParseObject: this gets called once per view but the input is a node within the Content but since the listener is only supported at the view level, this would be excessive
242
+ * 3) view -> resolve -> : all resolve hooks are called every update - the listeners should not change between data updates.
243
+ */
244
+ const resolveViewInterceptor = {
245
+ context: false,
246
+ call: (view: View) => {
247
+ const playerState = player.getState();
248
+
249
+ if (playerState.status !== 'in-progress') {
250
+ return;
251
+ }
252
+
253
+ dataChangeListeners = extractDataChangeListeners(
254
+ view,
255
+ playerState.controllers.binding
256
+ );
257
+ },
258
+ };
259
+
260
+ player.hooks.viewController.tap(
261
+ this.name,
262
+ (viewController: ViewController) => {
263
+ viewController.hooks.resolveView.intercept(resolveViewInterceptor);
264
+ }
265
+ );
266
+
267
+ player.hooks.flowController.tap(this.name, (flowController) => {
268
+ flowController.hooks.flow.tap(this.name, (flow) => {
269
+ flow.hooks.transition.tap(this.name, (from, to) => {
270
+ if (to.value.state_type !== 'VIEW') {
271
+ dataChangeListeners = [];
272
+ }
273
+ });
274
+ });
275
+ });
276
+ }
277
+ }