@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.
- package/dist/index.cjs.js +899 -336
- package/dist/index.d.ts +275 -93
- package/dist/index.esm.js +890 -334
- package/dist/player.dev.js +11429 -0
- package/dist/player.prod.js +2 -0
- package/package.json +16 -5
- package/src/binding/binding.ts +8 -0
- package/src/binding/index.ts +14 -4
- package/src/binding/resolver.ts +2 -4
- 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} +62 -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 +375 -148
- 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 +60 -8
- package/src/data/noop-model.ts +2 -0
- package/src/expressions/evaluator-functions.ts +24 -2
- package/src/expressions/evaluator.ts +38 -34
- package/src/expressions/index.ts +1 -0
- package/src/expressions/parser.ts +116 -44
- package/src/expressions/types.ts +50 -17
- package/src/expressions/utils.ts +143 -1
- package/src/player.ts +60 -46
- 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 +26 -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 +58 -6
- package/src/view/parser/index.ts +51 -3
- package/src/view/plugins/applicability.ts +1 -1
- package/src/view/plugins/string-resolver.ts +35 -9
- 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,12 +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,
|
|
17
|
+
StrongOrWeakBinding,
|
|
18
|
+
} from '../../validator';
|
|
19
|
+
import {
|
|
20
|
+
ValidationMiddleware,
|
|
21
|
+
ValidatorRegistry,
|
|
22
|
+
removeBindingAndChildrenFromMap,
|
|
15
23
|
} from '../../validator';
|
|
16
|
-
import { ValidationMiddleware, ValidatorRegistry } from '../../validator';
|
|
17
24
|
import type { Logger } from '../../logger';
|
|
18
25
|
import { ProxyLogger } from '../../logger';
|
|
19
26
|
import type { Resolve, ViewInstance } from '../../view';
|
|
@@ -27,7 +34,22 @@ import type {
|
|
|
27
34
|
import type { BindingTracker } from './binding-tracker';
|
|
28
35
|
import { ValidationBindingTrackerViewPlugin } from './binding-tracker';
|
|
29
36
|
|
|
30
|
-
|
|
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
|
+
>;
|
|
31
53
|
|
|
32
54
|
interface BaseActiveValidation<T> {
|
|
33
55
|
/** The validation is being actively shown */
|
|
@@ -51,7 +73,10 @@ type StatefulWarning = {
|
|
|
51
73
|
type: 'warning';
|
|
52
74
|
|
|
53
75
|
/** The underlying validation this tracks */
|
|
54
|
-
value:
|
|
76
|
+
value: ValidationObjectWithSource;
|
|
77
|
+
|
|
78
|
+
/** If this is currently preventing navigation from continuing */
|
|
79
|
+
isBlockingNavigation: boolean;
|
|
55
80
|
} & (
|
|
56
81
|
| {
|
|
57
82
|
/** warnings start with no state, but can active or dismissed */
|
|
@@ -66,7 +91,10 @@ type StatefulError = {
|
|
|
66
91
|
type: 'error';
|
|
67
92
|
|
|
68
93
|
/** The underlying validation this tracks */
|
|
69
|
-
value:
|
|
94
|
+
value: ValidationObjectWithSource;
|
|
95
|
+
|
|
96
|
+
/** If this is currently preventing navigation from continuing */
|
|
97
|
+
isBlockingNavigation: boolean;
|
|
70
98
|
} & (
|
|
71
99
|
| {
|
|
72
100
|
/** Errors start with no state an can be activated */
|
|
@@ -77,18 +105,26 @@ type StatefulError = {
|
|
|
77
105
|
|
|
78
106
|
export type StatefulValidationObject = StatefulWarning | StatefulError;
|
|
79
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
|
+
|
|
80
115
|
/** Helper for initializing a validation object that tracks state */
|
|
81
116
|
function createStatefulValidationObject(
|
|
82
|
-
obj:
|
|
117
|
+
obj: ValidationObjectWithSource
|
|
83
118
|
): StatefulValidationObject {
|
|
84
119
|
return {
|
|
85
120
|
value: obj,
|
|
86
121
|
type: obj.severity,
|
|
87
122
|
state: 'none',
|
|
123
|
+
isBlockingNavigation: false,
|
|
88
124
|
};
|
|
89
125
|
}
|
|
90
126
|
|
|
91
|
-
type ValidationRunner = (obj:
|
|
127
|
+
type ValidationRunner = (obj: ValidationObjectWithHandler) =>
|
|
92
128
|
| {
|
|
93
129
|
/** A validation message */
|
|
94
130
|
message: string;
|
|
@@ -97,7 +133,7 @@ type ValidationRunner = (obj: ValidationObject) =>
|
|
|
97
133
|
|
|
98
134
|
/** A class that manages validating bindings across phases */
|
|
99
135
|
class ValidatedBinding {
|
|
100
|
-
|
|
136
|
+
public currentPhase?: Validation.Trigger;
|
|
101
137
|
private applicableValidations: Array<StatefulValidationObject> = [];
|
|
102
138
|
private validationsByState: Record<
|
|
103
139
|
Validation.Trigger,
|
|
@@ -108,11 +144,16 @@ class ValidatedBinding {
|
|
|
108
144
|
navigation: [],
|
|
109
145
|
};
|
|
110
146
|
|
|
147
|
+
public get allValidations(): Array<StatefulValidationObject> {
|
|
148
|
+
return Object.values(this.validationsByState).flat();
|
|
149
|
+
}
|
|
150
|
+
|
|
111
151
|
public weakBindings: Set<BindingInstance>;
|
|
152
|
+
|
|
112
153
|
private onDismiss?: () => void;
|
|
113
154
|
|
|
114
155
|
constructor(
|
|
115
|
-
possibleValidations: Array<
|
|
156
|
+
possibleValidations: Array<ValidationObjectWithSource>,
|
|
116
157
|
onDismiss?: () => void,
|
|
117
158
|
log?: Logger,
|
|
118
159
|
weakBindings?: Set<BindingInstance>
|
|
@@ -122,9 +163,8 @@ class ValidatedBinding {
|
|
|
122
163
|
const { trigger } = vObj;
|
|
123
164
|
|
|
124
165
|
if (this.validationsByState[trigger]) {
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
);
|
|
166
|
+
const statefulValidationObject = createStatefulValidationObject(vObj);
|
|
167
|
+
this.validationsByState[trigger].push(statefulValidationObject);
|
|
128
168
|
} else {
|
|
129
169
|
log?.warn(`Unknown validation trigger: ${trigger}`);
|
|
130
170
|
}
|
|
@@ -132,87 +172,125 @@ class ValidatedBinding {
|
|
|
132
172
|
this.weakBindings = weakBindings ?? new Set();
|
|
133
173
|
}
|
|
134
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
|
+
|
|
135
200
|
public get(): ValidationResponse | undefined {
|
|
136
|
-
const
|
|
137
|
-
|
|
138
|
-
this.currentPhase === 'navigation' ? statefulObj.value.blocking : true;
|
|
139
|
-
return statefulObj.state === 'active' && blocking !== false;
|
|
201
|
+
const firstInvalid = this.applicableValidations.find((statefulObj) => {
|
|
202
|
+
return statefulObj.state === 'active' && statefulObj.response;
|
|
140
203
|
});
|
|
141
204
|
|
|
142
|
-
if (
|
|
143
|
-
return
|
|
205
|
+
if (firstInvalid?.state === 'active') {
|
|
206
|
+
return {
|
|
207
|
+
...firstInvalid.response,
|
|
208
|
+
blocking: this.checkIfBlocking(firstInvalid),
|
|
209
|
+
};
|
|
144
210
|
}
|
|
145
211
|
}
|
|
146
212
|
|
|
147
213
|
private runApplicableValidations(
|
|
148
214
|
runner: ValidationRunner,
|
|
149
|
-
canDismiss: boolean
|
|
215
|
+
canDismiss: boolean,
|
|
216
|
+
phase: Validation.Trigger
|
|
150
217
|
) {
|
|
151
218
|
// If the currentState is not load, skip those
|
|
152
|
-
this.applicableValidations = this.applicableValidations.map(
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
219
|
+
this.applicableValidations = this.applicableValidations.map(
|
|
220
|
+
(originalValue) => {
|
|
221
|
+
if (originalValue.state === 'dismissed') {
|
|
222
|
+
// Don't rerun any dismissed warnings
|
|
223
|
+
return originalValue;
|
|
224
|
+
}
|
|
157
225
|
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
(
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
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;
|
|
236
|
+
|
|
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
|
+
}
|
|
177
257
|
|
|
178
|
-
|
|
258
|
+
warn.dismissable = true;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
return warn as StatefulValidationObject;
|
|
262
|
+
}
|
|
179
263
|
}
|
|
180
264
|
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
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
|
+
};
|
|
185
289
|
}
|
|
186
|
-
}
|
|
187
290
|
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
const newState = {
|
|
191
|
-
type: obj.type,
|
|
192
|
-
value: obj.value,
|
|
193
|
-
state: response ? 'active' : 'none',
|
|
194
|
-
dismissable:
|
|
195
|
-
obj.value.severity === 'warning' &&
|
|
196
|
-
this.currentPhase === 'navigation',
|
|
197
|
-
response: response
|
|
198
|
-
? {
|
|
199
|
-
...obj.value,
|
|
200
|
-
message: response.message ?? 'Something is broken',
|
|
201
|
-
severity: obj.value.severity,
|
|
202
|
-
displayTarget: obj.value.displayTarget ?? 'field',
|
|
203
|
-
}
|
|
204
|
-
: undefined,
|
|
205
|
-
} as StatefulValidationObject;
|
|
206
|
-
|
|
207
|
-
if (newState.state === 'active' && obj.value.severity === 'warning') {
|
|
208
|
-
(newState.response as WarningValidationResponse).dismiss = () => {
|
|
209
|
-
(newState as StatefulWarning).state = 'dismissed';
|
|
210
|
-
this.onDismiss?.();
|
|
211
|
-
};
|
|
291
|
+
return newState;
|
|
212
292
|
}
|
|
213
|
-
|
|
214
|
-
return newState;
|
|
215
|
-
});
|
|
293
|
+
);
|
|
216
294
|
}
|
|
217
295
|
|
|
218
296
|
public update(
|
|
@@ -220,6 +298,8 @@ class ValidatedBinding {
|
|
|
220
298
|
canDismiss: boolean,
|
|
221
299
|
runner: ValidationRunner
|
|
222
300
|
) {
|
|
301
|
+
const newApplicableValidations: StatefulValidationObject[] = [];
|
|
302
|
+
|
|
223
303
|
if (phase === 'load' && this.currentPhase !== undefined) {
|
|
224
304
|
// Tried to run the 'load' phase twice. Aborting
|
|
225
305
|
return;
|
|
@@ -227,7 +307,7 @@ class ValidatedBinding {
|
|
|
227
307
|
|
|
228
308
|
if (this.currentPhase === 'navigation' || phase === this.currentPhase) {
|
|
229
309
|
// Already added all the types. No need to continue adding new validations
|
|
230
|
-
this.runApplicableValidations(runner, canDismiss);
|
|
310
|
+
this.runApplicableValidations(runner, canDismiss, phase);
|
|
231
311
|
return;
|
|
232
312
|
}
|
|
233
313
|
|
|
@@ -246,15 +326,30 @@ class ValidatedBinding {
|
|
|
246
326
|
(this.currentPhase === 'load' || this.currentPhase === 'change')
|
|
247
327
|
) {
|
|
248
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
|
+
|
|
249
344
|
this.applicableValidations = [
|
|
250
|
-
...
|
|
251
|
-
...(this.currentPhase === 'load' ? this.validationsByState.change : []),
|
|
345
|
+
...newApplicableValidations,
|
|
252
346
|
...this.validationsByState.navigation,
|
|
347
|
+
...(this.currentPhase === 'load' ? this.validationsByState.change : []),
|
|
253
348
|
];
|
|
254
349
|
this.currentPhase = 'navigation';
|
|
255
350
|
}
|
|
256
351
|
|
|
257
|
-
this.runApplicableValidations(runner, canDismiss);
|
|
352
|
+
this.runApplicableValidations(runner, canDismiss, phase);
|
|
258
353
|
}
|
|
259
354
|
}
|
|
260
355
|
|
|
@@ -291,21 +386,48 @@ export class ValidationController implements BindingTracker {
|
|
|
291
386
|
onRemoveValidation: new SyncWaterfallHook<
|
|
292
387
|
[ValidationResponse, BindingInstance]
|
|
293
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]>(),
|
|
294
407
|
};
|
|
295
408
|
|
|
296
409
|
private tracker: BindingTracker | undefined;
|
|
297
410
|
private validations = new Map<BindingInstance, ValidatedBinding>();
|
|
298
411
|
private validatorRegistry?: ValidatorRegistry;
|
|
299
412
|
private schema: SchemaController;
|
|
300
|
-
|
|
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;
|
|
301
424
|
private options?: SimpleValidatorContext;
|
|
302
425
|
private weakBindingTracker = new Set<BindingInstance>();
|
|
303
|
-
private lastActiveBindings = new Set<BindingInstance>();
|
|
304
426
|
|
|
305
427
|
constructor(schema: SchemaController, options?: SimpleValidatorContext) {
|
|
306
428
|
this.schema = schema;
|
|
307
429
|
this.options = options;
|
|
308
|
-
this.
|
|
430
|
+
this.reset();
|
|
309
431
|
}
|
|
310
432
|
|
|
311
433
|
setOptions(options: SimpleValidatorContext) {
|
|
@@ -315,6 +437,22 @@ export class ValidationController implements BindingTracker {
|
|
|
315
437
|
/** Return the middleware for the data-model to stop propagation of invalid data */
|
|
316
438
|
public getDataMiddleware(): Array<DataModelMiddleware> {
|
|
317
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
|
+
},
|
|
318
456
|
new ValidationMiddleware(
|
|
319
457
|
(binding) => {
|
|
320
458
|
if (!this.options) {
|
|
@@ -322,28 +460,38 @@ export class ValidationController implements BindingTracker {
|
|
|
322
460
|
}
|
|
323
461
|
|
|
324
462
|
this.updateValidationsForBinding(binding, 'change', this.options);
|
|
325
|
-
|
|
326
463
|
const strongValidation = this.getValidationForBinding(binding);
|
|
327
464
|
|
|
328
465
|
// return validation issues directly on bindings first
|
|
329
|
-
if (strongValidation?.get()
|
|
466
|
+
if (strongValidation?.get()?.severity === 'error') {
|
|
467
|
+
return strongValidation.get();
|
|
468
|
+
}
|
|
330
469
|
|
|
331
470
|
// if none, check to see any validations this binding may be a weak ref of and return
|
|
332
|
-
const newInvalidBindings: Set<
|
|
333
|
-
|
|
471
|
+
const newInvalidBindings: Set<StrongOrWeakBinding> = new Set();
|
|
472
|
+
this.validations.forEach((weakValidation, strongBinding) => {
|
|
334
473
|
if (
|
|
335
474
|
caresAboutDataChanges(
|
|
336
475
|
new Set([binding]),
|
|
337
476
|
weakValidation.weakBindings
|
|
338
477
|
) &&
|
|
339
|
-
weakValidation?.get()
|
|
478
|
+
weakValidation?.get()?.severity === 'error'
|
|
340
479
|
) {
|
|
341
|
-
weakValidation?.weakBindings.forEach(
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
480
|
+
weakValidation?.weakBindings.forEach((weakBinding) => {
|
|
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
|
+
}
|
|
492
|
+
});
|
|
345
493
|
}
|
|
346
|
-
}
|
|
494
|
+
});
|
|
347
495
|
|
|
348
496
|
if (newInvalidBindings.size > 0) {
|
|
349
497
|
return newInvalidBindings;
|
|
@@ -354,9 +502,44 @@ export class ValidationController implements BindingTracker {
|
|
|
354
502
|
];
|
|
355
503
|
}
|
|
356
504
|
|
|
357
|
-
|
|
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() {
|
|
358
537
|
this.validations.clear();
|
|
538
|
+
this.tracker = undefined;
|
|
539
|
+
}
|
|
359
540
|
|
|
541
|
+
public onView(view: ViewInstance): void {
|
|
542
|
+
this.validations.clear();
|
|
360
543
|
if (!this.options) {
|
|
361
544
|
return;
|
|
362
545
|
}
|
|
@@ -365,7 +548,10 @@ export class ValidationController implements BindingTracker {
|
|
|
365
548
|
...this.options,
|
|
366
549
|
callbacks: {
|
|
367
550
|
onAdd: (binding) => {
|
|
368
|
-
if (
|
|
551
|
+
if (
|
|
552
|
+
!this.options ||
|
|
553
|
+
this.getValidationForBinding(binding) !== undefined
|
|
554
|
+
) {
|
|
369
555
|
return;
|
|
370
556
|
}
|
|
371
557
|
|
|
@@ -376,7 +562,10 @@ export class ValidationController implements BindingTracker {
|
|
|
376
562
|
});
|
|
377
563
|
|
|
378
564
|
if (originalValue !== withoutDefault) {
|
|
379
|
-
|
|
565
|
+
// Don't trigger updates when setting the default value
|
|
566
|
+
this.options.model.set([[binding, originalValue]], {
|
|
567
|
+
silent: true,
|
|
568
|
+
});
|
|
380
569
|
}
|
|
381
570
|
|
|
382
571
|
this.updateValidationsForBinding(
|
|
@@ -387,30 +576,43 @@ export class ValidationController implements BindingTracker {
|
|
|
387
576
|
view.update(new Set([binding]));
|
|
388
577
|
}
|
|
389
578
|
);
|
|
579
|
+
|
|
580
|
+
this.hooks.onTrackBinding.call(binding);
|
|
390
581
|
},
|
|
391
582
|
},
|
|
392
583
|
});
|
|
393
584
|
|
|
394
585
|
this.tracker = bindingTrackerPlugin;
|
|
395
|
-
this.
|
|
586
|
+
this.viewValidationProvider = view;
|
|
396
587
|
|
|
397
588
|
bindingTrackerPlugin.apply(view);
|
|
398
589
|
}
|
|
399
590
|
|
|
400
|
-
|
|
591
|
+
updateValidationsForBinding(
|
|
401
592
|
binding: BindingInstance,
|
|
402
593
|
trigger: Validation.Trigger,
|
|
403
|
-
|
|
594
|
+
validationContext?: SimpleValidatorContext,
|
|
404
595
|
onDismiss?: () => void
|
|
405
596
|
): void {
|
|
597
|
+
const context = validationContext ?? this.options;
|
|
598
|
+
|
|
599
|
+
if (!context) {
|
|
600
|
+
throw new Error(`Context is required for executing validations`);
|
|
601
|
+
}
|
|
602
|
+
|
|
406
603
|
if (trigger === 'load') {
|
|
407
604
|
// Get all of the validations from each provider
|
|
408
|
-
const possibleValidations = this.
|
|
409
|
-
Array<
|
|
605
|
+
const possibleValidations = this.getValidationProviders().reduce<
|
|
606
|
+
Array<ValidationObjectWithSource>
|
|
410
607
|
>(
|
|
411
608
|
(vals, provider) => [
|
|
412
609
|
...vals,
|
|
413
|
-
...(provider.
|
|
610
|
+
...(provider.provider
|
|
611
|
+
.getValidationsForBinding?.(binding)
|
|
612
|
+
?.map((valObj) => ({
|
|
613
|
+
...valObj,
|
|
614
|
+
[VALIDATION_PROVIDER_NAME_SYMBOL]: provider.source,
|
|
615
|
+
})) ?? []),
|
|
414
616
|
],
|
|
415
617
|
[]
|
|
416
618
|
);
|
|
@@ -431,7 +633,7 @@ export class ValidationController implements BindingTracker {
|
|
|
431
633
|
|
|
432
634
|
const trackedValidations = this.validations.get(binding);
|
|
433
635
|
trackedValidations?.update(trigger, true, (validationObj) => {
|
|
434
|
-
const response = this.validationRunner(validationObj,
|
|
636
|
+
const response = this.validationRunner(validationObj, binding, context);
|
|
435
637
|
|
|
436
638
|
if (this.weakBindingTracker.size > 0) {
|
|
437
639
|
const t = this.validations.get(binding) as ValidatedBinding;
|
|
@@ -451,8 +653,8 @@ export class ValidationController implements BindingTracker {
|
|
|
451
653
|
validation.update(trigger, true, (validationObj) => {
|
|
452
654
|
const response = this.validationRunner(
|
|
453
655
|
validationObj,
|
|
454
|
-
|
|
455
|
-
|
|
656
|
+
vBinding,
|
|
657
|
+
context
|
|
456
658
|
);
|
|
457
659
|
return response ? { message: response.message } : undefined;
|
|
458
660
|
});
|
|
@@ -461,21 +663,28 @@ export class ValidationController implements BindingTracker {
|
|
|
461
663
|
}
|
|
462
664
|
}
|
|
463
665
|
|
|
464
|
-
|
|
465
|
-
validationObj:
|
|
466
|
-
|
|
467
|
-
|
|
666
|
+
validationRunner(
|
|
667
|
+
validationObj: ValidationObjectWithHandler,
|
|
668
|
+
binding: BindingInstance,
|
|
669
|
+
context: SimpleValidatorContext | undefined = this.options
|
|
468
670
|
) {
|
|
469
|
-
|
|
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
|
+
|
|
470
678
|
const weakBindings = new Set<BindingInstance>();
|
|
471
679
|
|
|
472
680
|
// For any data-gets in the validation runner, default to using the _invalid_ value (since that's what we're testing against)
|
|
473
681
|
const model: DataModelWithParser = {
|
|
474
|
-
get(b, options
|
|
682
|
+
get(b, options) {
|
|
475
683
|
weakBindings.add(isBinding(b) ? binding : context.parseBinding(b));
|
|
476
|
-
return context.model.get(b, options);
|
|
684
|
+
return context.model.get(b, { ...options, includeInvalid: true });
|
|
477
685
|
},
|
|
478
686
|
set: context.model.set,
|
|
687
|
+
delete: context.model.delete,
|
|
479
688
|
};
|
|
480
689
|
|
|
481
690
|
const result = handler?.(
|
|
@@ -487,6 +696,7 @@ export class ValidationController implements BindingTracker {
|
|
|
487
696
|
) => context.evaluate(exp, options),
|
|
488
697
|
model,
|
|
489
698
|
validation: validationObj,
|
|
699
|
+
schemaType: this.schema.getType(binding),
|
|
490
700
|
},
|
|
491
701
|
context.model.get(binding, {
|
|
492
702
|
includeInvalid: true,
|
|
@@ -506,7 +716,6 @@ export class ValidationController implements BindingTracker {
|
|
|
506
716
|
model,
|
|
507
717
|
evaluate: context.evaluate,
|
|
508
718
|
});
|
|
509
|
-
|
|
510
719
|
if (parameters) {
|
|
511
720
|
message = replaceParams(message, parameters);
|
|
512
721
|
}
|
|
@@ -519,31 +728,34 @@ export class ValidationController implements BindingTracker {
|
|
|
519
728
|
}
|
|
520
729
|
|
|
521
730
|
private updateValidationsForView(trigger: Validation.Trigger): void {
|
|
522
|
-
const
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
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
|
+
}
|
|
533
743
|
|
|
534
|
-
|
|
744
|
+
return this.validationRunner(obj, binding, this.options);
|
|
745
|
+
});
|
|
535
746
|
});
|
|
536
|
-
}
|
|
747
|
+
};
|
|
537
748
|
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
}
|
|
541
|
-
}
|
|
749
|
+
// Should dismiss for non-navigation triggers.
|
|
750
|
+
updateValidations(!isNavigationTrigger);
|
|
542
751
|
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
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
|
+
}
|
|
547
759
|
}
|
|
548
760
|
|
|
549
761
|
private get activeBindings(): Set<BindingInstance> {
|
|
@@ -570,6 +782,10 @@ export class ValidationController implements BindingTracker {
|
|
|
570
782
|
return this.tracker?.getBindings() ?? new Set();
|
|
571
783
|
}
|
|
572
784
|
|
|
785
|
+
trackBinding(binding: BindingInstance): void {
|
|
786
|
+
this.tracker?.trackBinding(binding);
|
|
787
|
+
}
|
|
788
|
+
|
|
573
789
|
/** Executes all known validations for the tracked bindings using the given model */
|
|
574
790
|
validateView(trigger: Validation.Trigger = 'navigation'): {
|
|
575
791
|
/** Indicating if the view can proceed without error */
|
|
@@ -582,26 +798,35 @@ export class ValidationController implements BindingTracker {
|
|
|
582
798
|
|
|
583
799
|
const validations = new Map<BindingInstance, ValidationResponse>();
|
|
584
800
|
|
|
585
|
-
|
|
586
|
-
const invalid = this.getValidationForBinding(b)?.get();
|
|
801
|
+
let canTransition = true;
|
|
587
802
|
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
`Validation on binding: ${b.asString()} is preventing navigation. ${JSON.stringify(
|
|
591
|
-
invalid
|
|
592
|
-
)}`
|
|
593
|
-
);
|
|
803
|
+
this.getBindings().forEach((b) => {
|
|
804
|
+
const allValidations = this.getValidationForBinding(b)?.getAll();
|
|
594
805
|
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
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
|
+
);
|
|
813
|
+
|
|
814
|
+
canTransition = false;
|
|
815
|
+
}
|
|
816
|
+
|
|
817
|
+
if (!validations.has(b)) {
|
|
818
|
+
validations.set(b, v);
|
|
819
|
+
}
|
|
820
|
+
});
|
|
821
|
+
});
|
|
598
822
|
|
|
599
823
|
return {
|
|
600
|
-
canTransition
|
|
824
|
+
canTransition,
|
|
601
825
|
validations: validations.size ? validations : undefined,
|
|
602
826
|
};
|
|
603
827
|
}
|
|
604
828
|
|
|
829
|
+
/** Get the current tracked validation for the given binding */
|
|
605
830
|
public getValidationForBinding(
|
|
606
831
|
binding: BindingInstance
|
|
607
832
|
): ValidatedBinding | undefined {
|
|
@@ -613,11 +838,10 @@ export class ValidationController implements BindingTracker {
|
|
|
613
838
|
_getValidationForBinding: (binding) => {
|
|
614
839
|
return this.getValidationForBinding(
|
|
615
840
|
isBinding(binding) ? binding : parser(binding)
|
|
616
|
-
)
|
|
841
|
+
);
|
|
617
842
|
},
|
|
618
843
|
getAll: () => {
|
|
619
844
|
const bindings = this.getBindings();
|
|
620
|
-
|
|
621
845
|
if (bindings.size === 0) {
|
|
622
846
|
return undefined;
|
|
623
847
|
}
|
|
@@ -640,6 +864,9 @@ export class ValidationController implements BindingTracker {
|
|
|
640
864
|
get() {
|
|
641
865
|
throw new Error('Error Access be provided by the view plugin');
|
|
642
866
|
},
|
|
867
|
+
getValidationsForBinding() {
|
|
868
|
+
throw new Error('Error rollup should be provided by the view plugin');
|
|
869
|
+
},
|
|
643
870
|
getChildren() {
|
|
644
871
|
throw new Error('Error rollup should be provided by the view plugin');
|
|
645
872
|
},
|
|
@@ -651,7 +878,7 @@ export class ValidationController implements BindingTracker {
|
|
|
651
878
|
},
|
|
652
879
|
register: () => {
|
|
653
880
|
throw new Error(
|
|
654
|
-
'Section
|
|
881
|
+
'Section functionality should be provided by the view plugin'
|
|
655
882
|
);
|
|
656
883
|
},
|
|
657
884
|
type: (binding) =>
|