@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
@@ -1,4 +1,12 @@
1
- import type { ExpressionHandler } from './types';
1
+ import { isExpressionNode } from './types';
2
+ import type {
3
+ ExpressionHandler,
4
+ ExpressionNode,
5
+ ExpressionObjectType,
6
+ ExpressionType,
7
+ NodeLocation,
8
+ NodePosition,
9
+ } from './types';
2
10
 
3
11
  /** Generates a function by removing the first context argument */
4
12
  export function withoutContext<T extends unknown[], Return>(
@@ -6,3 +14,137 @@ export function withoutContext<T extends unknown[], Return>(
6
14
  ): ExpressionHandler<T, Return> {
7
15
  return (_context, ...args) => fn(...args);
8
16
  }
17
+
18
+ /** Checks if the location includes the target position */
19
+ function isInRange(position: NodePosition, location: NodeLocation) {
20
+ return (
21
+ position.character >= location.start.character &&
22
+ position.character <= location.end.character
23
+ );
24
+ }
25
+
26
+ /** Get the node in the expression that's closest to the desired position */
27
+ export function findClosestNodeAtPosition(
28
+ node: ExpressionNode,
29
+ position: NodePosition
30
+ ): ExpressionNode | undefined {
31
+ // This is just mapping recursively over nodes in the tree
32
+
33
+ // eslint-disable-next-line default-case
34
+ switch (node.type) {
35
+ case 'Modification':
36
+ case 'Assignment':
37
+ case 'LogicalExpression':
38
+ case 'BinaryExpression': {
39
+ const check =
40
+ findClosestNodeAtPosition(node.left, position) ??
41
+ findClosestNodeAtPosition(node.right, position);
42
+ if (check) {
43
+ return check;
44
+ }
45
+
46
+ break;
47
+ }
48
+
49
+ case 'UnaryExpression': {
50
+ const checkArg = findClosestNodeAtPosition(node.argument, position);
51
+ if (checkArg) {
52
+ return checkArg;
53
+ }
54
+
55
+ break;
56
+ }
57
+
58
+ case 'MemberExpression': {
59
+ const checkObject =
60
+ findClosestNodeAtPosition(node.object, position) ??
61
+ findClosestNodeAtPosition(node.property, position);
62
+ if (checkObject) {
63
+ return checkObject;
64
+ }
65
+
66
+ break;
67
+ }
68
+
69
+ case 'ConditionalExpression': {
70
+ const checkObject =
71
+ findClosestNodeAtPosition(node.test, position) ??
72
+ findClosestNodeAtPosition(node.consequent, position) ??
73
+ findClosestNodeAtPosition(node.alternate, position);
74
+ if (checkObject) {
75
+ return checkObject;
76
+ }
77
+
78
+ break;
79
+ }
80
+
81
+ case 'ArrayExpression':
82
+ case 'Compound': {
83
+ const elements =
84
+ node.type === 'ArrayExpression' ? node.elements : node.body;
85
+
86
+ const anyElements = elements.find((e) =>
87
+ findClosestNodeAtPosition(e, position)
88
+ );
89
+
90
+ if (anyElements) {
91
+ return anyElements;
92
+ }
93
+
94
+ break;
95
+ }
96
+
97
+ case 'Object': {
98
+ const checkObject = node.attributes.reduce<ExpressionNode | undefined>(
99
+ (found, next) => {
100
+ return (
101
+ found ??
102
+ findClosestNodeAtPosition(next.key, position) ??
103
+ findClosestNodeAtPosition(next.value, position)
104
+ );
105
+ },
106
+ undefined
107
+ );
108
+
109
+ if (checkObject) {
110
+ return checkObject;
111
+ }
112
+
113
+ break;
114
+ }
115
+
116
+ case 'CallExpression': {
117
+ const anyArgs =
118
+ node.args.find((arg) => {
119
+ return findClosestNodeAtPosition(arg, position);
120
+ }) ?? findClosestNodeAtPosition(node.callTarget, position);
121
+
122
+ if (anyArgs) {
123
+ return anyArgs;
124
+ }
125
+
126
+ break;
127
+ }
128
+ }
129
+
130
+ // Lastly check for yourself
131
+ if (node.location && isInRange(position, node.location)) {
132
+ return node;
133
+ }
134
+ }
135
+
136
+ /** Checks if the expression is a simple type */
137
+ export function isObjectExpression(
138
+ expr: ExpressionType
139
+ ): expr is ExpressionObjectType {
140
+ if (isExpressionNode(expr)) {
141
+ return false;
142
+ }
143
+
144
+ return (
145
+ typeof expr === 'object' &&
146
+ expr !== null &&
147
+ !Array.isArray(expr) &&
148
+ 'value' in expr
149
+ );
150
+ }
package/src/player.ts CHANGED
@@ -1,12 +1,11 @@
1
1
  import { setIn } from 'timm';
2
2
  import deferred from 'p-defer';
3
- import queueMicrotask from 'queue-microtask';
4
3
  import type { Flow as FlowType, FlowResult } from '@player-ui/types';
5
4
 
6
5
  import { SyncHook, SyncWaterfallHook } from 'tapable-ts';
7
6
  import type { Logger } from './logger';
8
7
  import { TapableLogger } from './logger';
9
- import type { ExpressionHandler } from './expressions';
8
+ import type { ExpressionType } from './expressions';
10
9
  import { ExpressionEvaluator } from './expressions';
11
10
  import { SchemaController } from './schema';
12
11
  import { BindingParser } from './binding';
@@ -21,6 +20,7 @@ import {
21
20
  FlowController,
22
21
  } from './controllers';
23
22
  import { FlowExpPlugin } from './plugins/flow-exp-plugin';
23
+ import { DefaultExpPlugin } from './plugins/default-exp-plugin';
24
24
  import type {
25
25
  PlayerFlowState,
26
26
  InProgressState,
@@ -30,8 +30,8 @@ import type {
30
30
  import { NOT_STARTED_STATE } from './types';
31
31
 
32
32
  // Variables injected at build time
33
- const PLAYER_VERSION = '0.3.1-next.1';
34
- const COMMIT = 'b51603be07572111ca68e73d1b710ed2d0aa9412';
33
+ const PLAYER_VERSION = '0.3.1';
34
+ const COMMIT = 'e8392cd5df3c84fb9c68daf149ea88c593ce0428';
35
35
 
36
36
  export interface PlayerPlugin {
37
37
  /**
@@ -49,6 +49,18 @@ export interface PlayerPlugin {
49
49
  apply: (player: Player) => void;
50
50
  }
51
51
 
52
+ // eslint-disable-next-line @typescript-eslint/no-empty-interface
53
+ export interface ExtendedPlayerPlugin<
54
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
55
+ Assets = void,
56
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
57
+ Views = void,
58
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
59
+ Expressions = void,
60
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
61
+ DataTypes = void
62
+ > {}
63
+
52
64
  export interface PlayerConfigOptions {
53
65
  /** A set of plugins to load */
54
66
  plugins?: PlayerPlugin[];
@@ -117,17 +129,16 @@ export class Player {
117
129
  };
118
130
 
119
131
  constructor(config?: PlayerConfigOptions) {
120
- const initialPlugins: PlayerPlugin[] = [];
121
- const flowExpPlugin = new FlowExpPlugin();
122
-
123
- initialPlugins.push(flowExpPlugin);
124
-
125
132
  if (config?.logger) {
126
133
  this.logger.addHandler(config.logger);
127
134
  }
128
135
 
129
136
  this.config = config || {};
130
- this.config.plugins = [...(this.config.plugins || []), ...initialPlugins];
137
+ this.config.plugins = [
138
+ new DefaultExpPlugin(),
139
+ ...(this.config.plugins || []),
140
+ new FlowExpPlugin(),
141
+ ];
131
142
  this.config.plugins?.forEach((plugin) => {
132
143
  plugin.apply(this);
133
144
  });
@@ -277,34 +288,41 @@ export class Player {
277
288
  });
278
289
 
279
290
  /** Resolve any data references in a string */
280
- function resolveStrings<T>(val: T) {
291
+ function resolveStrings<T>(val: T, formatted?: boolean) {
281
292
  return resolveDataRefs(val, {
282
293
  model: dataController,
283
294
  evaluate: expressionEvaluator.evaluate,
295
+ formatted,
284
296
  });
285
297
  }
286
298
 
287
299
  flowController.hooks.flow.tap('player', (flow: FlowInstance) => {
288
300
  flow.hooks.beforeTransition.tap('player', (state, transitionVal) => {
289
- if (
290
- state.onEnd &&
291
- (state.transitions[transitionVal] || state.transitions['*'])
292
- ) {
301
+ /** Checks to see if there are any transitions for a specific transition state (i.e. next, back). If not, it will default to * */
302
+ const computedTransitionVal = state.transitions[transitionVal]
303
+ ? transitionVal
304
+ : '*';
305
+ if (state.onEnd && state.transitions[computedTransitionVal]) {
293
306
  if (typeof state.onEnd === 'object' && 'exp' in state.onEnd) {
294
307
  expressionEvaluator?.evaluate(state.onEnd.exp);
295
308
  } else {
296
- expressionEvaluator?.evaluate(state.onEnd);
309
+ expressionEvaluator?.evaluate(state.onEnd as ExpressionType);
297
310
  }
298
311
  }
299
312
 
300
- if (!('transitions' in state) || !state.transitions[transitionVal]) {
313
+ /** If the transition does not exist, then do not resolve any expressions */
314
+ if (
315
+ !('transitions' in state) ||
316
+ !state.transitions[computedTransitionVal]
317
+ ) {
301
318
  return state;
302
319
  }
303
320
 
321
+ /** resolves and sets the transition to the computed exp */
304
322
  return setIn(
305
323
  state,
306
- ['transitions', transitionVal],
307
- resolveStrings(state.transitions[transitionVal])
324
+ ['transitions', computedTransitionVal],
325
+ resolveStrings(state.transitions[computedTransitionVal])
308
326
  ) as any;
309
327
  });
310
328
 
@@ -335,7 +353,7 @@ export class Player {
335
353
  newState = setIn(
336
354
  state,
337
355
  ['param'],
338
- resolveStrings(state.param)
356
+ resolveStrings(state.param, false)
339
357
  ) as any;
340
358
  }
341
359
 
@@ -343,17 +361,18 @@ export class Player {
343
361
  });
344
362
 
345
363
  flow.hooks.transition.tap('player', (_oldState, newState) => {
346
- if (newState.value.state_type === 'ACTION') {
347
- const { exp } = newState.value;
348
-
349
- // The nested transition call would trigger another round of the flow transition hooks to be called.
350
- // This created a weird timing where this nested transition would happen before the view had a chance to respond to the first one
351
- // Use a queueMicrotask to make sure the expression transition is outside the scope of the flow hook
352
- queueMicrotask(() => {
353
- flowController?.transition(
354
- String(expressionEvaluator?.evaluate(exp))
355
- );
356
- });
364
+ if (newState.value.state_type !== 'VIEW') {
365
+ validationController.reset();
366
+ }
367
+ });
368
+
369
+ flow.hooks.afterTransition.tap('player', (flowInstance) => {
370
+ const value = flowInstance.currentState?.value;
371
+ if (value && value.state_type === 'ACTION') {
372
+ const { exp } = value;
373
+ flowController?.transition(
374
+ String(expressionEvaluator?.evaluate(exp))
375
+ );
357
376
  }
358
377
 
359
378
  expressionEvaluator.reset();
@@ -375,6 +394,11 @@ export class Player {
375
394
  parseBinding,
376
395
  transition: flowController.transition,
377
396
  model: dataController,
397
+ utils: {
398
+ findPlugin: <Plugin = unknown>(pluginSymbol: symbol) => {
399
+ return this.findPlugin(pluginSymbol) as unknown as Plugin;
400
+ },
401
+ },
378
402
  logger: this.logger,
379
403
  flowController,
380
404
  schema,
@@ -392,6 +416,7 @@ export class Player {
392
416
  ...validationController.forView(parseBinding),
393
417
  type: (b) => schema.getType(parseBinding(b)),
394
418
  },
419
+ constants: this.constantsController,
395
420
  });
396
421
  viewController.hooks.view.tap('player', (view) => {
397
422
  validationController.onView(view);
@@ -399,26 +424,13 @@ export class Player {
399
424
  });
400
425
  this.hooks.viewController.call(viewController);
401
426
 
402
- /** Gets formatter for given formatName and formats value if found, returns value otherwise */
403
- const formatFunction: ExpressionHandler<[unknown, string], any> = (
404
- ctx,
405
- value,
406
- formatName
407
- ) => {
408
- return (
409
- schema.getFormatterForType({ type: formatName })?.format(value) ?? value
410
- );
411
- };
412
-
413
- expressionEvaluator.addExpressionFunction('format', formatFunction);
414
-
415
427
  return {
416
428
  start: () => {
417
429
  flowController
418
430
  .start()
419
431
  .then((endState) => {
420
432
  const flowResult: FlowResult = {
421
- endState: resolveStrings(endState),
433
+ endState: resolveStrings(endState, false),
422
434
  data: dataController.serialize(),
423
435
  };
424
436
 
@@ -489,7 +501,9 @@ export class Player {
489
501
  ref,
490
502
  status: 'completed',
491
503
  flow: state.flow,
492
- dataModel: state.controllers.data.getModel(),
504
+ controllers: {
505
+ data: state.controllers.data.makeReadOnly(),
506
+ },
493
507
  } as const;
494
508
 
495
509
  return maybeUpdateState({
@@ -0,0 +1,57 @@
1
+ import type { ExpressionHandler, ExpressionType } from '../expressions';
2
+ import type { SchemaController } from '../schema';
3
+ import type { Player, PlayerPlugin } from '../player';
4
+
5
+ /** Gets formatter for given formatName and formats value if found, returns value otherwise */
6
+ const createFormatFunction = (schema: SchemaController) => {
7
+ /**
8
+ * The generated handler for the given schema
9
+ */
10
+ const handler: ExpressionHandler<[unknown, string], any> = (
11
+ ctx,
12
+ value,
13
+ formatName
14
+ ) => {
15
+ return (
16
+ schema.getFormatterForType({ type: formatName })?.format(value) ?? value
17
+ );
18
+ };
19
+
20
+ return handler;
21
+ };
22
+
23
+ /**
24
+ * A plugin that provides the out-of-the-box expressions for player
25
+ */
26
+ export class DefaultExpPlugin implements PlayerPlugin {
27
+ name = 'flow-exp-plugin';
28
+
29
+ apply(player: Player) {
30
+ let formatFunction: ExpressionHandler<[unknown, string]> | undefined;
31
+
32
+ player.hooks.schema.tap(this.name, (schemaController) => {
33
+ formatFunction = createFormatFunction(schemaController);
34
+ });
35
+
36
+ player.hooks.expressionEvaluator.tap(this.name, (expEvaluator) => {
37
+ if (formatFunction) {
38
+ expEvaluator.addExpressionFunction('format', formatFunction);
39
+ }
40
+
41
+ expEvaluator.addExpressionFunction('log', (ctx, ...args) => {
42
+ player.logger.info(...args);
43
+ });
44
+
45
+ expEvaluator.addExpressionFunction('debug', (ctx, ...args) => {
46
+ player.logger.debug(...args);
47
+ });
48
+
49
+ expEvaluator.addExpressionFunction(
50
+ 'eval',
51
+ (ctx, ...args: [ExpressionType]) => {
52
+ return ctx.evaluate(...args);
53
+ }
54
+ );
55
+ });
56
+ }
57
+ }
@@ -3,7 +3,7 @@ import type {
3
3
  ExpressionObject,
4
4
  NavigationFlowState,
5
5
  } from '@player-ui/types';
6
- import type { ExpressionEvaluator } from '../expressions';
6
+ import type { ExpressionEvaluator, ExpressionType } from '../expressions';
7
7
  import type { FlowInstance } from '../controllers';
8
8
  import type { Player, PlayerPlugin } from '../player';
9
9
 
@@ -28,7 +28,7 @@ export class FlowExpPlugin implements PlayerPlugin {
28
28
  if (typeof exp === 'object' && 'exp' in exp) {
29
29
  expressionEvaluator?.evaluate(exp.exp);
30
30
  } else {
31
- expressionEvaluator?.evaluate(exp);
31
+ expressionEvaluator?.evaluate(exp as ExpressionType);
32
32
  }
33
33
  }
34
34
  };
@@ -11,8 +11,8 @@ const identify = (val: any) => val;
11
11
  /** Expand the authored schema into a set of paths -> DataTypes */
12
12
  export function parse(
13
13
  schema: SchemaType.Schema
14
- ): Map<string, SchemaType.DataType> {
15
- const expandedPaths = new Map<string, SchemaType.DataType>();
14
+ ): Map<string, SchemaType.DataTypes> {
15
+ const expandedPaths = new Map<string, SchemaType.DataTypes>();
16
16
 
17
17
  if (!schema.ROOT) {
18
18
  return expandedPaths;
@@ -62,6 +62,10 @@ export function parse(
62
62
  nestedPath.push('[]');
63
63
  }
64
64
 
65
+ if (type.isRecord) {
66
+ nestedPath.push('{}');
67
+ }
68
+
65
69
  if (type.type && schema[type.type]) {
66
70
  parseQueue.push({
67
71
  path: nestedPath,
@@ -85,14 +89,14 @@ export class SchemaController implements ValidationProvider {
85
89
  new Map();
86
90
 
87
91
  private types: Map<string, SchemaType.DataType<any>> = new Map();
88
- public readonly schema: Map<string, SchemaType.DataType> = new Map();
92
+ public readonly schema: Map<string, SchemaType.DataTypes> = new Map();
89
93
 
90
94
  private bindingSchemaNormalizedCache: Map<BindingInstance, string> =
91
95
  new Map();
92
96
 
93
97
  public readonly hooks = {
94
98
  resolveTypeForBinding: new SyncWaterfallHook<
95
- [SchemaType.DataType | undefined, BindingInstance]
99
+ [SchemaType.DataTypes | undefined, BindingInstance]
96
100
  >(),
97
101
  };
98
102
 
@@ -135,17 +139,32 @@ export class SchemaController implements ValidationProvider {
135
139
  return cached;
136
140
  }
137
141
 
138
- const normalized = binding
139
- .asArray()
142
+ let bindingArray = binding.asArray();
143
+ let normalized = bindingArray
140
144
  .map((p) => (typeof p === 'number' ? '[]' : p))
141
145
  .join('.');
142
146
 
143
- this.bindingSchemaNormalizedCache.set(binding, normalized);
147
+ if (normalized) {
148
+ this.bindingSchemaNormalizedCache.set(binding, normalized);
149
+ bindingArray = normalized.split('.');
150
+ }
151
+
152
+ bindingArray.forEach((item) => {
153
+ const recordBinding = bindingArray
154
+ .map((p) => (p === item ? '{}' : p))
155
+ .join('.');
156
+
157
+ if (this.schema.get(recordBinding)) {
158
+ this.bindingSchemaNormalizedCache.set(binding, recordBinding);
159
+ bindingArray = recordBinding.split('.');
160
+ normalized = recordBinding;
161
+ }
162
+ });
144
163
 
145
164
  return normalized;
146
165
  }
147
166
 
148
- public getType(binding: BindingInstance): SchemaType.DataType | undefined {
167
+ public getType(binding: BindingInstance): SchemaType.DataTypes | undefined {
149
168
  return this.hooks.resolveTypeForBinding.call(
150
169
  this.schema.get(this.normalizeBinding(binding)),
151
170
  binding
@@ -154,7 +173,7 @@ export class SchemaController implements ValidationProvider {
154
173
 
155
174
  public getApparentType(
156
175
  binding: BindingInstance
157
- ): SchemaType.DataType | undefined {
176
+ ): SchemaType.DataTypes | undefined {
158
177
  const schemaType = this.getType(binding);
159
178
 
160
179
  if (schemaType === undefined) {
@@ -6,11 +6,22 @@ const DOUBLE_OPEN_CURLY = '{{';
6
6
  const DOUBLE_CLOSE_CURLY = '}}';
7
7
 
8
8
  export interface Options {
9
- /** The model to use when resolving refs */
10
- model: DataModelWithParser;
11
-
12
- /** A function to evaluate an expression */
13
- evaluate: (exp: Expression) => any;
9
+ /**
10
+ * The model to use when resolving refs
11
+ * Passing `false` will skip trying to resolve any direct model refs ({{foo}})
12
+ */
13
+ model: false | DataModelWithParser;
14
+
15
+ /**
16
+ * A function to evaluate an expression
17
+ * Passing `false` will skip trying to evaluate any expressions (@[ foo() ]@)
18
+ */
19
+ evaluate: false | ((exp: Expression) => any);
20
+
21
+ /**
22
+ * Optionaly resolve binding without formatting in case Type format applies
23
+ */
24
+ formatted?: boolean;
14
25
  }
15
26
 
16
27
  /** Search the given string for the coordinates of the next expression to resolve */
@@ -70,6 +81,10 @@ export function resolveExpressionsInString(
70
81
  val: string,
71
82
  { evaluate }: Options
72
83
  ): string {
84
+ if (!evaluate) {
85
+ return val;
86
+ }
87
+
73
88
  const expMatch = /@\[.*?\]@/;
74
89
  let newVal = val;
75
90
  let match = newVal.match(expMatch);
@@ -106,10 +121,11 @@ export function resolveExpressionsInString(
106
121
 
107
122
  /** Return a string with all data model references resolved */
108
123
  export function resolveDataRefsInString(val: string, options: Options): string {
109
- const { model } = options;
124
+ const { model, formatted = true } = options;
110
125
  let workingString = resolveExpressionsInString(val, options);
111
126
 
112
127
  if (
128
+ !model ||
113
129
  typeof workingString !== 'string' ||
114
130
  workingString.indexOf(DOUBLE_OPEN_CURLY) === -1
115
131
  ) {
@@ -133,7 +149,7 @@ export function resolveDataRefsInString(val: string, options: Options): string {
133
149
  )
134
150
  .trim();
135
151
 
136
- const evaledVal = model.get(binding, { formatted: true });
152
+ const evaledVal = model.get(binding, { formatted });
137
153
 
138
154
  // Exit early if the string is _just_ a model lookup
139
155
  // If the result is a string, we may need further processing for nested bindings
@@ -160,18 +176,19 @@ function traverseObject<T>(val: T, options: Options): T {
160
176
  }
161
177
 
162
178
  case 'object': {
179
+ if (!val) return val;
163
180
  // TODO: Do we care refs in keys?
164
181
  const keys = Object.keys(val);
165
182
  let newVal = val;
166
183
 
167
184
  if (keys.length > 0) {
168
- for (const key of keys) {
185
+ keys.forEach((key) => {
169
186
  newVal = setIn(
170
187
  newVal as any,
171
188
  [key],
172
189
  traverseObject((val as any)[key], options)
173
190
  ) as any;
174
- }
191
+ });
175
192
  }
176
193
 
177
194
  return newVal;
package/src/types.ts CHANGED
@@ -1,5 +1,4 @@
1
1
  import type { Flow, FlowResult } from '@player-ui/types';
2
- import type { DataModelWithParser } from './data';
3
2
  import type { BindingParser, BindingLike } from './binding';
4
3
  import type { SchemaController } from './schema';
5
4
  import type { ExpressionEvaluator } from './expressions';
@@ -10,6 +9,7 @@ import type {
10
9
  ValidationController,
11
10
  FlowController,
12
11
  } from './controllers';
12
+ import type { ReadOnlyDataController } from './controllers/data/utils';
13
13
 
14
14
  /** The status for a flow's execution state */
15
15
  export type PlayerFlowStatus =
@@ -86,8 +86,11 @@ export type InProgressState = BaseFlowState<'in-progress'> &
86
86
  export type CompletedState = BaseFlowState<'completed'> &
87
87
  PlayerFlowExecutionData &
88
88
  FlowResult & {
89
- /** The top-level data-model for the flow */
90
- dataModel: DataModelWithParser;
89
+ /** Readonly Player controllers to provide Player functionality after the flow has ended */
90
+ controllers: {
91
+ /** A read only instance of the Data Controller */
92
+ data: ReadOnlyDataController;
93
+ };
91
94
  };
92
95
 
93
96
  /** The flow finished but not successfully */