@player-ui/player 0.4.0 → 0.4.1-next.0
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 +795 -390
- package/dist/index.d.ts +238 -81
- package/dist/index.esm.js +787 -388
- package/dist/player.dev.js +4768 -5282
- package/dist/player.prod.js +1 -1
- package/package.json +12 -3
- package/src/binding/binding.ts +8 -0
- package/src/binding/index.ts +14 -4
- package/src/binding/resolver.ts +1 -1
- package/src/binding-grammar/custom/index.ts +17 -9
- package/src/controllers/constants/index.ts +9 -5
- package/src/controllers/{data.ts → data/controller.ts} +60 -61
- package/src/controllers/data/index.ts +1 -0
- package/src/controllers/data/utils.ts +42 -0
- package/src/controllers/flow/controller.ts +16 -12
- package/src/controllers/flow/flow.ts +6 -1
- package/src/controllers/index.ts +1 -1
- package/src/controllers/validation/binding-tracker.ts +42 -19
- package/src/controllers/validation/controller.ts +359 -145
- package/src/controllers/view/asset-transform.ts +4 -1
- package/src/controllers/view/controller.ts +20 -3
- 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 +37 -33
- package/src/expressions/index.ts +1 -0
- package/src/expressions/parser.ts +53 -27
- package/src/expressions/types.ts +23 -5
- package/src/expressions/utils.ts +19 -0
- package/src/player.ts +47 -48
- package/src/plugins/default-exp-plugin.ts +57 -0
- package/src/plugins/flow-exp-plugin.ts +2 -2
- package/src/schema/schema.ts +28 -9
- package/src/string-resolver/index.ts +25 -9
- package/src/types.ts +6 -3
- 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 +38 -4
- package/src/view/parser/index.ts +51 -3
- 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",
|
|
3
|
+
"version": "0.4.1-next.0",
|
|
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",
|
|
11
|
-
"@player-ui/types": "0.4.0",
|
|
10
|
+
"@player-ui/partial-match-registry": "0.4.1-next.0",
|
|
11
|
+
"@player-ui/types": "0.4.1-next.0",
|
|
12
12
|
"dequal": "^2.0.2",
|
|
13
13
|
"p-defer": "^3.0.0",
|
|
14
14
|
"queue-microtask": "^1.2.3",
|
|
@@ -21,6 +21,7 @@
|
|
|
21
21
|
"ebnf": "^1.9.0",
|
|
22
22
|
"timm": "^1.6.2",
|
|
23
23
|
"error-polyfill": "^0.1.3",
|
|
24
|
+
"ts-nested-error": "^1.2.1",
|
|
24
25
|
"@babel/runtime": "7.15.4"
|
|
25
26
|
},
|
|
26
27
|
"main": "dist/index.cjs.js",
|
|
@@ -64,6 +65,14 @@
|
|
|
64
65
|
{
|
|
65
66
|
"name": "Kelly Harrop",
|
|
66
67
|
"url": "https://github.com/kharrop"
|
|
68
|
+
},
|
|
69
|
+
{
|
|
70
|
+
"name": "Alejandro Fimbres",
|
|
71
|
+
"url": "https://github.com/lexfm"
|
|
72
|
+
},
|
|
73
|
+
{
|
|
74
|
+
"name": "Rafael Campos",
|
|
75
|
+
"url": "https://github.com/rafbcampos"
|
|
67
76
|
}
|
|
68
77
|
],
|
|
69
78
|
"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
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { SyncBailHook, SyncWaterfallHook } from 'tapable-ts';
|
|
2
|
-
import NestedError from 'nested-error
|
|
2
|
+
import { NestedError } from 'ts-nested-error';
|
|
3
3
|
import type { ParserResult, AnyNode } from '../binding-grammar';
|
|
4
4
|
import {
|
|
5
5
|
// We can swap this with whichever parser we want to use
|
|
@@ -15,6 +15,8 @@ export * from './utils';
|
|
|
15
15
|
export * from './binding';
|
|
16
16
|
|
|
17
17
|
export const SIMPLE_BINDING_REGEX = /^[\w\-@]+(\.[\w\-@]+)*$/;
|
|
18
|
+
export const BINDING_BRACKETS_REGEX = /[\s()*=`{}'"[\]]/;
|
|
19
|
+
const LAZY_BINDING_REGEX = /^[^.]+(\..+)*$/;
|
|
18
20
|
|
|
19
21
|
const DEFAULT_OPTIONS: BindingParserOptions = {
|
|
20
22
|
get: () => {
|
|
@@ -28,6 +30,9 @@ const DEFAULT_OPTIONS: BindingParserOptions = {
|
|
|
28
30
|
},
|
|
29
31
|
};
|
|
30
32
|
|
|
33
|
+
type BeforeResolveNodeContext = Required<NormalizedResult> &
|
|
34
|
+
ResolveBindingASTOptions;
|
|
35
|
+
|
|
31
36
|
/** A parser for creating bindings from a string */
|
|
32
37
|
export class BindingParser {
|
|
33
38
|
private cache: Record<string, BindingInstance>;
|
|
@@ -37,7 +42,7 @@ export class BindingParser {
|
|
|
37
42
|
public hooks = {
|
|
38
43
|
skipOptimization: new SyncBailHook<[string], boolean>(),
|
|
39
44
|
beforeResolveNode: new SyncWaterfallHook<
|
|
40
|
-
[AnyNode,
|
|
45
|
+
[AnyNode, BeforeResolveNodeContext]
|
|
41
46
|
>(),
|
|
42
47
|
};
|
|
43
48
|
|
|
@@ -56,8 +61,13 @@ export class BindingParser {
|
|
|
56
61
|
path: string,
|
|
57
62
|
resolveOptions: ResolveBindingASTOptions
|
|
58
63
|
) {
|
|
64
|
+
/**
|
|
65
|
+
* Ensure no binding characters exist in path and the characters remaining
|
|
66
|
+
* look like a binding format.
|
|
67
|
+
*/
|
|
59
68
|
if (
|
|
60
|
-
|
|
69
|
+
!BINDING_BRACKETS_REGEX.test(path) &&
|
|
70
|
+
LAZY_BINDING_REGEX.test(path) &&
|
|
61
71
|
this.hooks.skipOptimization.call(path) !== true
|
|
62
72
|
) {
|
|
63
73
|
return { path: path.split('.'), updates: undefined } as NormalizedResult;
|
|
@@ -172,7 +182,7 @@ export class BindingParser {
|
|
|
172
182
|
|
|
173
183
|
const updateKeys = Object.keys(updates);
|
|
174
184
|
|
|
175
|
-
if (updateKeys.length > 0) {
|
|
185
|
+
if (!options.readOnly && updateKeys.length > 0) {
|
|
176
186
|
const updateTransaction = updateKeys.map<[BindingInstance, any]>(
|
|
177
187
|
(updatedBinding) => [
|
|
178
188
|
this.parse(updatedBinding),
|
package/src/binding/resolver.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import NestedError from 'nested-error
|
|
1
|
+
import { NestedError } from 'ts-nested-error';
|
|
2
2
|
import type { SyncWaterfallHook } from 'tapable-ts';
|
|
3
3
|
import type { PathNode, AnyNode } from '../binding-grammar';
|
|
4
4
|
import { findInArray, maybeConvertToNum } from './utils';
|
|
@@ -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
|
|
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
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
charCode ===
|
|
42
|
-
charCode ===
|
|
43
|
-
charCode ===
|
|
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
|
|
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
|
}
|
|
@@ -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 '
|
|
5
|
-
import type { BindingParser, BindingLike } from '
|
|
6
|
-
import { BindingInstance } from '
|
|
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 '
|
|
15
|
-
import { PipelinedDataModel, LocalModel } from '
|
|
16
|
-
import type { RawSetTransaction } from '
|
|
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]>(),
|
|
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
|
[]
|
|
@@ -157,8 +167,6 @@ export class DataController implements DataModelWithParser<DataModelOptions> {
|
|
|
157
167
|
|
|
158
168
|
this.hooks.onSet.call(normalizedTransaction);
|
|
159
169
|
|
|
160
|
-
this.hooks.onSet.call(normalizedTransaction);
|
|
161
|
-
|
|
162
170
|
if (setUpdates.length > 0) {
|
|
163
171
|
this.hooks.onUpdate.call(setUpdates, options);
|
|
164
172
|
}
|
|
@@ -166,15 +174,17 @@ export class DataController implements DataModelWithParser<DataModelOptions> {
|
|
|
166
174
|
return result;
|
|
167
175
|
}
|
|
168
176
|
|
|
169
|
-
private resolve(binding: BindingLike): BindingInstance {
|
|
177
|
+
private resolve(binding: BindingLike, readOnly: boolean): BindingInstance {
|
|
170
178
|
return Array.isArray(binding) || typeof binding === 'string'
|
|
171
|
-
? this.pathResolver.parse(binding)
|
|
179
|
+
? this.pathResolver.parse(binding, { readOnly })
|
|
172
180
|
: binding;
|
|
173
181
|
}
|
|
174
182
|
|
|
175
183
|
public get(binding: BindingLike, options?: DataModelOptions) {
|
|
176
184
|
const resolved =
|
|
177
|
-
binding instanceof BindingInstance
|
|
185
|
+
binding instanceof BindingInstance
|
|
186
|
+
? binding
|
|
187
|
+
: this.resolve(binding, true);
|
|
178
188
|
let result = this.getModel().get(resolved, options);
|
|
179
189
|
|
|
180
190
|
if (result === undefined && !options?.ignoreDefaultValue) {
|
|
@@ -187,6 +197,8 @@ export class DataController implements DataModelWithParser<DataModelOptions> {
|
|
|
187
197
|
|
|
188
198
|
if (options?.formatted) {
|
|
189
199
|
result = this.hooks.format.call(result, resolved);
|
|
200
|
+
} else if (options?.formatted === false) {
|
|
201
|
+
result = this.hooks.deformat.call(result, resolved);
|
|
190
202
|
}
|
|
191
203
|
|
|
192
204
|
this.hooks.onGet.call(binding, result);
|
|
@@ -194,56 +206,43 @@ export class DataController implements DataModelWithParser<DataModelOptions> {
|
|
|
194
206
|
return result;
|
|
195
207
|
}
|
|
196
208
|
|
|
197
|
-
public delete(binding: BindingLike) {
|
|
198
|
-
if (
|
|
199
|
-
|
|
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)');
|
|
200
216
|
}
|
|
201
217
|
|
|
202
|
-
const resolved =
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
public getTrash(): Set<BindingInstance> {
|
|
208
|
-
return this.trash;
|
|
209
|
-
}
|
|
210
|
-
|
|
211
|
-
private addToTrash(binding: BindingInstance) {
|
|
212
|
-
this.trash.add(binding);
|
|
213
|
-
}
|
|
218
|
+
const resolved =
|
|
219
|
+
binding instanceof BindingInstance
|
|
220
|
+
? binding
|
|
221
|
+
: this.resolve(binding, false);
|
|
214
222
|
|
|
215
|
-
|
|
216
|
-
const
|
|
217
|
-
const
|
|
218
|
-
const property = binding.key();
|
|
223
|
+
const parentBinding = resolved.parent();
|
|
224
|
+
const property = resolved.key();
|
|
225
|
+
const parentValue = this.get(parentBinding);
|
|
219
226
|
|
|
220
|
-
const existedBeforeDelete =
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
227
|
+
const existedBeforeDelete =
|
|
228
|
+
typeof parentValue === 'object' &&
|
|
229
|
+
parentValue !== null &&
|
|
230
|
+
Object.prototype.hasOwnProperty.call(parentValue, property);
|
|
224
231
|
|
|
225
|
-
|
|
226
|
-
const parent = parentBinding ? this.get(parentBinding) : undefined;
|
|
232
|
+
this.getModel().delete(resolved, options);
|
|
227
233
|
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
if (parentPath && Array.isArray(parent)) {
|
|
231
|
-
if (parent.length > property) {
|
|
232
|
-
this.set([[parentBinding, removeAt(parent, property as number)]]);
|
|
233
|
-
}
|
|
234
|
-
} else if (parentPath && parent[property]) {
|
|
235
|
-
this.set([[parentBinding, omit(parent, property as string)]]);
|
|
236
|
-
} else if (!parentPath) {
|
|
237
|
-
this.getModel().reset(omit(this.get(''), property as string));
|
|
238
|
-
}
|
|
234
|
+
if (existedBeforeDelete && !this.get(resolved)) {
|
|
235
|
+
this.trash.add(resolved);
|
|
239
236
|
}
|
|
240
237
|
|
|
241
|
-
|
|
242
|
-
this.addToTrash(binding);
|
|
243
|
-
}
|
|
238
|
+
this.hooks.onDelete.call(resolved);
|
|
244
239
|
}
|
|
245
240
|
|
|
246
241
|
public serialize(): object {
|
|
247
242
|
return this.hooks.serialize.call(this.get(''));
|
|
248
243
|
}
|
|
244
|
+
|
|
245
|
+
public makeReadOnly(): ReadOnlyDataController {
|
|
246
|
+
return new ReadOnlyDataController(this, this.logger);
|
|
247
|
+
}
|
|
249
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
|
|
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
|
}
|
package/src/controllers/index.ts
CHANGED
|
@@ -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();
|