@player-ui/player 0.3.1-next.1 → 0.3.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 +899 -336
  2. package/dist/index.d.ts +275 -93
  3. package/dist/index.esm.js +890 -334
  4. package/dist/player.dev.js +11429 -0
  5. package/dist/player.prod.js +2 -0
  6. package/package.json +16 -5
  7. package/src/binding/binding.ts +8 -0
  8. package/src/binding/index.ts +14 -4
  9. package/src/binding/resolver.ts +2 -4
  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} +62 -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 +375 -148
  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 +60 -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 +38 -34
  28. package/src/expressions/index.ts +1 -0
  29. package/src/expressions/parser.ts +116 -44
  30. package/src/expressions/types.ts +50 -17
  31. package/src/expressions/utils.ts +143 -1
  32. package/src/player.ts +60 -46
  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 +26 -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 +58 -6
  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 +35 -9
  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
@@ -0,0 +1,59 @@
1
+ import type { BindingInstance } from '../binding';
2
+
3
+ /**
4
+ * Remove a binding, and any children from from the map
5
+ * If the binding is an array-item, then it will be spliced from the array and the others will be shifted down
6
+ *
7
+ * @param sourceMap - A map of bindings to values
8
+ * @param binding - The binding to remove from the map
9
+ */
10
+ export function removeBindingAndChildrenFromMap<T>(
11
+ sourceMap: Map<BindingInstance, T>,
12
+ binding: BindingInstance
13
+ ): Map<BindingInstance, T> {
14
+ const targetMap = new Map(sourceMap);
15
+
16
+ const parentBinding = binding.parent();
17
+ const property = binding.key();
18
+
19
+ // Clear out any that are sub-bindings of this binding
20
+
21
+ targetMap.forEach((_value, trackedBinding) => {
22
+ if (binding === trackedBinding || binding.contains(trackedBinding)) {
23
+ targetMap.delete(trackedBinding);
24
+ }
25
+ });
26
+
27
+ if (typeof property === 'number') {
28
+ // Splice out this index from the rest
29
+
30
+ // Order matters here b/c we are shifting items in the array
31
+ // Start with the smallest index and work our way down
32
+ const bindingsToRewrite = Array.from(sourceMap.keys())
33
+ .filter((b) => {
34
+ if (parentBinding.contains(b)) {
35
+ const [childIndex] = b.relative(parentBinding);
36
+ return typeof childIndex === 'number' && childIndex > property;
37
+ }
38
+
39
+ return false;
40
+ })
41
+ .sort();
42
+
43
+ bindingsToRewrite.forEach((trackedBinding) => {
44
+ // If the tracked binding is a sub-binding of the parent binding, then we need to
45
+ // update the path to reflect the new index
46
+
47
+ const [childIndex, ...childPath] = trackedBinding.relative(parentBinding);
48
+
49
+ if (typeof childIndex === 'number') {
50
+ const newSegments = [childIndex - 1, ...childPath];
51
+ const newChildBinding = parentBinding.descendent(newSegments);
52
+ targetMap.set(newChildBinding, targetMap.get(trackedBinding) as T);
53
+ targetMap.delete(trackedBinding);
54
+ }
55
+ });
56
+ }
57
+
58
+ return targetMap;
59
+ }
@@ -1,3 +1,4 @@
1
1
  export * from './validation-middleware';
2
2
  export * from './types';
3
3
  export * from './registry';
4
+ export * from './binding-map-splice';
@@ -1,4 +1,4 @@
1
- import type { Validation } from '@player-ui/types';
1
+ import type { Schema, Validation } from '@player-ui/types';
2
2
 
3
3
  import type { BindingInstance, BindingFactory } from '../binding';
4
4
  import type { DataModelWithParser } from '../data';
@@ -40,12 +40,17 @@ type RequiredValidationKeys = 'severity' | 'trigger';
40
40
  export type ValidationObject = Validation.Reference &
41
41
  Required<Pick<Validation.Reference, RequiredValidationKeys>>;
42
42
 
43
+ export type ValidationObjectWithHandler = ValidationObject & {
44
+ /** A predefined handler for this validation object */
45
+ handler?: ValidatorFunction;
46
+ };
47
+
43
48
  export interface ValidationProvider {
44
49
  getValidationsForBinding?(
45
50
  binding: BindingInstance
46
- ): Array<ValidationObject> | undefined;
51
+ ): Array<ValidationObjectWithHandler> | undefined;
47
52
 
48
- getValidationsForView?(): Array<ValidationObject> | undefined;
53
+ getValidationsForView?(): Array<ValidationObjectWithHandler> | undefined;
49
54
  }
50
55
 
51
56
  export interface ValidatorContext {
@@ -66,6 +71,9 @@ export interface ValidatorContext {
66
71
 
67
72
  /** The constants for messages */
68
73
  constants: ConstantsProvider;
74
+
75
+ /** The type in the schema that triggered the validation if there is one */
76
+ schemaType: Schema.DataType | undefined;
69
77
  }
70
78
 
71
79
  export type ValidatorFunction<Options = unknown> = (
@@ -11,6 +11,17 @@ import { toModel } from '../data';
11
11
  import type { Logger } from '../logger';
12
12
 
13
13
  import type { ValidationResponse } from './types';
14
+ import { removeBindingAndChildrenFromMap } from './binding-map-splice';
15
+
16
+ /**
17
+ * A BindingInstance with an indicator of whether or not it's a strong binding
18
+ */
19
+ export type StrongOrWeakBinding = {
20
+ /** BindingInstance in question */
21
+ binding: BindingInstance;
22
+ /** Boolean indicating whether the relevant BindingInstance is a strong binding */
23
+ isStrong: boolean;
24
+ };
14
25
 
15
26
  /**
16
27
  * Returns a validation object if the data is invalid or an set of BindingsInstances if the binding itself is a weak ref of another invalid validation
@@ -18,7 +29,7 @@ import type { ValidationResponse } from './types';
18
29
  export type MiddlewareChecker = (
19
30
  binding: BindingInstance,
20
31
  model: DataModelImpl
21
- ) => ValidationResponse | Set<BindingInstance> | undefined;
32
+ ) => ValidationResponse | Set<StrongOrWeakBinding> | undefined;
22
33
 
23
34
  /**
24
35
  * Middleware for the data-model that caches the results of invalid data
@@ -27,17 +38,21 @@ export class ValidationMiddleware implements DataModelMiddleware {
27
38
  public validator: MiddlewareChecker;
28
39
  public shadowModelPaths: Map<BindingInstance, any>;
29
40
  private logger?: Logger;
41
+ private shouldIncludeInvalid?: (options?: DataModelOptions) => boolean;
30
42
 
31
43
  constructor(
32
44
  validator: MiddlewareChecker,
33
45
  options?: {
34
46
  /** A logger instance */
35
47
  logger?: Logger;
48
+ /** Optional function to include data staged in shadowModel */
49
+ shouldIncludeInvalid?: (options?: DataModelOptions) => boolean;
36
50
  }
37
51
  ) {
38
52
  this.validator = validator;
39
53
  this.shadowModelPaths = new Map();
40
54
  this.logger = options?.logger;
55
+ this.shouldIncludeInvalid = options?.shouldIncludeInvalid;
41
56
  }
42
57
 
43
58
  public set(
@@ -48,8 +63,11 @@ export class ValidationMiddleware implements DataModelMiddleware {
48
63
  const asModel = toModel(this, { ...options, includeInvalid: true }, next);
49
64
  const nextTransaction: BatchSetTransaction = [];
50
65
 
66
+ const includedBindings = new Set<BindingInstance>();
67
+
51
68
  transaction.forEach(([binding, value]) => {
52
69
  this.shadowModelPaths.set(binding, value);
70
+ includedBindings.add(binding);
53
71
  });
54
72
 
55
73
  const invalidBindings: Array<BindingInstance> = [];
@@ -60,8 +78,17 @@ export class ValidationMiddleware implements DataModelMiddleware {
60
78
  if (validations === undefined) {
61
79
  nextTransaction.push([binding, value]);
62
80
  } else if (validations instanceof Set) {
63
- invalidBindings.push(...validations);
64
- } else {
81
+ validations.forEach((validation) => {
82
+ invalidBindings.push(validation.binding);
83
+ if (
84
+ !validation.isStrong &&
85
+ validation.binding.asString() === binding.asString()
86
+ ) {
87
+ nextTransaction.push([validation.binding, value]);
88
+ }
89
+ });
90
+ } else if (includedBindings.has(binding)) {
91
+ invalidBindings.push(binding);
65
92
  this.logger?.debug(
66
93
  `Invalid value for path: ${binding.asString()} - ${
67
94
  validations.severity
@@ -70,15 +97,22 @@ export class ValidationMiddleware implements DataModelMiddleware {
70
97
  }
71
98
  });
72
99
 
100
+ let validResults: Updates = [];
101
+
73
102
  if (next && nextTransaction.length > 0) {
74
103
  // defer clearing the shadow model to prevent validations that are run twice due to weak binding refs still needing the data
75
104
  nextTransaction.forEach(([binding]) =>
76
105
  this.shadowModelPaths.delete(binding)
77
106
  );
78
- return next.set(nextTransaction, options);
107
+ const result = next.set(nextTransaction, options);
108
+ if (invalidBindings.length === 0) {
109
+ return result;
110
+ }
111
+
112
+ validResults = result;
79
113
  }
80
114
 
81
- return invalidBindings.map((binding) => {
115
+ const invalidResults = invalidBindings.map((binding) => {
82
116
  return {
83
117
  binding,
84
118
  oldValue: asModel.get(binding),
@@ -86,6 +120,8 @@ export class ValidationMiddleware implements DataModelMiddleware {
86
120
  force: true,
87
121
  };
88
122
  });
123
+
124
+ return [...validResults, ...invalidResults];
89
125
  }
90
126
 
91
127
  public get(
@@ -95,7 +131,10 @@ export class ValidationMiddleware implements DataModelMiddleware {
95
131
  ) {
96
132
  let val = next?.get(binding, options);
97
133
 
98
- if (options?.includeInvalid === true) {
134
+ if (
135
+ this.shouldIncludeInvalid?.(options) ??
136
+ options?.includeInvalid === true
137
+ ) {
99
138
  this.shadowModelPaths.forEach((shadowValue, shadowBinding) => {
100
139
  if (shadowBinding === binding) {
101
140
  val = shadowValue;
@@ -111,4 +150,17 @@ export class ValidationMiddleware implements DataModelMiddleware {
111
150
 
112
151
  return val;
113
152
  }
153
+
154
+ public delete(
155
+ binding: BindingInstance,
156
+ options?: DataModelOptions,
157
+ next?: DataModelImpl
158
+ ) {
159
+ this.shadowModelPaths = removeBindingAndChildrenFromMap(
160
+ this.shadowModelPaths,
161
+ binding
162
+ );
163
+
164
+ return next?.delete(binding, options);
165
+ }
114
166
  }
@@ -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,11 +107,17 @@ 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 */
110
114
  export default class StringResolverPlugin implements ViewPlugin {
115
+ private propertiesToSkipCache: Map<string, Set<string>>;
116
+
117
+ constructor() {
118
+ this.propertiesToSkipCache = new Map();
119
+ }
120
+
111
121
  applyResolver(resolver: Resolver) {
112
122
  resolver.hooks.resolve.tap('string-resolver', (value, node, options) => {
113
123
  if (node.type === NodeType.Empty || node.type === NodeType.Unknown) {
@@ -120,13 +130,29 @@ export default class StringResolverPlugin implements ViewPlugin {
120
130
  node.type === NodeType.View
121
131
  ) {
122
132
  /** Use specified properties to skip during string resolution, or default */
123
- const propsToSkip = new Set<string>(
124
- node.plugins?.stringResolver?.propertiesToSkip
125
- ? node.plugins?.stringResolver?.propertiesToSkip
126
- : []
127
- );
133
+ let propsToSkip: Set<string>;
134
+ if (node.type === NodeType.Asset || node.type === NodeType.View) {
135
+ propsToSkip = new Set(
136
+ node.plugins?.stringResolver?.propertiesToSkip ?? ['exp']
137
+ );
138
+ if (node.value?.id) {
139
+ this.propertiesToSkipCache.set(node.value.id, propsToSkip);
140
+ }
141
+ } else if (
142
+ node.parent?.type === NodeType.MultiNode &&
143
+ (node.parent?.parent?.type === NodeType.Asset ||
144
+ node.parent?.parent?.type === NodeType.View) &&
145
+ node.parent.parent.value?.id &&
146
+ this.propertiesToSkipCache.has(node.parent.parent.value.id)
147
+ ) {
148
+ propsToSkip = this.propertiesToSkipCache.get(
149
+ node.parent.parent.value.id
150
+ ) as Set<string>;
151
+ } else {
152
+ propsToSkip = new Set(['exp']);
153
+ }
128
154
 
129
- const nodePath = findBasePath(node);
155
+ const nodePath = findBasePath(node, resolver);
130
156
 
131
157
  /** If the path includes something that is supposed to be skipped, this node should be skipped too. */
132
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