@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.
- package/dist/index.cjs.js +622 -325
- package/dist/index.d.ts +188 -72
- package/dist/index.esm.js +619 -327
- package/dist/player.dev.js +623 -326
- package/dist/player.prod.js +1 -1
- package/package.json +11 -3
- package/src/binding/binding.ts +8 -0
- package/src/binding/index.ts +1 -1
- package/src/controllers/constants/index.ts +9 -5
- package/src/controllers/data.ts +49 -52
- package/src/controllers/flow/controller.ts +16 -12
- package/src/controllers/flow/flow.ts +6 -1
- package/src/controllers/validation/binding-tracker.ts +42 -19
- package/src/controllers/validation/controller.ts +265 -85
- package/src/controllers/view/asset-transform.ts +4 -1
- package/src/controllers/view/controller.ts +19 -2
- package/src/data/dependency-tracker.ts +14 -0
- package/src/data/local-model.ts +25 -1
- package/src/data/model.ts +55 -8
- package/src/data/noop-model.ts +2 -0
- package/src/expressions/evaluator-functions.ts +24 -2
- package/src/expressions/evaluator.ts +35 -31
- package/src/expressions/types.ts +17 -5
- package/src/expressions/utils.ts +19 -0
- package/src/player.ts +26 -29
- package/src/plugins/flow-exp-plugin.ts +2 -2
- package/src/string-resolver/index.ts +7 -2
- package/src/types.ts +1 -4
- package/src/validator/binding-map-splice.ts +59 -0
- package/src/validator/index.ts +1 -0
- package/src/validator/types.ts +11 -3
- package/src/validator/validation-middleware.ts +34 -3
- package/src/view/parser/index.ts +44 -2
- package/src/view/plugins/applicability.ts +1 -1
- package/src/view/plugins/string-resolver.ts +8 -4
- package/src/view/plugins/template-plugin.ts +1 -6
- package/src/view/resolver/index.ts +119 -54
- 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.
|
|
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.
|
|
11
|
-
"@player-ui/types": "0.4.0-next.
|
|
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"
|
package/src/binding/binding.ts
CHANGED
|
@@ -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;
|
package/src/binding/index.ts
CHANGED
|
@@ -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
|
|
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
|
-
|
|
82
|
-
|
|
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
|
}
|
package/src/controllers/data.ts
CHANGED
|
@@ -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
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
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
|
|
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 (
|
|
197
|
-
|
|
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 =
|
|
201
|
-
|
|
202
|
-
|
|
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
|
-
|
|
214
|
-
const
|
|
215
|
-
const
|
|
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 =
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
227
|
+
const existedBeforeDelete =
|
|
228
|
+
typeof parentValue === 'object' &&
|
|
229
|
+
parentValue !== null &&
|
|
230
|
+
Object.prototype.hasOwnProperty.call(parentValue, property);
|
|
222
231
|
|
|
223
|
-
|
|
224
|
-
const parent = parentBinding ? this.get(parentBinding) : undefined;
|
|
232
|
+
this.getModel().delete(resolved, options);
|
|
225
233
|
|
|
226
|
-
|
|
227
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
133
|
-
|
|
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
|
|
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
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
) {
|
|
153
|
-
|
|
164
|
+
return firstFieldEOW;
|
|
165
|
+
},
|
|
166
|
+
getValidationsForBinding(binding, getOptions) {
|
|
167
|
+
if (getOptions?.track) {
|
|
168
|
+
track(binding);
|
|
154
169
|
}
|
|
155
170
|
|
|
156
|
-
return
|
|
171
|
+
return (
|
|
172
|
+
options.validation
|
|
173
|
+
?._getValidationForBinding(binding)
|
|
174
|
+
?.getAll(getOptions) ?? []
|
|
175
|
+
);
|
|
157
176
|
},
|
|
158
|
-
getChildren: (type
|
|
177
|
+
getChildren: (type?: Validation.DisplayTarget) => {
|
|
159
178
|
const validations = new Array<ValidationResponse>();
|
|
160
179
|
lastComputedBindingTree.get(node)?.forEach((binding) => {
|
|
161
|
-
const eow = options.validation
|
|
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
|
|
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)
|
|
239
|
+
this.trackedBindings = new Set(currentBindingTree.get(node));
|
|
217
240
|
lastComputedBindingTree = currentBindingTree;
|
|
218
241
|
|
|
219
242
|
lastSectionBindingTree.clear();
|