@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
@@ -26,7 +26,7 @@ const DOUBLE_QUOTE = '"';
26
26
  const BACK_TICK = '`';
27
27
  // const IDENTIFIER_REGEX = /[\w\-@]+/;
28
28
 
29
- /** A _faster_ way to match chars instead of a regex (/[\w\-@]+/) */
29
+ /** A _faster_ way to match chars instead of a regex. */
30
30
  const isIdentifierChar = (char?: string): boolean => {
31
31
  if (!char) {
32
32
  return false;
@@ -34,14 +34,22 @@ const isIdentifierChar = (char?: string): boolean => {
34
34
 
35
35
  const charCode = char.charCodeAt(0);
36
36
 
37
- return (
38
- (charCode >= 48 && charCode <= 57) || // 0 - 9
39
- (charCode >= 65 && charCode <= 90) || // A-Z
40
- (charCode >= 97 && charCode <= 122) || // a-z
41
- charCode === 95 || // _
42
- charCode === 45 || // -
43
- charCode === 64 // @
44
- );
37
+ const matches =
38
+ charCode === 32 || // ' '
39
+ charCode === 34 || // "
40
+ charCode === 39 || // '
41
+ charCode === 40 || // (
42
+ charCode === 41 || // )
43
+ charCode === 42 || // *
44
+ charCode === 46 || // .
45
+ charCode === 61 || // =
46
+ charCode === 91 || // [
47
+ charCode === 93 || // ]
48
+ charCode === 96 || // `
49
+ charCode === 123 || // {
50
+ charCode === 125; // }
51
+
52
+ return !matches;
45
53
  };
46
54
 
47
55
  /** Parse out a binding AST from a path */
@@ -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
  }
@@ -1,9 +1,8 @@
1
1
  import { SyncHook, SyncWaterfallHook, SyncBailHook } from 'tapable-ts';
2
- import { omit, removeAt } from 'timm';
3
2
  import { dequal } from 'dequal';
4
- import type { Logger } from '../logger';
5
- import type { BindingParser, BindingLike } from '../binding';
6
- import { BindingInstance } from '../binding';
3
+ import type { Logger } from '../../logger';
4
+ import type { BindingParser, BindingLike } from '../../binding';
5
+ import { BindingInstance } from '../../binding';
7
6
  import type {
8
7
  BatchSetTransaction,
9
8
  Updates,
@@ -11,9 +10,10 @@ import type {
11
10
  DataModelWithParser,
12
11
  DataPipeline,
13
12
  DataModelMiddleware,
14
- } from '../data';
15
- import { PipelinedDataModel, LocalModel } from '../data';
16
- import type { RawSetTransaction } from '../types';
13
+ } from '../../data';
14
+ import { PipelinedDataModel, LocalModel } from '../../data';
15
+ import type { RawSetTransaction } from '../../types';
16
+ import { ReadOnlyDataController } from './utils';
17
17
 
18
18
  /** The orchestrator for player data */
19
19
  export class DataController implements DataModelWithParser<DataModelOptions> {
@@ -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]>(),
30
- onUpdate: new SyncHook<[Updates]>(),
32
+
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
  []
@@ -158,21 +168,23 @@ export class DataController implements DataModelWithParser<DataModelOptions> {
158
168
  this.hooks.onSet.call(normalizedTransaction);
159
169
 
160
170
  if (setUpdates.length > 0) {
161
- this.hooks.onUpdate.call(setUpdates);
171
+ this.hooks.onUpdate.call(setUpdates, options);
162
172
  }
163
173
 
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,56 +206,43 @@ 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
- }
218
+ const resolved =
219
+ binding instanceof BindingInstance
220
+ ? binding
221
+ : this.resolve(binding, false);
208
222
 
209
- private addToTrash(binding: BindingInstance) {
210
- this.trash.add(binding);
211
- }
223
+ const parentBinding = resolved.parent();
224
+ const property = resolved.key();
225
+ const parentValue = this.get(parentBinding);
212
226
 
213
- private deleteData(binding: BindingInstance) {
214
- const parentBinding = binding.parent();
215
- const parentPath = parentBinding.asString();
216
- const property = binding.key();
227
+ const existedBeforeDelete =
228
+ typeof parentValue === 'object' &&
229
+ parentValue !== null &&
230
+ Object.prototype.hasOwnProperty.call(parentValue, property);
217
231
 
218
- const existedBeforeDelete = Object.prototype.hasOwnProperty.call(
219
- this.get(parentBinding),
220
- property
221
- );
232
+ this.getModel().delete(resolved, options);
222
233
 
223
- if (property !== undefined) {
224
- const parent = parentBinding ? this.get(parentBinding) : undefined;
225
-
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 {
245
242
  return this.hooks.serialize.call(this.get(''));
246
243
  }
244
+
245
+ public makeReadOnly(): ReadOnlyDataController {
246
+ return new ReadOnlyDataController(this, this.logger);
247
+ }
247
248
  }
@@ -0,0 +1 @@
1
+ export * from './controller';
@@ -0,0 +1,42 @@
1
+ /* eslint-disable @typescript-eslint/no-unused-vars */
2
+ import type { DataController } from '.';
3
+ import type { Logger } from '../../logger';
4
+ import type { BindingLike } from '../../binding';
5
+ import type {
6
+ DataModelWithParser,
7
+ DataModelOptions,
8
+ Updates,
9
+ } from '../../data';
10
+
11
+ /** Wrapper for the Data Controller Class that prevents writes */
12
+ export class ReadOnlyDataController
13
+ implements DataModelWithParser<DataModelOptions>
14
+ {
15
+ private controller: DataController;
16
+ private logger?: Logger;
17
+
18
+ constructor(controller: DataController, logger?: Logger) {
19
+ this.controller = controller;
20
+ this.logger = logger;
21
+ }
22
+
23
+ get(binding: BindingLike, options?: DataModelOptions | undefined) {
24
+ return this.controller.get(binding, options);
25
+ }
26
+
27
+ set(
28
+ transaction: [BindingLike, any][],
29
+ options?: DataModelOptions | undefined
30
+ ): Updates {
31
+ this.logger?.error(
32
+ 'Error: Tried to set in a read only instance of the DataController'
33
+ );
34
+ return [];
35
+ }
36
+
37
+ delete(binding: BindingLike, options?: DataModelOptions | undefined): void {
38
+ this.logger?.error(
39
+ 'Error: Tried to delete in a read only instance of the DataController'
40
+ );
41
+ }
42
+ }
@@ -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
  }
@@ -1,5 +1,5 @@
1
1
  export * from './flow';
2
2
  export * from './validation';
3
3
  export * from './view';
4
- export * from './data';
4
+ export * from './data/controller';
5
5
  export * from './constants';
@@ -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();