@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
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { Validation } from '@player-ui/types';
|
|
2
2
|
import { SyncHook, SyncWaterfallHook } from 'tapable-ts';
|
|
3
|
+
import { setIn } from 'timm';
|
|
3
4
|
|
|
4
5
|
import type { BindingInstance, BindingFactory } from '../../binding';
|
|
5
6
|
import { isBinding } from '../../binding';
|
|
@@ -8,13 +9,18 @@ import type { SchemaController } from '../../schema';
|
|
|
8
9
|
import type {
|
|
9
10
|
ErrorValidationResponse,
|
|
10
11
|
ValidationObject,
|
|
12
|
+
ValidationObjectWithHandler,
|
|
11
13
|
ValidatorContext,
|
|
12
14
|
ValidationProvider,
|
|
13
15
|
ValidationResponse,
|
|
14
16
|
WarningValidationResponse,
|
|
15
17
|
StrongOrWeakBinding,
|
|
16
18
|
} from '../../validator';
|
|
17
|
-
import {
|
|
19
|
+
import {
|
|
20
|
+
ValidationMiddleware,
|
|
21
|
+
ValidatorRegistry,
|
|
22
|
+
removeBindingAndChildrenFromMap,
|
|
23
|
+
} from '../../validator';
|
|
18
24
|
import type { Logger } from '../../logger';
|
|
19
25
|
import { ProxyLogger } from '../../logger';
|
|
20
26
|
import type { Resolve, ViewInstance } from '../../view';
|
|
@@ -28,7 +34,22 @@ import type {
|
|
|
28
34
|
import type { BindingTracker } from './binding-tracker';
|
|
29
35
|
import { ValidationBindingTrackerViewPlugin } from './binding-tracker';
|
|
30
36
|
|
|
31
|
-
|
|
37
|
+
export const SCHEMA_VALIDATION_PROVIDER_NAME = 'schema';
|
|
38
|
+
export const VIEW_VALIDATION_PROVIDER_NAME = 'view';
|
|
39
|
+
|
|
40
|
+
export const VALIDATION_PROVIDER_NAME_SYMBOL: unique symbol = Symbol.for(
|
|
41
|
+
'validation-provider-name'
|
|
42
|
+
);
|
|
43
|
+
|
|
44
|
+
export type ValidationObjectWithSource = ValidationObjectWithHandler & {
|
|
45
|
+
/** The name of the validation */
|
|
46
|
+
[VALIDATION_PROVIDER_NAME_SYMBOL]: string;
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
type SimpleValidatorContext = Omit<
|
|
50
|
+
ValidatorContext,
|
|
51
|
+
'validation' | 'schemaType'
|
|
52
|
+
>;
|
|
32
53
|
|
|
33
54
|
interface BaseActiveValidation<T> {
|
|
34
55
|
/** The validation is being actively shown */
|
|
@@ -52,7 +73,10 @@ type StatefulWarning = {
|
|
|
52
73
|
type: 'warning';
|
|
53
74
|
|
|
54
75
|
/** The underlying validation this tracks */
|
|
55
|
-
value:
|
|
76
|
+
value: ValidationObjectWithSource;
|
|
77
|
+
|
|
78
|
+
/** If this is currently preventing navigation from continuing */
|
|
79
|
+
isBlockingNavigation: boolean;
|
|
56
80
|
} & (
|
|
57
81
|
| {
|
|
58
82
|
/** warnings start with no state, but can active or dismissed */
|
|
@@ -67,7 +91,10 @@ type StatefulError = {
|
|
|
67
91
|
type: 'error';
|
|
68
92
|
|
|
69
93
|
/** The underlying validation this tracks */
|
|
70
|
-
value:
|
|
94
|
+
value: ValidationObjectWithSource;
|
|
95
|
+
|
|
96
|
+
/** If this is currently preventing navigation from continuing */
|
|
97
|
+
isBlockingNavigation: boolean;
|
|
71
98
|
} & (
|
|
72
99
|
| {
|
|
73
100
|
/** Errors start with no state an can be activated */
|
|
@@ -78,18 +105,26 @@ type StatefulError = {
|
|
|
78
105
|
|
|
79
106
|
export type StatefulValidationObject = StatefulWarning | StatefulError;
|
|
80
107
|
|
|
108
|
+
/** Helper function to determin if the subset is within the containingSet */
|
|
109
|
+
function isSubset<T>(subset: Set<T>, containingSet: Set<T>): boolean {
|
|
110
|
+
if (subset.size > containingSet.size) return false;
|
|
111
|
+
for (const entry of subset) if (!containingSet.has(entry)) return false;
|
|
112
|
+
return true;
|
|
113
|
+
}
|
|
114
|
+
|
|
81
115
|
/** Helper for initializing a validation object that tracks state */
|
|
82
116
|
function createStatefulValidationObject(
|
|
83
|
-
obj:
|
|
117
|
+
obj: ValidationObjectWithSource
|
|
84
118
|
): StatefulValidationObject {
|
|
85
119
|
return {
|
|
86
120
|
value: obj,
|
|
87
121
|
type: obj.severity,
|
|
88
122
|
state: 'none',
|
|
123
|
+
isBlockingNavigation: false,
|
|
89
124
|
};
|
|
90
125
|
}
|
|
91
126
|
|
|
92
|
-
type ValidationRunner = (obj:
|
|
127
|
+
type ValidationRunner = (obj: ValidationObjectWithHandler) =>
|
|
93
128
|
| {
|
|
94
129
|
/** A validation message */
|
|
95
130
|
message: string;
|
|
@@ -98,7 +133,7 @@ type ValidationRunner = (obj: ValidationObject) =>
|
|
|
98
133
|
|
|
99
134
|
/** A class that manages validating bindings across phases */
|
|
100
135
|
class ValidatedBinding {
|
|
101
|
-
|
|
136
|
+
public currentPhase?: Validation.Trigger;
|
|
102
137
|
private applicableValidations: Array<StatefulValidationObject> = [];
|
|
103
138
|
private validationsByState: Record<
|
|
104
139
|
Validation.Trigger,
|
|
@@ -109,11 +144,16 @@ class ValidatedBinding {
|
|
|
109
144
|
navigation: [],
|
|
110
145
|
};
|
|
111
146
|
|
|
147
|
+
public get allValidations(): Array<StatefulValidationObject> {
|
|
148
|
+
return Object.values(this.validationsByState).flat();
|
|
149
|
+
}
|
|
150
|
+
|
|
112
151
|
public weakBindings: Set<BindingInstance>;
|
|
152
|
+
|
|
113
153
|
private onDismiss?: () => void;
|
|
114
154
|
|
|
115
155
|
constructor(
|
|
116
|
-
possibleValidations: Array<
|
|
156
|
+
possibleValidations: Array<ValidationObjectWithSource>,
|
|
117
157
|
onDismiss?: () => void,
|
|
118
158
|
log?: Logger,
|
|
119
159
|
weakBindings?: Set<BindingInstance>
|
|
@@ -123,9 +163,8 @@ class ValidatedBinding {
|
|
|
123
163
|
const { trigger } = vObj;
|
|
124
164
|
|
|
125
165
|
if (this.validationsByState[trigger]) {
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
);
|
|
166
|
+
const statefulValidationObject = createStatefulValidationObject(vObj);
|
|
167
|
+
this.validationsByState[trigger].push(statefulValidationObject);
|
|
129
168
|
} else {
|
|
130
169
|
log?.warn(`Unknown validation trigger: ${trigger}`);
|
|
131
170
|
}
|
|
@@ -133,87 +172,125 @@ class ValidatedBinding {
|
|
|
133
172
|
this.weakBindings = weakBindings ?? new Set();
|
|
134
173
|
}
|
|
135
174
|
|
|
175
|
+
private checkIfBlocking(statefulObj: StatefulValidationObject) {
|
|
176
|
+
if (statefulObj.state === 'active') {
|
|
177
|
+
const { isBlockingNavigation } = statefulObj;
|
|
178
|
+
return isBlockingNavigation;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
return false;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
public getAll(): Array<ValidationResponse> {
|
|
185
|
+
return this.applicableValidations.reduce((all, statefulObj) => {
|
|
186
|
+
if (statefulObj.state === 'active' && statefulObj.response) {
|
|
187
|
+
return [
|
|
188
|
+
...all,
|
|
189
|
+
{
|
|
190
|
+
...statefulObj.response,
|
|
191
|
+
blocking: this.checkIfBlocking(statefulObj),
|
|
192
|
+
},
|
|
193
|
+
];
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
return all;
|
|
197
|
+
}, [] as Array<ValidationResponse>);
|
|
198
|
+
}
|
|
199
|
+
|
|
136
200
|
public get(): ValidationResponse | undefined {
|
|
137
|
-
const
|
|
138
|
-
|
|
139
|
-
this.currentPhase === 'navigation' ? statefulObj.value.blocking : true;
|
|
140
|
-
return statefulObj.state === 'active' && blocking !== false;
|
|
201
|
+
const firstInvalid = this.applicableValidations.find((statefulObj) => {
|
|
202
|
+
return statefulObj.state === 'active' && statefulObj.response;
|
|
141
203
|
});
|
|
142
204
|
|
|
143
|
-
if (
|
|
144
|
-
return
|
|
205
|
+
if (firstInvalid?.state === 'active') {
|
|
206
|
+
return {
|
|
207
|
+
...firstInvalid.response,
|
|
208
|
+
blocking: this.checkIfBlocking(firstInvalid),
|
|
209
|
+
};
|
|
145
210
|
}
|
|
146
211
|
}
|
|
147
212
|
|
|
148
213
|
private runApplicableValidations(
|
|
149
214
|
runner: ValidationRunner,
|
|
150
|
-
canDismiss: boolean
|
|
215
|
+
canDismiss: boolean,
|
|
216
|
+
phase: Validation.Trigger
|
|
151
217
|
) {
|
|
152
218
|
// If the currentState is not load, skip those
|
|
153
|
-
this.applicableValidations = this.applicableValidations.map(
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
219
|
+
this.applicableValidations = this.applicableValidations.map(
|
|
220
|
+
(originalValue) => {
|
|
221
|
+
if (originalValue.state === 'dismissed') {
|
|
222
|
+
// Don't rerun any dismissed warnings
|
|
223
|
+
return originalValue;
|
|
224
|
+
}
|
|
158
225
|
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
(
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
dismissable
|
|
170
|
-
) {
|
|
171
|
-
if (obj.value.severity === 'warning') {
|
|
172
|
-
const warn = obj as ActiveWarning;
|
|
173
|
-
if (warn.dismissable && warn.response.dismiss) {
|
|
174
|
-
warn.response.dismiss();
|
|
175
|
-
} else {
|
|
176
|
-
warn.dismissable = true;
|
|
177
|
-
}
|
|
226
|
+
// treat all warnings the same and block it once (unless blocking is true)
|
|
227
|
+
const blocking =
|
|
228
|
+
originalValue.value.blocking ??
|
|
229
|
+
((originalValue.value.severity === 'warning' && 'once') || true);
|
|
230
|
+
|
|
231
|
+
const obj = setIn(
|
|
232
|
+
originalValue,
|
|
233
|
+
['value', 'blocking'],
|
|
234
|
+
blocking
|
|
235
|
+
) as StatefulValidationObject;
|
|
178
236
|
|
|
179
|
-
|
|
237
|
+
const isBlockingNavigation =
|
|
238
|
+
blocking === true || (blocking === 'once' && !canDismiss);
|
|
239
|
+
|
|
240
|
+
if (
|
|
241
|
+
phase === 'navigation' &&
|
|
242
|
+
obj.state === 'active' &&
|
|
243
|
+
obj.value.blocking !== true
|
|
244
|
+
) {
|
|
245
|
+
if (obj.value.severity === 'warning') {
|
|
246
|
+
const warn = obj as ActiveWarning;
|
|
247
|
+
if (
|
|
248
|
+
warn.dismissable &&
|
|
249
|
+
warn.response.dismiss &&
|
|
250
|
+
(warn.response.blocking !== 'once' || !warn.response.blocking)
|
|
251
|
+
) {
|
|
252
|
+
warn.response.dismiss();
|
|
253
|
+
} else {
|
|
254
|
+
if (warn?.response.blocking === 'once') {
|
|
255
|
+
warn.response.blocking = false;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
warn.dismissable = true;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
return warn as StatefulValidationObject;
|
|
262
|
+
}
|
|
180
263
|
}
|
|
181
264
|
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
265
|
+
const response = runner(obj.value);
|
|
266
|
+
|
|
267
|
+
const newState = {
|
|
268
|
+
type: obj.type,
|
|
269
|
+
value: obj.value,
|
|
270
|
+
state: response ? 'active' : 'none',
|
|
271
|
+
isBlockingNavigation,
|
|
272
|
+
dismissable:
|
|
273
|
+
obj.value.severity === 'warning' && phase === 'navigation',
|
|
274
|
+
response: response
|
|
275
|
+
? {
|
|
276
|
+
...obj.value,
|
|
277
|
+
message: response.message ?? 'Something is broken',
|
|
278
|
+
severity: obj.value.severity,
|
|
279
|
+
displayTarget: obj.value.displayTarget ?? 'field',
|
|
280
|
+
}
|
|
281
|
+
: undefined,
|
|
282
|
+
} as StatefulValidationObject;
|
|
283
|
+
|
|
284
|
+
if (newState.state === 'active' && obj.value.severity === 'warning') {
|
|
285
|
+
(newState.response as WarningValidationResponse).dismiss = () => {
|
|
286
|
+
(newState as StatefulWarning).state = 'dismissed';
|
|
287
|
+
this.onDismiss?.();
|
|
288
|
+
};
|
|
186
289
|
}
|
|
187
|
-
}
|
|
188
290
|
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
const newState = {
|
|
192
|
-
type: obj.type,
|
|
193
|
-
value: obj.value,
|
|
194
|
-
state: response ? 'active' : 'none',
|
|
195
|
-
dismissable:
|
|
196
|
-
obj.value.severity === 'warning' &&
|
|
197
|
-
this.currentPhase === 'navigation',
|
|
198
|
-
response: response
|
|
199
|
-
? {
|
|
200
|
-
...obj.value,
|
|
201
|
-
message: response.message ?? 'Something is broken',
|
|
202
|
-
severity: obj.value.severity,
|
|
203
|
-
displayTarget: obj.value.displayTarget ?? 'field',
|
|
204
|
-
}
|
|
205
|
-
: undefined,
|
|
206
|
-
} as StatefulValidationObject;
|
|
207
|
-
|
|
208
|
-
if (newState.state === 'active' && obj.value.severity === 'warning') {
|
|
209
|
-
(newState.response as WarningValidationResponse).dismiss = () => {
|
|
210
|
-
(newState as StatefulWarning).state = 'dismissed';
|
|
211
|
-
this.onDismiss?.();
|
|
212
|
-
};
|
|
291
|
+
return newState;
|
|
213
292
|
}
|
|
214
|
-
|
|
215
|
-
return newState;
|
|
216
|
-
});
|
|
293
|
+
);
|
|
217
294
|
}
|
|
218
295
|
|
|
219
296
|
public update(
|
|
@@ -221,6 +298,8 @@ class ValidatedBinding {
|
|
|
221
298
|
canDismiss: boolean,
|
|
222
299
|
runner: ValidationRunner
|
|
223
300
|
) {
|
|
301
|
+
const newApplicableValidations: StatefulValidationObject[] = [];
|
|
302
|
+
|
|
224
303
|
if (phase === 'load' && this.currentPhase !== undefined) {
|
|
225
304
|
// Tried to run the 'load' phase twice. Aborting
|
|
226
305
|
return;
|
|
@@ -228,7 +307,7 @@ class ValidatedBinding {
|
|
|
228
307
|
|
|
229
308
|
if (this.currentPhase === 'navigation' || phase === this.currentPhase) {
|
|
230
309
|
// Already added all the types. No need to continue adding new validations
|
|
231
|
-
this.runApplicableValidations(runner, canDismiss);
|
|
310
|
+
this.runApplicableValidations(runner, canDismiss, phase);
|
|
232
311
|
return;
|
|
233
312
|
}
|
|
234
313
|
|
|
@@ -247,15 +326,30 @@ class ValidatedBinding {
|
|
|
247
326
|
(this.currentPhase === 'load' || this.currentPhase === 'change')
|
|
248
327
|
) {
|
|
249
328
|
// Can transition to a nav state from a change or load
|
|
329
|
+
|
|
330
|
+
// if there is an non-blocking error that is active then remove the error from applicable validations so it can no longer be shown
|
|
331
|
+
// which is needed if there are additional warnings to become active for that binding after the error is shown
|
|
332
|
+
this.applicableValidations.forEach((element) => {
|
|
333
|
+
if (
|
|
334
|
+
!(
|
|
335
|
+
element.type === 'error' &&
|
|
336
|
+
element.state === 'active' &&
|
|
337
|
+
element.isBlockingNavigation === false
|
|
338
|
+
)
|
|
339
|
+
) {
|
|
340
|
+
newApplicableValidations.push(element);
|
|
341
|
+
}
|
|
342
|
+
});
|
|
343
|
+
|
|
250
344
|
this.applicableValidations = [
|
|
251
|
-
...
|
|
252
|
-
...(this.currentPhase === 'load' ? this.validationsByState.change : []),
|
|
345
|
+
...newApplicableValidations,
|
|
253
346
|
...this.validationsByState.navigation,
|
|
347
|
+
...(this.currentPhase === 'load' ? this.validationsByState.change : []),
|
|
254
348
|
];
|
|
255
349
|
this.currentPhase = 'navigation';
|
|
256
350
|
}
|
|
257
351
|
|
|
258
|
-
this.runApplicableValidations(runner, canDismiss);
|
|
352
|
+
this.runApplicableValidations(runner, canDismiss, phase);
|
|
259
353
|
}
|
|
260
354
|
}
|
|
261
355
|
|
|
@@ -292,21 +386,48 @@ export class ValidationController implements BindingTracker {
|
|
|
292
386
|
onRemoveValidation: new SyncWaterfallHook<
|
|
293
387
|
[ValidationResponse, BindingInstance]
|
|
294
388
|
>(),
|
|
389
|
+
|
|
390
|
+
resolveValidationProviders: new SyncWaterfallHook<
|
|
391
|
+
[
|
|
392
|
+
Array<{
|
|
393
|
+
/** The name of the provider */
|
|
394
|
+
source: string;
|
|
395
|
+
/** The provider itself */
|
|
396
|
+
provider: ValidationProvider;
|
|
397
|
+
}>
|
|
398
|
+
],
|
|
399
|
+
{
|
|
400
|
+
/** The view this is triggered for */
|
|
401
|
+
view?: ViewInstance;
|
|
402
|
+
}
|
|
403
|
+
>(),
|
|
404
|
+
|
|
405
|
+
/** A hook called when a binding is added to the tracker */
|
|
406
|
+
onTrackBinding: new SyncHook<[BindingInstance]>(),
|
|
295
407
|
};
|
|
296
408
|
|
|
297
409
|
private tracker: BindingTracker | undefined;
|
|
298
410
|
private validations = new Map<BindingInstance, ValidatedBinding>();
|
|
299
411
|
private validatorRegistry?: ValidatorRegistry;
|
|
300
412
|
private schema: SchemaController;
|
|
301
|
-
|
|
413
|
+
|
|
414
|
+
private providers:
|
|
415
|
+
| Array<{
|
|
416
|
+
/** The name of the provider */
|
|
417
|
+
source: string;
|
|
418
|
+
/** The provider itself */
|
|
419
|
+
provider: ValidationProvider;
|
|
420
|
+
}>
|
|
421
|
+
| undefined;
|
|
422
|
+
|
|
423
|
+
private viewValidationProvider?: ValidationProvider;
|
|
302
424
|
private options?: SimpleValidatorContext;
|
|
303
425
|
private weakBindingTracker = new Set<BindingInstance>();
|
|
304
|
-
private lastActiveBindings = new Set<BindingInstance>();
|
|
305
426
|
|
|
306
427
|
constructor(schema: SchemaController, options?: SimpleValidatorContext) {
|
|
307
428
|
this.schema = schema;
|
|
308
429
|
this.options = options;
|
|
309
|
-
this.
|
|
430
|
+
this.reset();
|
|
310
431
|
}
|
|
311
432
|
|
|
312
433
|
setOptions(options: SimpleValidatorContext) {
|
|
@@ -316,6 +437,22 @@ export class ValidationController implements BindingTracker {
|
|
|
316
437
|
/** Return the middleware for the data-model to stop propagation of invalid data */
|
|
317
438
|
public getDataMiddleware(): Array<DataModelMiddleware> {
|
|
318
439
|
return [
|
|
440
|
+
{
|
|
441
|
+
set: (transaction, options, next) => {
|
|
442
|
+
return next?.set(transaction, options) ?? [];
|
|
443
|
+
},
|
|
444
|
+
get: (binding, options, next) => {
|
|
445
|
+
return next?.get(binding, options);
|
|
446
|
+
},
|
|
447
|
+
delete: (binding, options, next) => {
|
|
448
|
+
this.validations = removeBindingAndChildrenFromMap(
|
|
449
|
+
this.validations,
|
|
450
|
+
binding
|
|
451
|
+
);
|
|
452
|
+
|
|
453
|
+
return next?.delete(binding, options);
|
|
454
|
+
},
|
|
455
|
+
},
|
|
319
456
|
new ValidationMiddleware(
|
|
320
457
|
(binding) => {
|
|
321
458
|
if (!this.options) {
|
|
@@ -323,7 +460,6 @@ export class ValidationController implements BindingTracker {
|
|
|
323
460
|
}
|
|
324
461
|
|
|
325
462
|
this.updateValidationsForBinding(binding, 'change', this.options);
|
|
326
|
-
|
|
327
463
|
const strongValidation = this.getValidationForBinding(binding);
|
|
328
464
|
|
|
329
465
|
// return validation issues directly on bindings first
|
|
@@ -342,15 +478,17 @@ export class ValidationController implements BindingTracker {
|
|
|
342
478
|
weakValidation?.get()?.severity === 'error'
|
|
343
479
|
) {
|
|
344
480
|
weakValidation?.weakBindings.forEach((weakBinding) => {
|
|
345
|
-
weakBinding === strongBinding
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
481
|
+
if (weakBinding === strongBinding) {
|
|
482
|
+
newInvalidBindings.add({
|
|
483
|
+
binding: weakBinding,
|
|
484
|
+
isStrong: true,
|
|
485
|
+
});
|
|
486
|
+
} else {
|
|
487
|
+
newInvalidBindings.add({
|
|
488
|
+
binding: weakBinding,
|
|
489
|
+
isStrong: false,
|
|
490
|
+
});
|
|
491
|
+
}
|
|
354
492
|
});
|
|
355
493
|
}
|
|
356
494
|
});
|
|
@@ -364,9 +502,44 @@ export class ValidationController implements BindingTracker {
|
|
|
364
502
|
];
|
|
365
503
|
}
|
|
366
504
|
|
|
367
|
-
|
|
505
|
+
private getValidationProviders() {
|
|
506
|
+
if (this.providers) {
|
|
507
|
+
return this.providers;
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
this.providers = this.hooks.resolveValidationProviders.call([
|
|
511
|
+
{
|
|
512
|
+
source: SCHEMA_VALIDATION_PROVIDER_NAME,
|
|
513
|
+
provider: this.schema,
|
|
514
|
+
},
|
|
515
|
+
{
|
|
516
|
+
source: VIEW_VALIDATION_PROVIDER_NAME,
|
|
517
|
+
provider: {
|
|
518
|
+
getValidationsForBinding: (
|
|
519
|
+
binding: BindingInstance
|
|
520
|
+
): Array<ValidationObject> | undefined => {
|
|
521
|
+
return this.viewValidationProvider?.getValidationsForBinding?.(
|
|
522
|
+
binding
|
|
523
|
+
);
|
|
524
|
+
},
|
|
525
|
+
|
|
526
|
+
getValidationsForView: (): Array<ValidationObject> | undefined => {
|
|
527
|
+
return this.viewValidationProvider?.getValidationsForView?.();
|
|
528
|
+
},
|
|
529
|
+
},
|
|
530
|
+
},
|
|
531
|
+
]);
|
|
532
|
+
|
|
533
|
+
return this.providers;
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
public reset() {
|
|
368
537
|
this.validations.clear();
|
|
538
|
+
this.tracker = undefined;
|
|
539
|
+
}
|
|
369
540
|
|
|
541
|
+
public onView(view: ViewInstance): void {
|
|
542
|
+
this.validations.clear();
|
|
370
543
|
if (!this.options) {
|
|
371
544
|
return;
|
|
372
545
|
}
|
|
@@ -375,7 +548,10 @@ export class ValidationController implements BindingTracker {
|
|
|
375
548
|
...this.options,
|
|
376
549
|
callbacks: {
|
|
377
550
|
onAdd: (binding) => {
|
|
378
|
-
if (
|
|
551
|
+
if (
|
|
552
|
+
!this.options ||
|
|
553
|
+
this.getValidationForBinding(binding) !== undefined
|
|
554
|
+
) {
|
|
379
555
|
return;
|
|
380
556
|
}
|
|
381
557
|
|
|
@@ -400,30 +576,43 @@ export class ValidationController implements BindingTracker {
|
|
|
400
576
|
view.update(new Set([binding]));
|
|
401
577
|
}
|
|
402
578
|
);
|
|
579
|
+
|
|
580
|
+
this.hooks.onTrackBinding.call(binding);
|
|
403
581
|
},
|
|
404
582
|
},
|
|
405
583
|
});
|
|
406
584
|
|
|
407
585
|
this.tracker = bindingTrackerPlugin;
|
|
408
|
-
this.
|
|
586
|
+
this.viewValidationProvider = view;
|
|
409
587
|
|
|
410
588
|
bindingTrackerPlugin.apply(view);
|
|
411
589
|
}
|
|
412
590
|
|
|
413
|
-
|
|
591
|
+
updateValidationsForBinding(
|
|
414
592
|
binding: BindingInstance,
|
|
415
593
|
trigger: Validation.Trigger,
|
|
416
|
-
|
|
594
|
+
validationContext?: SimpleValidatorContext,
|
|
417
595
|
onDismiss?: () => void
|
|
418
596
|
): void {
|
|
597
|
+
const context = validationContext ?? this.options;
|
|
598
|
+
|
|
599
|
+
if (!context) {
|
|
600
|
+
throw new Error(`Context is required for executing validations`);
|
|
601
|
+
}
|
|
602
|
+
|
|
419
603
|
if (trigger === 'load') {
|
|
420
604
|
// Get all of the validations from each provider
|
|
421
|
-
const possibleValidations = this.
|
|
422
|
-
Array<
|
|
605
|
+
const possibleValidations = this.getValidationProviders().reduce<
|
|
606
|
+
Array<ValidationObjectWithSource>
|
|
423
607
|
>(
|
|
424
608
|
(vals, provider) => [
|
|
425
609
|
...vals,
|
|
426
|
-
...(provider.
|
|
610
|
+
...(provider.provider
|
|
611
|
+
.getValidationsForBinding?.(binding)
|
|
612
|
+
?.map((valObj) => ({
|
|
613
|
+
...valObj,
|
|
614
|
+
[VALIDATION_PROVIDER_NAME_SYMBOL]: provider.source,
|
|
615
|
+
})) ?? []),
|
|
427
616
|
],
|
|
428
617
|
[]
|
|
429
618
|
);
|
|
@@ -444,7 +633,7 @@ export class ValidationController implements BindingTracker {
|
|
|
444
633
|
|
|
445
634
|
const trackedValidations = this.validations.get(binding);
|
|
446
635
|
trackedValidations?.update(trigger, true, (validationObj) => {
|
|
447
|
-
const response = this.validationRunner(validationObj,
|
|
636
|
+
const response = this.validationRunner(validationObj, binding, context);
|
|
448
637
|
|
|
449
638
|
if (this.weakBindingTracker.size > 0) {
|
|
450
639
|
const t = this.validations.get(binding) as ValidatedBinding;
|
|
@@ -464,8 +653,8 @@ export class ValidationController implements BindingTracker {
|
|
|
464
653
|
validation.update(trigger, true, (validationObj) => {
|
|
465
654
|
const response = this.validationRunner(
|
|
466
655
|
validationObj,
|
|
467
|
-
|
|
468
|
-
|
|
656
|
+
vBinding,
|
|
657
|
+
context
|
|
469
658
|
);
|
|
470
659
|
return response ? { message: response.message } : undefined;
|
|
471
660
|
});
|
|
@@ -474,21 +663,28 @@ export class ValidationController implements BindingTracker {
|
|
|
474
663
|
}
|
|
475
664
|
}
|
|
476
665
|
|
|
477
|
-
|
|
478
|
-
validationObj:
|
|
479
|
-
|
|
480
|
-
|
|
666
|
+
validationRunner(
|
|
667
|
+
validationObj: ValidationObjectWithHandler,
|
|
668
|
+
binding: BindingInstance,
|
|
669
|
+
context: SimpleValidatorContext | undefined = this.options
|
|
481
670
|
) {
|
|
482
|
-
|
|
671
|
+
if (!context) {
|
|
672
|
+
throw new Error('No context provided to validation runner');
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
const handler =
|
|
676
|
+
validationObj.handler ?? this.getValidator(validationObj.type);
|
|
677
|
+
|
|
483
678
|
const weakBindings = new Set<BindingInstance>();
|
|
484
679
|
|
|
485
680
|
// For any data-gets in the validation runner, default to using the _invalid_ value (since that's what we're testing against)
|
|
486
681
|
const model: DataModelWithParser = {
|
|
487
|
-
get(b, options
|
|
682
|
+
get(b, options) {
|
|
488
683
|
weakBindings.add(isBinding(b) ? binding : context.parseBinding(b));
|
|
489
|
-
return context.model.get(b, options);
|
|
684
|
+
return context.model.get(b, { ...options, includeInvalid: true });
|
|
490
685
|
},
|
|
491
686
|
set: context.model.set,
|
|
687
|
+
delete: context.model.delete,
|
|
492
688
|
};
|
|
493
689
|
|
|
494
690
|
const result = handler?.(
|
|
@@ -500,6 +696,7 @@ export class ValidationController implements BindingTracker {
|
|
|
500
696
|
) => context.evaluate(exp, options),
|
|
501
697
|
model,
|
|
502
698
|
validation: validationObj,
|
|
699
|
+
schemaType: this.schema.getType(binding),
|
|
503
700
|
},
|
|
504
701
|
context.model.get(binding, {
|
|
505
702
|
includeInvalid: true,
|
|
@@ -519,7 +716,6 @@ export class ValidationController implements BindingTracker {
|
|
|
519
716
|
model,
|
|
520
717
|
evaluate: context.evaluate,
|
|
521
718
|
});
|
|
522
|
-
|
|
523
719
|
if (parameters) {
|
|
524
720
|
message = replaceParams(message, parameters);
|
|
525
721
|
}
|
|
@@ -532,31 +728,34 @@ export class ValidationController implements BindingTracker {
|
|
|
532
728
|
}
|
|
533
729
|
|
|
534
730
|
private updateValidationsForView(trigger: Validation.Trigger): void {
|
|
535
|
-
const
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
731
|
+
const isNavigationTrigger = trigger === 'navigation';
|
|
732
|
+
const lastActiveBindings = this.activeBindings;
|
|
733
|
+
|
|
734
|
+
/** Run validations for all bindings in view */
|
|
735
|
+
const updateValidations = (dismissValidations: boolean) => {
|
|
736
|
+
this.getBindings().forEach((binding) => {
|
|
737
|
+
this.validations
|
|
738
|
+
.get(binding)
|
|
739
|
+
?.update(trigger, dismissValidations, (obj) => {
|
|
740
|
+
if (!this.options) {
|
|
741
|
+
return;
|
|
742
|
+
}
|
|
546
743
|
|
|
547
|
-
|
|
744
|
+
return this.validationRunner(obj, binding, this.options);
|
|
745
|
+
});
|
|
548
746
|
});
|
|
549
|
-
}
|
|
747
|
+
};
|
|
550
748
|
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
}
|
|
554
|
-
}
|
|
749
|
+
// Should dismiss for non-navigation triggers.
|
|
750
|
+
updateValidations(!isNavigationTrigger);
|
|
555
751
|
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
752
|
+
if (isNavigationTrigger) {
|
|
753
|
+
// If validations didn't change since last update, dismiss all dismissible validations.
|
|
754
|
+
const { activeBindings } = this;
|
|
755
|
+
if (isSubset(activeBindings, lastActiveBindings)) {
|
|
756
|
+
updateValidations(true);
|
|
757
|
+
}
|
|
758
|
+
}
|
|
560
759
|
}
|
|
561
760
|
|
|
562
761
|
private get activeBindings(): Set<BindingInstance> {
|
|
@@ -583,6 +782,10 @@ export class ValidationController implements BindingTracker {
|
|
|
583
782
|
return this.tracker?.getBindings() ?? new Set();
|
|
584
783
|
}
|
|
585
784
|
|
|
785
|
+
trackBinding(binding: BindingInstance): void {
|
|
786
|
+
this.tracker?.trackBinding(binding);
|
|
787
|
+
}
|
|
788
|
+
|
|
586
789
|
/** Executes all known validations for the tracked bindings using the given model */
|
|
587
790
|
validateView(trigger: Validation.Trigger = 'navigation'): {
|
|
588
791
|
/** Indicating if the view can proceed without error */
|
|
@@ -595,26 +798,35 @@ export class ValidationController implements BindingTracker {
|
|
|
595
798
|
|
|
596
799
|
const validations = new Map<BindingInstance, ValidationResponse>();
|
|
597
800
|
|
|
801
|
+
let canTransition = true;
|
|
802
|
+
|
|
598
803
|
this.getBindings().forEach((b) => {
|
|
599
|
-
const
|
|
804
|
+
const allValidations = this.getValidationForBinding(b)?.getAll();
|
|
805
|
+
|
|
806
|
+
allValidations?.forEach((v) => {
|
|
807
|
+
if (trigger === 'navigation' && v.blocking) {
|
|
808
|
+
this.options?.logger.debug(
|
|
809
|
+
`Validation on binding: ${b.asString()} is preventing navigation. ${JSON.stringify(
|
|
810
|
+
v
|
|
811
|
+
)}`
|
|
812
|
+
);
|
|
600
813
|
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
`Validation on binding: ${b.asString()} is preventing navigation. ${JSON.stringify(
|
|
604
|
-
invalid
|
|
605
|
-
)}`
|
|
606
|
-
);
|
|
814
|
+
canTransition = false;
|
|
815
|
+
}
|
|
607
816
|
|
|
608
|
-
validations.
|
|
609
|
-
|
|
817
|
+
if (!validations.has(b)) {
|
|
818
|
+
validations.set(b, v);
|
|
819
|
+
}
|
|
820
|
+
});
|
|
610
821
|
});
|
|
611
822
|
|
|
612
823
|
return {
|
|
613
|
-
canTransition
|
|
824
|
+
canTransition,
|
|
614
825
|
validations: validations.size ? validations : undefined,
|
|
615
826
|
};
|
|
616
827
|
}
|
|
617
828
|
|
|
829
|
+
/** Get the current tracked validation for the given binding */
|
|
618
830
|
public getValidationForBinding(
|
|
619
831
|
binding: BindingInstance
|
|
620
832
|
): ValidatedBinding | undefined {
|
|
@@ -626,11 +838,10 @@ export class ValidationController implements BindingTracker {
|
|
|
626
838
|
_getValidationForBinding: (binding) => {
|
|
627
839
|
return this.getValidationForBinding(
|
|
628
840
|
isBinding(binding) ? binding : parser(binding)
|
|
629
|
-
)
|
|
841
|
+
);
|
|
630
842
|
},
|
|
631
843
|
getAll: () => {
|
|
632
844
|
const bindings = this.getBindings();
|
|
633
|
-
|
|
634
845
|
if (bindings.size === 0) {
|
|
635
846
|
return undefined;
|
|
636
847
|
}
|
|
@@ -653,6 +864,9 @@ export class ValidationController implements BindingTracker {
|
|
|
653
864
|
get() {
|
|
654
865
|
throw new Error('Error Access be provided by the view plugin');
|
|
655
866
|
},
|
|
867
|
+
getValidationsForBinding() {
|
|
868
|
+
throw new Error('Error rollup should be provided by the view plugin');
|
|
869
|
+
},
|
|
656
870
|
getChildren() {
|
|
657
871
|
throw new Error('Error rollup should be provided by the view plugin');
|
|
658
872
|
},
|
|
@@ -664,7 +878,7 @@ export class ValidationController implements BindingTracker {
|
|
|
664
878
|
},
|
|
665
879
|
register: () => {
|
|
666
880
|
throw new Error(
|
|
667
|
-
'Section
|
|
881
|
+
'Section functionality should be provided by the view plugin'
|
|
668
882
|
);
|
|
669
883
|
},
|
|
670
884
|
type: (binding) =>
|