@player-ui/player 0.4.0-next.7 → 0.4.0-next.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (38) hide show
  1. package/dist/index.cjs.js +622 -325
  2. package/dist/index.d.ts +188 -72
  3. package/dist/index.esm.js +619 -327
  4. package/dist/player.dev.js +623 -326
  5. package/dist/player.prod.js +1 -1
  6. package/package.json +11 -3
  7. package/src/binding/binding.ts +8 -0
  8. package/src/binding/index.ts +1 -1
  9. package/src/controllers/constants/index.ts +9 -5
  10. package/src/controllers/data.ts +49 -52
  11. package/src/controllers/flow/controller.ts +16 -12
  12. package/src/controllers/flow/flow.ts +6 -1
  13. package/src/controllers/validation/binding-tracker.ts +42 -19
  14. package/src/controllers/validation/controller.ts +265 -85
  15. package/src/controllers/view/asset-transform.ts +4 -1
  16. package/src/controllers/view/controller.ts +19 -2
  17. package/src/data/dependency-tracker.ts +14 -0
  18. package/src/data/local-model.ts +25 -1
  19. package/src/data/model.ts +55 -8
  20. package/src/data/noop-model.ts +2 -0
  21. package/src/expressions/evaluator-functions.ts +24 -2
  22. package/src/expressions/evaluator.ts +35 -31
  23. package/src/expressions/types.ts +17 -5
  24. package/src/expressions/utils.ts +19 -0
  25. package/src/player.ts +26 -29
  26. package/src/plugins/flow-exp-plugin.ts +2 -2
  27. package/src/string-resolver/index.ts +7 -2
  28. package/src/types.ts +1 -4
  29. package/src/validator/binding-map-splice.ts +59 -0
  30. package/src/validator/index.ts +1 -0
  31. package/src/validator/types.ts +11 -3
  32. package/src/validator/validation-middleware.ts +34 -3
  33. package/src/view/parser/index.ts +44 -2
  34. package/src/view/plugins/applicability.ts +1 -1
  35. package/src/view/plugins/string-resolver.ts +8 -4
  36. package/src/view/plugins/template-plugin.ts +1 -6
  37. package/src/view/resolver/index.ts +119 -54
  38. package/src/view/resolver/types.ts +48 -7
package/package.json CHANGED
@@ -1,14 +1,14 @@
1
1
  {
2
2
  "name": "@player-ui/player",
3
- "version": "0.4.0-next.7",
3
+ "version": "0.4.0-next.9",
4
4
  "private": false,
5
5
  "publishConfig": {
6
6
  "registry": "https://registry.npmjs.org"
7
7
  },
8
8
  "peerDependencies": {},
9
9
  "dependencies": {
10
- "@player-ui/partial-match-registry": "0.4.0-next.7",
11
- "@player-ui/types": "0.4.0-next.7",
10
+ "@player-ui/partial-match-registry": "0.4.0-next.9",
11
+ "@player-ui/types": "0.4.0-next.9",
12
12
  "dequal": "^2.0.2",
13
13
  "p-defer": "^3.0.0",
14
14
  "queue-microtask": "^1.2.3",
@@ -64,6 +64,14 @@
64
64
  {
65
65
  "name": "Kelly Harrop",
66
66
  "url": "https://github.com/kharrop"
67
+ },
68
+ {
69
+ "name": "Alejandro Fimbres",
70
+ "url": "https://github.com/lexfm"
71
+ },
72
+ {
73
+ "name": "Rafael Campos",
74
+ "url": "https://github.com/rafbcampos"
67
75
  }
68
76
  ],
69
77
  "bundle": "./dist/player.prod.js"
@@ -14,6 +14,14 @@ export interface BindingParserOptions {
14
14
  * Get the result of evaluating an expression
15
15
  */
16
16
  evaluate: (exp: string) => any;
17
+
18
+ /**
19
+ * Without readOnly, if a binding such as this is used: arr[key='does not exist'],
20
+ * then an object with that key will be created.
21
+ * This is done to make assignment such as arr[key='abc'].val = 'foo' work smoothly.
22
+ * Setting readOnly to true will prevent this behavior, avoiding unintended data changes.
23
+ */
24
+ readOnly?: boolean;
17
25
  }
18
26
 
19
27
  export type Getter = (path: BindingInstance) => any;
@@ -172,7 +172,7 @@ export class BindingParser {
172
172
 
173
173
  const updateKeys = Object.keys(updates);
174
174
 
175
- if (updateKeys.length > 0) {
175
+ if (!options.readOnly && updateKeys.length > 0) {
176
176
  const updateTransaction = updateKeys.map<[BindingInstance, any]>(
177
177
  (updatedBinding) => [
178
178
  this.parse(updatedBinding),
@@ -10,7 +10,7 @@ export interface ConstantsProvider {
10
10
  addConstants(data: Record<string, any>, namespace: string): void;
11
11
 
12
12
  /**
13
- * Function to retreive constants from the providers store
13
+ * Function to retrieve constants from the providers store
14
14
  * - @param key Key used for the store access
15
15
  * - @param namespace namespace values were loaded under (defined in the plugin)
16
16
  * - @param fallback Optional - if key doesn't exist in namespace what to return (will return unknown if not provided)
@@ -77,9 +77,13 @@ export class ConstantsController implements ConstantsProvider {
77
77
  }
78
78
  }
79
79
 
80
- clearTemporaryValues(): void {
81
- this.tempStore.forEach((value: LocalModel) => {
82
- value.reset();
83
- });
80
+ clearTemporaryValues(namespace?: string): void {
81
+ if (namespace) {
82
+ this.tempStore.get(namespace)?.reset();
83
+ } else {
84
+ this.tempStore.forEach((value: LocalModel) => {
85
+ value.reset();
86
+ });
87
+ }
84
88
  }
85
89
  }
@@ -25,11 +25,15 @@ export class DataController implements DataModelWithParser<DataModelOptions> {
25
25
  resolveDefaultValue: new SyncBailHook<[BindingInstance], any>(),
26
26
 
27
27
  onDelete: new SyncHook<[any]>(),
28
+
28
29
  onSet: new SyncHook<[BatchSetTransaction]>(),
30
+
29
31
  onGet: new SyncHook<[any, any]>(),
32
+
30
33
  onUpdate: new SyncHook<[Updates, DataModelOptions | undefined]>(),
31
34
 
32
35
  format: new SyncWaterfallHook<[any, BindingInstance]>(),
36
+
33
37
  deformat: new SyncWaterfallHook<[any, BindingInstance]>(),
34
38
 
35
39
  serialize: new SyncWaterfallHook<[any]>(),
@@ -119,18 +123,24 @@ export class DataController implements DataModelWithParser<DataModelOptions> {
119
123
  (updates, [binding, newVal]) => {
120
124
  const oldVal = this.get(binding, { includeInvalid: true });
121
125
 
122
- if (!dequal(oldVal, newVal)) {
123
- updates.push({
124
- binding,
125
- newValue: newVal,
126
- oldValue: oldVal,
127
- });
126
+ const update = {
127
+ binding,
128
+ newValue: newVal,
129
+ oldValue: oldVal,
130
+ };
131
+
132
+ if (dequal(oldVal, newVal)) {
133
+ this.logger?.debug(
134
+ `Skipping update for path: ${binding.asString()}. Value was unchanged: ${oldVal}`
135
+ );
136
+ } else {
137
+ updates.push(update);
138
+
139
+ this.logger?.debug(
140
+ `Setting path: ${binding.asString()} from: ${oldVal} to: ${newVal}`
141
+ );
128
142
  }
129
143
 
130
- this.logger?.debug(
131
- `Setting path: ${binding.asString()} from: ${oldVal} to: ${newVal}`
132
- );
133
-
134
144
  return updates;
135
145
  },
136
146
  []
@@ -164,15 +174,17 @@ export class DataController implements DataModelWithParser<DataModelOptions> {
164
174
  return result;
165
175
  }
166
176
 
167
- private resolve(binding: BindingLike): BindingInstance {
177
+ private resolve(binding: BindingLike, readOnly: boolean): BindingInstance {
168
178
  return Array.isArray(binding) || typeof binding === 'string'
169
- ? this.pathResolver.parse(binding)
179
+ ? this.pathResolver.parse(binding, { readOnly })
170
180
  : binding;
171
181
  }
172
182
 
173
183
  public get(binding: BindingLike, options?: DataModelOptions) {
174
184
  const resolved =
175
- binding instanceof BindingInstance ? binding : this.resolve(binding);
185
+ binding instanceof BindingInstance
186
+ ? binding
187
+ : this.resolve(binding, true);
176
188
  let result = this.getModel().get(resolved, options);
177
189
 
178
190
  if (result === undefined && !options?.ignoreDefaultValue) {
@@ -185,6 +197,8 @@ export class DataController implements DataModelWithParser<DataModelOptions> {
185
197
 
186
198
  if (options?.formatted) {
187
199
  result = this.hooks.format.call(result, resolved);
200
+ } else if (options?.formatted === false) {
201
+ result = this.hooks.deformat.call(result, resolved);
188
202
  }
189
203
 
190
204
  this.hooks.onGet.call(binding, result);
@@ -192,53 +206,36 @@ export class DataController implements DataModelWithParser<DataModelOptions> {
192
206
  return result;
193
207
  }
194
208
 
195
- public delete(binding: BindingLike) {
196
- if (binding === undefined || binding === null) {
197
- throw new Error(`Invalid arguments: delete expects a data path (string)`);
209
+ public delete(binding: BindingLike, options?: DataModelOptions) {
210
+ if (
211
+ typeof binding !== 'string' &&
212
+ !Array.isArray(binding) &&
213
+ !(binding instanceof BindingInstance)
214
+ ) {
215
+ throw new Error('Invalid arguments: delete expects a data path (string)');
198
216
  }
199
217
 
200
- const resolved = this.resolve(binding);
201
- this.hooks.onDelete.call(resolved);
202
- this.deleteData(resolved);
203
- }
204
-
205
- public getTrash(): Set<BindingInstance> {
206
- return this.trash;
207
- }
208
-
209
- private addToTrash(binding: BindingInstance) {
210
- this.trash.add(binding);
211
- }
218
+ const resolved =
219
+ binding instanceof BindingInstance
220
+ ? binding
221
+ : this.resolve(binding, false);
212
222
 
213
- private deleteData(binding: BindingInstance) {
214
- const parentBinding = binding.parent();
215
- const parentPath = parentBinding.asString();
216
- const property = binding.key();
223
+ const parentBinding = resolved.parent();
224
+ const property = resolved.key();
225
+ const parentValue = this.get(parentBinding);
217
226
 
218
- const existedBeforeDelete = Object.prototype.hasOwnProperty.call(
219
- this.get(parentBinding),
220
- property
221
- );
227
+ const existedBeforeDelete =
228
+ typeof parentValue === 'object' &&
229
+ parentValue !== null &&
230
+ Object.prototype.hasOwnProperty.call(parentValue, property);
222
231
 
223
- if (property !== undefined) {
224
- const parent = parentBinding ? this.get(parentBinding) : undefined;
232
+ this.getModel().delete(resolved, options);
225
233
 
226
- // If we're deleting an item in an array, we just splice it out
227
- // Don't add it to the trash
228
- if (parentPath && Array.isArray(parent)) {
229
- if (parent.length > property) {
230
- this.set([[parentBinding, removeAt(parent, property as number)]]);
231
- }
232
- } else if (parentPath && parent[property]) {
233
- this.set([[parentBinding, omit(parent, property as string)]]);
234
- } else if (!parentPath) {
235
- this.getModel().reset(omit(this.get(''), property as string));
236
- }
234
+ if (existedBeforeDelete && !this.get(resolved)) {
235
+ this.trash.add(resolved);
237
236
  }
238
237
 
239
- if (existedBeforeDelete && !this.get(binding)) {
240
- this.addToTrash(binding);
241
- }
238
+ this.hooks.onDelete.call(resolved);
242
239
  }
243
240
 
244
241
  public serialize(): object {
@@ -29,6 +29,7 @@ export class FlowController {
29
29
  this.start = this.start.bind(this);
30
30
  this.run = this.run.bind(this);
31
31
  this.transition = this.transition.bind(this);
32
+ this.addNewFlow = this.addNewFlow.bind(this);
32
33
  }
33
34
 
34
35
  /** Navigate to another state in the state-machine */
@@ -40,20 +41,9 @@ export class FlowController {
40
41
  this.current.transition(stateTransition, options);
41
42
  }
42
43
 
43
- private async addNewFlow(flow: FlowInstance) {
44
+ private addNewFlow(flow: FlowInstance) {
44
45
  this.navStack.push(flow);
45
46
  this.current = flow;
46
- flow.hooks.transition.tap(
47
- 'flow-controller',
48
- async (_oldState, newState: NamedState) => {
49
- if (newState.value.state_type === 'FLOW') {
50
- this.log?.debug(`Got FLOW state. Loading flow ${newState.value.ref}`);
51
- const endState = await this.run(newState.value.ref);
52
- this.log?.debug(`Flow ended. Using outcome: ${endState.outcome}`);
53
- flow.transition(endState.outcome);
54
- }
55
- }
56
- );
57
47
  this.hooks.flow.call(flow);
58
48
  }
59
49
 
@@ -74,6 +64,20 @@ export class FlowController {
74
64
 
75
65
  const flow = new FlowInstance(startState, startFlow, { logger: this.log });
76
66
  this.addNewFlow(flow);
67
+
68
+ flow.hooks.afterTransition.tap('flow-controller', (flowInstance) => {
69
+ if (flowInstance.currentState?.value.state_type === 'FLOW') {
70
+ const subflowId = flowInstance.currentState?.value.ref;
71
+ this.log?.debug(`Loading subflow ${subflowId}`);
72
+ this.run(subflowId).then((subFlowEndState) => {
73
+ this.log?.debug(
74
+ `Subflow ended. Using outcome: ${subFlowEndState.outcome}`
75
+ );
76
+ flowInstance.transition(subFlowEndState?.outcome);
77
+ });
78
+ }
79
+ });
80
+
77
81
  const end = await flow.start();
78
82
  this.navStack.pop();
79
83
 
@@ -58,6 +58,9 @@ export class FlowInstance {
58
58
 
59
59
  /** A callback when a transition from 1 state to another was made */
60
60
  transition: new SyncHook<[NamedState | undefined, NamedState]>(),
61
+
62
+ /** A callback to run actions after a transition occurs */
63
+ afterTransition: new SyncHook<[FlowInstance]>(),
61
64
  };
62
65
 
63
66
  constructor(
@@ -131,7 +134,7 @@ export class FlowInstance {
131
134
 
132
135
  if (skipTransition) {
133
136
  this.log?.debug(
134
- `Skipping transition from ${this.currentState} b/c hook told us to`
137
+ `Skipping transition from ${this.currentState.name} b/c hook told us to`
135
138
  );
136
139
  return;
137
140
  }
@@ -201,5 +204,7 @@ export class FlowInstance {
201
204
  this.hooks.transition.call(prevState, {
202
205
  ...newCurrentState,
203
206
  });
207
+
208
+ this.hooks.afterTransition.call(this);
204
209
  }
205
210
  }
@@ -13,6 +13,9 @@ const CONTEXT = 'validation-binding-tracker';
13
13
  export interface BindingTracker {
14
14
  /** Get the bindings currently being tracked for validation */
15
15
  getBindings(): Set<BindingInstance>;
16
+
17
+ /** Add a binding to the tracked set */
18
+ trackBinding(binding: BindingInstance): void;
16
19
  }
17
20
  interface Options {
18
21
  /** Parse a binding from a view */
@@ -42,6 +45,16 @@ export class ValidationBindingTrackerViewPlugin
42
45
  return this.trackedBindings;
43
46
  }
44
47
 
48
+ /** Add a binding to the tracked set */
49
+ trackBinding(binding: BindingInstance) {
50
+ if (this.trackedBindings.has(binding)) {
51
+ return;
52
+ }
53
+
54
+ this.trackedBindings.add(binding);
55
+ this.options.callbacks?.onAdd?.(binding);
56
+ }
57
+
45
58
  /** Attach hooks to the given resolver */
46
59
  applyResolver(resolver: Resolver) {
47
60
  this.trackedBindings.clear();
@@ -52,9 +65,6 @@ export class ValidationBindingTrackerViewPlugin
52
65
  /** Each Node is a registered section or page that maps to a set of nodes in its section */
53
66
  const sections = new Map<Node.Node, Set<Node.Node>>();
54
67
 
55
- /** Keep track of all seen bindings so we can notify people when we hit one for the first time */
56
- const seenBindings = new Set<BindingInstance>();
57
-
58
68
  let lastViewUpdateChangeSet: Set<BindingInstance> | undefined;
59
69
 
60
70
  const nodeTree = new Map<Node.Node, Set<Node.Node>>();
@@ -129,10 +139,8 @@ export class ValidationBindingTrackerViewPlugin
129
139
  }
130
140
  }
131
141
 
132
- if (!seenBindings.has(parsed)) {
133
- seenBindings.add(parsed);
134
- this.options.callbacks?.onAdd?.(parsed);
135
- }
142
+ this.trackedBindings.add(parsed);
143
+ this.options.callbacks?.onAdd?.(parsed);
136
144
  };
137
145
 
138
146
  return {
@@ -144,23 +152,36 @@ export class ValidationBindingTrackerViewPlugin
144
152
  track(binding);
145
153
  }
146
154
 
147
- const eow = options.validation?._getValidationForBinding(binding);
155
+ const eows = options.validation
156
+ ?._getValidationForBinding(binding)
157
+ ?.getAll(getOptions);
158
+
159
+ const firstFieldEOW = eows?.find(
160
+ (eow) =>
161
+ eow.displayTarget === 'field' || eow.displayTarget === undefined
162
+ );
148
163
 
149
- if (
150
- eow?.displayTarget === undefined ||
151
- eow?.displayTarget === 'field'
152
- ) {
153
- return eow;
164
+ return firstFieldEOW;
165
+ },
166
+ getValidationsForBinding(binding, getOptions) {
167
+ if (getOptions?.track) {
168
+ track(binding);
154
169
  }
155
170
 
156
- return undefined;
171
+ return (
172
+ options.validation
173
+ ?._getValidationForBinding(binding)
174
+ ?.getAll(getOptions) ?? []
175
+ );
157
176
  },
158
- getChildren: (type: Validation.DisplayTarget) => {
177
+ getChildren: (type?: Validation.DisplayTarget) => {
159
178
  const validations = new Array<ValidationResponse>();
160
179
  lastComputedBindingTree.get(node)?.forEach((binding) => {
161
- const eow = options.validation?._getValidationForBinding(binding);
180
+ const eow = options.validation
181
+ ?._getValidationForBinding(binding)
182
+ ?.get();
162
183
 
163
- if (eow && type === eow.displayTarget) {
184
+ if (eow && (type === undefined || type === eow.displayTarget)) {
164
185
  validations.push(eow);
165
186
  }
166
187
  });
@@ -170,7 +191,9 @@ export class ValidationBindingTrackerViewPlugin
170
191
  getValidationsForSection: () => {
171
192
  const validations = new Array<ValidationResponse>();
172
193
  lastSectionBindingTree.get(node)?.forEach((binding) => {
173
- const eow = options.validation?._getValidationForBinding(binding);
194
+ const eow = options.validation
195
+ ?._getValidationForBinding(binding)
196
+ ?.get();
174
197
 
175
198
  if (eow && eow.displayTarget === 'section') {
176
199
  validations.push(eow);
@@ -213,7 +236,7 @@ export class ValidationBindingTrackerViewPlugin
213
236
  }
214
237
 
215
238
  if (node === resolver.root) {
216
- this.trackedBindings = currentBindingTree.get(node) ?? new Set();
239
+ this.trackedBindings = new Set(currentBindingTree.get(node));
217
240
  lastComputedBindingTree = currentBindingTree;
218
241
 
219
242
  lastSectionBindingTree.clear();