@player-ui/player 0.4.0 → 0.4.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.
Files changed (47) hide show
  1. package/dist/index.cjs.js +795 -390
  2. package/dist/index.d.ts +238 -81
  3. package/dist/index.esm.js +787 -388
  4. package/dist/player.dev.js +4768 -5282
  5. package/dist/player.prod.js +1 -1
  6. package/package.json +12 -3
  7. package/src/binding/binding.ts +8 -0
  8. package/src/binding/index.ts +14 -4
  9. package/src/binding/resolver.ts +1 -1
  10. package/src/binding-grammar/custom/index.ts +17 -9
  11. package/src/controllers/constants/index.ts +9 -5
  12. package/src/controllers/{data.ts → data/controller.ts} +60 -61
  13. package/src/controllers/data/index.ts +1 -0
  14. package/src/controllers/data/utils.ts +42 -0
  15. package/src/controllers/flow/controller.ts +16 -12
  16. package/src/controllers/flow/flow.ts +6 -1
  17. package/src/controllers/index.ts +1 -1
  18. package/src/controllers/validation/binding-tracker.ts +42 -19
  19. package/src/controllers/validation/controller.ts +359 -145
  20. package/src/controllers/view/asset-transform.ts +4 -1
  21. package/src/controllers/view/controller.ts +20 -3
  22. package/src/data/dependency-tracker.ts +14 -0
  23. package/src/data/local-model.ts +25 -1
  24. package/src/data/model.ts +55 -8
  25. package/src/data/noop-model.ts +2 -0
  26. package/src/expressions/evaluator-functions.ts +24 -2
  27. package/src/expressions/evaluator.ts +37 -33
  28. package/src/expressions/index.ts +1 -0
  29. package/src/expressions/parser.ts +53 -27
  30. package/src/expressions/types.ts +23 -5
  31. package/src/expressions/utils.ts +19 -0
  32. package/src/player.ts +47 -48
  33. package/src/plugins/default-exp-plugin.ts +57 -0
  34. package/src/plugins/flow-exp-plugin.ts +2 -2
  35. package/src/schema/schema.ts +28 -9
  36. package/src/string-resolver/index.ts +25 -9
  37. package/src/types.ts +6 -3
  38. package/src/validator/binding-map-splice.ts +59 -0
  39. package/src/validator/index.ts +1 -0
  40. package/src/validator/types.ts +11 -3
  41. package/src/validator/validation-middleware.ts +38 -4
  42. package/src/view/parser/index.ts +51 -3
  43. package/src/view/plugins/applicability.ts +1 -1
  44. package/src/view/plugins/string-resolver.ts +8 -4
  45. package/src/view/plugins/template-plugin.ts +1 -6
  46. package/src/view/resolver/index.ts +119 -54
  47. package/src/view/resolver/types.ts +48 -7
@@ -83,6 +83,21 @@ export class Parser {
83
83
  return tapped;
84
84
  }
85
85
 
86
+ /**
87
+ * Checks if there are templated values in the object
88
+ *
89
+ * @param obj - The Parsed Object to check to see if we have a template array type for
90
+ * @param localKey - The key being checked
91
+ */
92
+ private hasTemplateValues(obj: any, localKey: string) {
93
+ return (
94
+ Object.hasOwnProperty.call(obj, 'template') &&
95
+ Array.isArray(obj?.template) &&
96
+ obj.template.length &&
97
+ obj.template.find((tmpl: any) => tmpl.output === localKey)
98
+ );
99
+ }
100
+
86
101
  public parseObject(
87
102
  obj: object,
88
103
  type: Node.ChildrenTypes = NodeType.Value,
@@ -120,7 +135,13 @@ export class Parser {
120
135
 
121
136
  const objEntries = Array.isArray(localObj)
122
137
  ? localObj.map((v, i) => [i, v])
123
- : Object.entries(localObj);
138
+ : [
139
+ ...Object.entries(localObj),
140
+ ...Object.getOwnPropertySymbols(localObj).map((s) => [
141
+ s,
142
+ (localObj as any)[s],
143
+ ]),
144
+ ];
124
145
 
125
146
  const defaultValue: NestedObj = {
126
147
  children: [],
@@ -166,6 +187,13 @@ export class Parser {
166
187
  template
167
188
  );
168
189
 
190
+ if (templateAST?.type === NodeType.MultiNode) {
191
+ templateAST.values.forEach((v) => {
192
+ // eslint-disable-next-line no-param-reassign
193
+ v.parent = templateAST;
194
+ });
195
+ }
196
+
169
197
  if (templateAST) {
170
198
  return {
171
199
  path: [...path, template.output],
@@ -193,6 +221,26 @@ export class Parser {
193
221
  NodeType.Switch
194
222
  );
195
223
 
224
+ if (
225
+ localSwitch &&
226
+ localSwitch.type === NodeType.Value &&
227
+ localSwitch.children?.length === 1 &&
228
+ localSwitch.value === undefined
229
+ ) {
230
+ const firstChild = localSwitch.children[0];
231
+
232
+ return {
233
+ ...rest,
234
+ children: [
235
+ ...children,
236
+ {
237
+ path: [...path, localKey, ...firstChild.path],
238
+ value: firstChild.value,
239
+ },
240
+ ],
241
+ };
242
+ }
243
+
196
244
  if (localSwitch) {
197
245
  return {
198
246
  ...rest,
@@ -216,7 +264,7 @@ export class Parser {
216
264
  const multiNode = this.hooks.onCreateASTNode.call(
217
265
  {
218
266
  type: NodeType.MultiNode,
219
- override: true,
267
+ override: !this.hasTemplateValues(localObj, localKey),
220
268
  values: childValues,
221
269
  },
222
270
  localValue
@@ -249,7 +297,7 @@ export class Parser {
249
297
  if (determineNodeType === NodeType.Applicability) {
250
298
  const parsedNode = this.hooks.parseNode.call(
251
299
  localValue,
252
- type,
300
+ NodeType.Value,
253
301
  options,
254
302
  determineNodeType
255
303
  );
@@ -16,7 +16,7 @@ export default class ApplicabilityPlugin implements ViewPlugin {
16
16
  if (node?.type === NodeType.Applicability) {
17
17
  const isApplicable = options.evaluate(node.expression);
18
18
 
19
- if (!isApplicable) {
19
+ if (isApplicable === false) {
20
20
  return null;
21
21
  }
22
22
 
@@ -87,15 +87,19 @@ export function resolveAllRefs(
87
87
  }
88
88
 
89
89
  /** Traverse up the node tree finding the first available 'path' */
90
- const findBasePath = (node: Node.Node): Node.PathSegment[] => {
90
+ const findBasePath = (
91
+ node: Node.Node,
92
+ resolver: Resolver
93
+ ): Node.PathSegment[] => {
91
94
  const parentNode = node.parent;
92
95
  if (!parentNode) {
93
96
  return [];
94
97
  }
95
98
 
96
99
  if ('children' in parentNode) {
100
+ const original = resolver.getSourceNode(node);
97
101
  return (
98
- parentNode.children?.find((child) => child.value === node)?.path ?? []
102
+ parentNode.children?.find((child) => child.value === original)?.path ?? []
99
103
  );
100
104
  }
101
105
 
@@ -103,7 +107,7 @@ const findBasePath = (node: Node.Node): Node.PathSegment[] => {
103
107
  return [];
104
108
  }
105
109
 
106
- return findBasePath(parentNode);
110
+ return findBasePath(parentNode, resolver);
107
111
  };
108
112
 
109
113
  /** A plugin that resolves all string references for each node */
@@ -148,7 +152,7 @@ export default class StringResolverPlugin implements ViewPlugin {
148
152
  propsToSkip = new Set(['exp']);
149
153
  }
150
154
 
151
- const nodePath = findBasePath(node);
155
+ const nodePath = findBasePath(node, resolver);
152
156
 
153
157
  /** If the path includes something that is supposed to be skipped, this node should be skipped too. */
154
158
  if (
@@ -95,16 +95,11 @@ export default class TemplatePlugin implements ViewPlugin {
95
95
  });
96
96
 
97
97
  const result: Node.MultiNode = {
98
- parent: node.parent,
99
98
  type: NodeType.MultiNode,
99
+ override: false,
100
100
  values,
101
101
  };
102
102
 
103
- result.values.forEach((innerNode) => {
104
- // eslint-disable-next-line no-param-reassign
105
- innerNode.parent = result;
106
- });
107
-
108
103
  return result;
109
104
  }
110
105
 
@@ -1,5 +1,5 @@
1
1
  import { SyncWaterfallHook, SyncHook } from 'tapable-ts';
2
- import { setIn, addLast } from 'timm';
2
+ import { setIn, addLast, clone } from 'timm';
3
3
  import dlv from 'dlv';
4
4
  import { dequal } from 'dequal';
5
5
  import type { BindingInstance, BindingLike } from '../../binding';
@@ -42,6 +42,12 @@ const withContext = (model: DataModelWithParser): DataModelWithParser => {
42
42
  ...options,
43
43
  });
44
44
  },
45
+ delete: (binding: BindingLike, options?: DataModelOptions): void => {
46
+ return model.delete(binding, {
47
+ context: { model },
48
+ ...options,
49
+ });
50
+ },
45
51
  };
46
52
  };
47
53
 
@@ -141,6 +147,7 @@ export class Resolver {
141
147
  this.hooks.beforeUpdate.call(changes);
142
148
  const resolveCache = new Map<Node.Node, Resolve.ResolvedNode>();
143
149
  this.idCache.clear();
150
+ const prevASTMap = new Map(this.ASTMap);
144
151
  this.ASTMap.clear();
145
152
 
146
153
  const updated = this.computeTree(
@@ -148,7 +155,9 @@ export class Resolver {
148
155
  undefined,
149
156
  changes,
150
157
  resolveCache,
151
- toNodeResolveOptions(this.options)
158
+ toNodeResolveOptions(this.options),
159
+ undefined,
160
+ prevASTMap
152
161
  );
153
162
  this.resolveCache = resolveCache;
154
163
  this.hooks.afterUpdate.call(updated.value);
@@ -156,6 +165,10 @@ export class Resolver {
156
165
  return updated.value;
157
166
  }
158
167
 
168
+ public getResolveCache() {
169
+ return new Map(this.resolveCache);
170
+ }
171
+
159
172
  private getNodeID(node?: Node.Node): string | undefined {
160
173
  if (!node) {
161
174
  return;
@@ -206,12 +219,29 @@ export class Resolver {
206
219
  return this.resolveCache.get(node);
207
220
  }
208
221
 
222
+ private cloneNode(node: any) {
223
+ const clonedNode = clone(node);
224
+
225
+ Object.keys(clonedNode).forEach((key) => {
226
+ if (key === 'parent') return;
227
+
228
+ const value = clonedNode[key];
229
+ if (typeof value === 'object' && value !== null) {
230
+ clonedNode[key] = Array.isArray(value) ? [...value] : { ...value };
231
+ }
232
+ });
233
+
234
+ return clonedNode;
235
+ }
236
+
209
237
  private computeTree(
210
238
  node: Node.Node,
211
239
  parent: Node.Node | undefined,
212
240
  dataChanges: Set<BindingInstance> | undefined,
213
241
  cacheUpdate: Map<Node.Node, Resolve.ResolvedNode>,
214
- options: Resolve.NodeResolveOptions
242
+ options: Resolve.NodeResolveOptions,
243
+ parentNode: Node.Node | undefined,
244
+ prevASTMap: Map<Node.Node, Node.Node>
215
245
  ): NodeUpdate {
216
246
  const dependencyModel = new DependencyModel(options.data.model);
217
247
 
@@ -254,43 +284,65 @@ export class Resolver {
254
284
 
255
285
  /** Recursively repopulate the AST map given some AST Node and it's resolved AST representation */
256
286
  const repopulateASTMapFromCache = (
257
- resolvedAST: Node.Node,
258
- AST: Node.Node
287
+ resolvedNode: Resolve.ResolvedNode,
288
+ AST: Node.Node,
289
+ ASTParent: Node.Node | undefined
259
290
  ) => {
291
+ const { node: resolvedAST } = resolvedNode;
260
292
  this.ASTMap.set(resolvedAST, AST);
293
+ const resolvedUpdate = {
294
+ ...resolvedNode,
295
+ updated: false,
296
+ };
297
+ cacheUpdate.set(AST, resolvedUpdate);
298
+
299
+ /** Helper function for recursing over child node */
300
+ const handleChildNode = (childNode: Node.Node) => {
301
+ // In order to get the correct results, we need to use the node references from the last update.
302
+ const originalChildNode = prevASTMap.get(childNode) ?? childNode;
303
+ const previousChildResult = this.getPreviousResult(originalChildNode);
304
+ if (!previousChildResult) return;
305
+
306
+ repopulateASTMapFromCache(
307
+ previousChildResult,
308
+ originalChildNode,
309
+ AST
310
+ );
311
+ };
312
+
261
313
  if ('children' in resolvedAST) {
262
- resolvedAST.children?.forEach(({ value: childAST }) => {
263
- const { node: childResolvedAST } =
264
- this.getPreviousResult(childAST) || {};
265
- if (!childResolvedAST) return;
266
-
267
- repopulateASTMapFromCache(childResolvedAST, childAST);
268
-
269
- if (childResolvedAST.type === NodeType.MultiNode) {
270
- childResolvedAST.values.forEach((mChildAST) => {
271
- const { node: mChildResolvedAST } =
272
- this.getPreviousResult(mChildAST) || {};
273
- if (!mChildResolvedAST) return;
274
-
275
- repopulateASTMapFromCache(mChildResolvedAST, mChildAST);
276
- });
277
- }
278
- });
314
+ resolvedAST.children?.forEach(({ value: childAST }) =>
315
+ handleChildNode(childAST)
316
+ );
317
+ } else if (resolvedAST.type === NodeType.MultiNode) {
318
+ resolvedAST.values.forEach(handleChildNode);
279
319
  }
320
+
321
+ this.hooks.afterNodeUpdate.call(AST, ASTParent, resolvedUpdate);
280
322
  };
281
323
 
282
- const resolvedAST = previousResult.node;
283
- repopulateASTMapFromCache(resolvedAST, node);
324
+ // Point the root of the cached node to the new resolved node.
325
+ previousResult.node.parent = parentNode;
284
326
 
285
- this.hooks.afterNodeUpdate.call(node, parent, update);
327
+ repopulateASTMapFromCache(previousResult, node, parent);
286
328
 
287
329
  return update;
288
330
  }
289
331
 
290
- const resolvedAST = this.hooks.beforeResolve.call(node, resolveOptions) ?? {
332
+ // Shallow clone the node so that changes to it during the resolve steps don't impact the original.
333
+ // We are trusting that this becomes a deep clone once the whole node tree has been traversed.
334
+ const clonedNode = {
335
+ ...this.cloneNode(node),
336
+ parent: parentNode,
337
+ };
338
+ const resolvedAST = this.hooks.beforeResolve.call(
339
+ clonedNode,
340
+ resolveOptions
341
+ ) ?? {
291
342
  type: NodeType.Empty,
292
343
  };
293
344
 
345
+ resolvedAST.parent = parentNode;
294
346
  resolveOptions.node = resolvedAST;
295
347
 
296
348
  this.ASTMap.set(resolvedAST, node);
@@ -311,43 +363,25 @@ export class Resolver {
311
363
  dependencyModel.trackSubset('children');
312
364
 
313
365
  if ('children' in resolvedAST) {
314
- resolvedAST.children?.forEach((child) => {
366
+ const newChildren = resolvedAST.children?.map((child) => {
315
367
  const computedChildTree = this.computeTree(
316
368
  child.value,
317
369
  node,
318
370
  dataChanges,
319
371
  cacheUpdate,
320
- resolveOptions
372
+ resolveOptions,
373
+ resolvedAST,
374
+ prevASTMap
321
375
  );
322
- let { updated: childUpdated, value: childValue } = computedChildTree;
323
- const { node: childNode, dependencies: childTreeDeps } =
324
- computedChildTree;
376
+ const {
377
+ dependencies: childTreeDeps,
378
+ node: childNode,
379
+ updated: childUpdated,
380
+ value: childValue,
381
+ } = computedChildTree;
325
382
 
326
383
  childTreeDeps.forEach((binding) => childDependencies.add(binding));
327
384
 
328
- if (childNode.type === NodeType.MultiNode) {
329
- childValue = [];
330
- childNode.values.forEach((mValue) => {
331
- const mTree = this.computeTree(
332
- mValue,
333
- node,
334
- dataChanges,
335
- cacheUpdate,
336
- resolveOptions
337
- );
338
-
339
- if (mTree.value !== undefined && mTree.value !== null) {
340
- childValue.push(mTree.value);
341
- }
342
-
343
- mTree.dependencies.forEach((bindingDep) =>
344
- childDependencies.add(bindingDep)
345
- );
346
-
347
- childUpdated = childUpdated || mTree.updated;
348
- });
349
- }
350
-
351
385
  if (childValue) {
352
386
  if (childNode.type === NodeType.MultiNode && !childNode.override) {
353
387
  const arr = addLast(
@@ -361,7 +395,38 @@ export class Resolver {
361
395
  }
362
396
 
363
397
  updated = updated || childUpdated;
398
+
399
+ return { ...child, value: childNode };
364
400
  });
401
+
402
+ resolvedAST.children = newChildren;
403
+ } else if (resolvedAST.type === NodeType.MultiNode) {
404
+ const childValue: any = [];
405
+ const newValues = resolvedAST.values.map((mValue) => {
406
+ const mTree = this.computeTree(
407
+ mValue,
408
+ node,
409
+ dataChanges,
410
+ cacheUpdate,
411
+ resolveOptions,
412
+ resolvedAST,
413
+ prevASTMap
414
+ );
415
+ if (mTree.value !== undefined && mTree.value !== null) {
416
+ childValue.push(mTree.value);
417
+ }
418
+
419
+ mTree.dependencies.forEach((bindingDep) =>
420
+ childDependencies.add(bindingDep)
421
+ );
422
+
423
+ updated = updated || mTree.updated;
424
+
425
+ return mTree.node;
426
+ });
427
+
428
+ resolvedAST.values = newValues;
429
+ resolved = childValue;
365
430
  }
366
431
 
367
432
  childDependencies.forEach((bindingDep) =>
@@ -13,6 +13,7 @@ import type {
13
13
  DataModelImpl,
14
14
  DataModelOptions,
15
15
  } from '../../data';
16
+ import type { ConstantsProvider } from '../../controllers/constants';
16
17
  import type { TransitionFunction } from '../../controllers';
17
18
  import type { ExpressionEvaluator, ExpressionType } from '../../expressions';
18
19
  import type { ValidationResponse } from '../../validator';
@@ -20,30 +21,64 @@ import type { Logger } from '../../logger';
20
21
  import type { SchemaController } from '../../schema';
21
22
  import type { Node } from '../parser';
22
23
 
24
+ export interface ValidationGetResolveOptions {
25
+ /**
26
+ * If we should ignore any non-blocking validations in the return
27
+ * @default true
28
+ */
29
+ ignoreNonBlocking?: boolean;
30
+ }
31
+
32
+ export interface PlayerUtils {
33
+ findPlugin<Plugin = unknown>(symbol: symbol): Plugin | undefined;
34
+ }
35
+
23
36
  export declare namespace Resolve {
24
37
  export interface Validation {
25
38
  /** Fetch the data-type for the given binding */
26
39
  type(binding: BindingLike): Schema.DataType | undefined;
27
40
 
28
41
  /** Get all currently applicable validation errors */
29
- getAll(): Map<BindingInstance, ValidationResponse> | undefined;
42
+ getAll(
43
+ options?: ValidationGetResolveOptions
44
+ ): Map<BindingInstance, ValidationResponse> | undefined;
30
45
 
31
46
  /** Internal Method to lookup if there is a validation for the given binding */
32
- _getValidationForBinding(
33
- binding: BindingLike
34
- ): ValidationResponse | undefined;
47
+ _getValidationForBinding(binding: BindingLike):
48
+ | {
49
+ /** Get the validation for the given binding */
50
+ get: (
51
+ options?: ValidationGetResolveOptions
52
+ ) => ValidationResponse | undefined;
53
+
54
+ /** Get all validations for the given binding */
55
+ getAll: (
56
+ options?: ValidationGetResolveOptions
57
+ ) => Array<ValidationResponse>;
58
+ }
59
+ | undefined;
35
60
 
36
61
  /** Get field level error for the specific binding */
37
62
  get(
38
63
  binding: BindingLike,
39
64
  options?: {
40
65
  /** If this binding should also be tracked for validations */
41
- track: boolean;
42
- }
66
+ track?: boolean;
67
+ } & ValidationGetResolveOptions
43
68
  ): ValidationResponse | undefined;
44
69
 
70
+ getValidationsForBinding(
71
+ binding: BindingLike,
72
+ options?: {
73
+ /** If this binding should also be tracked for validations */
74
+ track?: boolean;
75
+ } & ValidationGetResolveOptions
76
+ ): Array<ValidationResponse>;
77
+
45
78
  /** Get errors for all children regardless of section */
46
- getChildren(type: ValidationTypes.DisplayTarget): Array<ValidationResponse>;
79
+ getChildren(
80
+ type?: ValidationTypes.DisplayTarget
81
+ ): Array<ValidationResponse>;
47
82
 
48
83
  /** Get errors for all children solely in this section */
49
84
  getValidationsForSection(): Array<ValidationResponse>;
@@ -62,6 +97,9 @@ export declare namespace Resolve {
62
97
  /** A logger to use */
63
98
  logger?: Logger;
64
99
 
100
+ /** Utils for various useful operations */
101
+ utils?: PlayerUtils;
102
+
65
103
  /** An optional set of validation features */
66
104
  validation?: Validation;
67
105
 
@@ -73,6 +111,9 @@ export declare namespace Resolve {
73
111
 
74
112
  /** The hub for data invariants and metaData associated with the data model */
75
113
  schema: SchemaController;
114
+
115
+ /** The constants for messages */
116
+ constants?: ConstantsProvider;
76
117
  }
77
118
 
78
119
  export interface NodeDataOptions {