@player-ui/player 0.0.1-next.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 +1394 -0
- package/dist/index.d.ts +434 -0
- package/dist/index.esm.js +1326 -0
- package/package.json +26 -0
- package/src/data.ts +247 -0
- package/src/index.ts +18 -0
- package/src/player.ts +497 -0
- package/src/plugins/flow-exp-plugin.ts +65 -0
- package/src/types.ts +114 -0
- package/src/utils/desc.d.ts +2 -0
- package/src/validation/binding-tracker.ts +239 -0
- package/src/validation/controller.ts +661 -0
- package/src/validation/index.ts +2 -0
- package/src/view/asset-transform.ts +147 -0
- package/src/view/controller.ts +148 -0
- package/src/view/index.ts +4 -0
- package/src/view/store.ts +94 -0
- package/src/view/types.ts +31 -0
|
@@ -0,0 +1,661 @@
|
|
|
1
|
+
import type { BindingInstance, BindingFactory } from '@player-ui/binding';
|
|
2
|
+
import { isBinding } from '@player-ui/binding';
|
|
3
|
+
import type { DataModelWithParser, DataModelMiddleware } from '@player-ui/data';
|
|
4
|
+
import type { SchemaController } from '@player-ui/schema';
|
|
5
|
+
import type { Validation } from '@player-ui/types';
|
|
6
|
+
import type {
|
|
7
|
+
ErrorValidationResponse,
|
|
8
|
+
ValidationObject,
|
|
9
|
+
ValidatorContext,
|
|
10
|
+
ValidationProvider,
|
|
11
|
+
ValidationResponse,
|
|
12
|
+
WarningValidationResponse,
|
|
13
|
+
} from '@player-ui/validator';
|
|
14
|
+
import { ValidationMiddleware, ValidatorRegistry } from '@player-ui/validator';
|
|
15
|
+
import type { Logger } from '@player-ui/logger';
|
|
16
|
+
import { ProxyLogger } from '@player-ui/logger';
|
|
17
|
+
import type { Resolve, ViewInstance } from '@player-ui/view';
|
|
18
|
+
import { caresAboutDataChanges } from '@player-ui/view';
|
|
19
|
+
import { replaceParams } from '@player-ui/utils';
|
|
20
|
+
import { resolveDataRefs } from '@player-ui/string-resolver';
|
|
21
|
+
import type {
|
|
22
|
+
ExpressionEvaluatorOptions,
|
|
23
|
+
ExpressionType,
|
|
24
|
+
} from '@player-ui/expressions';
|
|
25
|
+
import { SyncHook, SyncWaterfallHook } from 'tapable';
|
|
26
|
+
import type { BindingTracker } from './binding-tracker';
|
|
27
|
+
import { ValidationBindingTrackerViewPlugin } from './binding-tracker';
|
|
28
|
+
|
|
29
|
+
type SimpleValidatorContext = Omit<ValidatorContext, 'validation'>;
|
|
30
|
+
|
|
31
|
+
interface BaseActiveValidation<T> {
|
|
32
|
+
/** The validation is being actively shown */
|
|
33
|
+
state: 'active';
|
|
34
|
+
|
|
35
|
+
/** The validation response */
|
|
36
|
+
response: T;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
type ActiveWarning = BaseActiveValidation<WarningValidationResponse> & {
|
|
40
|
+
/** Warnings track if they can be dismissed automatically (by navigating) */
|
|
41
|
+
dismissable: boolean;
|
|
42
|
+
};
|
|
43
|
+
type ActiveError = BaseActiveValidation<ErrorValidationResponse>;
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* warnings that keep track of their active state
|
|
47
|
+
*/
|
|
48
|
+
type StatefulWarning = {
|
|
49
|
+
/** A common key to differentiate between errors and warnings */
|
|
50
|
+
type: 'warning';
|
|
51
|
+
|
|
52
|
+
/** The underlying validation this tracks */
|
|
53
|
+
value: ValidationObject;
|
|
54
|
+
} & (
|
|
55
|
+
| {
|
|
56
|
+
/** warnings start with no state, but can active or dismissed */
|
|
57
|
+
state: 'none' | 'dismissed';
|
|
58
|
+
}
|
|
59
|
+
| ActiveWarning
|
|
60
|
+
);
|
|
61
|
+
|
|
62
|
+
/** Errors that keep track of their state */
|
|
63
|
+
type StatefulError = {
|
|
64
|
+
/** A common key to differentiate between errors and warnings */
|
|
65
|
+
type: 'error';
|
|
66
|
+
|
|
67
|
+
/** The underlying validation this tracks */
|
|
68
|
+
value: ValidationObject;
|
|
69
|
+
} & (
|
|
70
|
+
| {
|
|
71
|
+
/** Errors start with no state an can be activated */
|
|
72
|
+
state: 'none';
|
|
73
|
+
}
|
|
74
|
+
| ActiveError
|
|
75
|
+
);
|
|
76
|
+
|
|
77
|
+
export type StatefulValidationObject = StatefulWarning | StatefulError;
|
|
78
|
+
|
|
79
|
+
/** Helper for initializing a validation object that tracks state */
|
|
80
|
+
function createStatefulValidationObject(
|
|
81
|
+
obj: ValidationObject
|
|
82
|
+
): StatefulValidationObject {
|
|
83
|
+
return {
|
|
84
|
+
value: obj,
|
|
85
|
+
type: obj.severity,
|
|
86
|
+
state: 'none',
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
type ValidationRunner = (obj: ValidationObject) =>
|
|
91
|
+
| {
|
|
92
|
+
/** A validation message */
|
|
93
|
+
message: string;
|
|
94
|
+
}
|
|
95
|
+
| undefined;
|
|
96
|
+
|
|
97
|
+
/** A class that manages validating bindings across phases */
|
|
98
|
+
class ValidatedBinding {
|
|
99
|
+
private currentPhase?: Validation.Trigger;
|
|
100
|
+
private applicableValidations: Array<StatefulValidationObject> = [];
|
|
101
|
+
private validationsByState: Record<
|
|
102
|
+
Validation.Trigger,
|
|
103
|
+
Array<StatefulValidationObject>
|
|
104
|
+
> = {
|
|
105
|
+
load: [],
|
|
106
|
+
change: [],
|
|
107
|
+
navigation: [],
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
public weakBindings: Set<BindingInstance>;
|
|
111
|
+
private onDismiss?: () => void;
|
|
112
|
+
|
|
113
|
+
constructor(
|
|
114
|
+
possibleValidations: Array<ValidationObject>,
|
|
115
|
+
onDismiss?: () => void,
|
|
116
|
+
log?: Logger,
|
|
117
|
+
weakBindings?: Set<BindingInstance>
|
|
118
|
+
) {
|
|
119
|
+
this.onDismiss = onDismiss;
|
|
120
|
+
possibleValidations.forEach((vObj) => {
|
|
121
|
+
const { trigger } = vObj;
|
|
122
|
+
|
|
123
|
+
if (this.validationsByState[trigger]) {
|
|
124
|
+
this.validationsByState[trigger].push(
|
|
125
|
+
createStatefulValidationObject(vObj)
|
|
126
|
+
);
|
|
127
|
+
} else {
|
|
128
|
+
log?.warn(`Unknown validation trigger: ${trigger}`);
|
|
129
|
+
}
|
|
130
|
+
});
|
|
131
|
+
this.weakBindings = weakBindings ?? new Set();
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
public get(): ValidationResponse | undefined {
|
|
135
|
+
const firstError = this.applicableValidations.find((statefulObj) => {
|
|
136
|
+
const blocking =
|
|
137
|
+
this.currentPhase === 'navigation' ? statefulObj.value.blocking : true;
|
|
138
|
+
return statefulObj.state === 'active' && blocking !== false;
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
if (firstError?.state === 'active') {
|
|
142
|
+
return firstError.response;
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
private runApplicableValidations(
|
|
147
|
+
runner: ValidationRunner,
|
|
148
|
+
canDismiss: boolean
|
|
149
|
+
) {
|
|
150
|
+
// If the currentState is not load, skip those
|
|
151
|
+
this.applicableValidations = this.applicableValidations.map((obj) => {
|
|
152
|
+
if (obj.state === 'dismissed') {
|
|
153
|
+
// Don't rerun any dismissed warnings
|
|
154
|
+
return obj;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
const blocking =
|
|
158
|
+
obj.value.blocking ??
|
|
159
|
+
((obj.value.severity === 'warning' && 'once') ||
|
|
160
|
+
(obj.value.severity === 'error' && true));
|
|
161
|
+
|
|
162
|
+
const dismissable = canDismiss && blocking === 'once';
|
|
163
|
+
|
|
164
|
+
if (
|
|
165
|
+
this.currentPhase === 'navigation' &&
|
|
166
|
+
obj.state === 'active' &&
|
|
167
|
+
dismissable
|
|
168
|
+
) {
|
|
169
|
+
if (obj.value.severity === 'warning') {
|
|
170
|
+
const warn = obj as ActiveWarning;
|
|
171
|
+
if (warn.dismissable && warn.response.dismiss) {
|
|
172
|
+
warn.response.dismiss();
|
|
173
|
+
} else {
|
|
174
|
+
warn.dismissable = true;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
return obj;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
if (obj.value.severity === 'error') {
|
|
181
|
+
const err = obj as StatefulError;
|
|
182
|
+
err.state = 'none';
|
|
183
|
+
return obj;
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
const response = runner(obj.value);
|
|
188
|
+
|
|
189
|
+
const newState = {
|
|
190
|
+
type: obj.type,
|
|
191
|
+
value: obj.value,
|
|
192
|
+
state: response ? 'active' : 'none',
|
|
193
|
+
dismissable:
|
|
194
|
+
obj.value.severity === 'warning' &&
|
|
195
|
+
this.currentPhase === 'navigation',
|
|
196
|
+
response: response
|
|
197
|
+
? {
|
|
198
|
+
...obj.value,
|
|
199
|
+
message: response.message ?? 'Something is broken',
|
|
200
|
+
severity: obj.value.severity,
|
|
201
|
+
displayTarget: obj.value.displayTarget ?? 'field',
|
|
202
|
+
}
|
|
203
|
+
: undefined,
|
|
204
|
+
} as StatefulValidationObject;
|
|
205
|
+
|
|
206
|
+
if (newState.state === 'active' && obj.value.severity === 'warning') {
|
|
207
|
+
(newState.response as WarningValidationResponse).dismiss = () => {
|
|
208
|
+
(newState as StatefulWarning).state = 'dismissed';
|
|
209
|
+
this.onDismiss?.();
|
|
210
|
+
};
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
return newState;
|
|
214
|
+
});
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
public update(
|
|
218
|
+
phase: Validation.Trigger,
|
|
219
|
+
canDismiss: boolean,
|
|
220
|
+
runner: ValidationRunner
|
|
221
|
+
) {
|
|
222
|
+
if (phase === 'load' && this.currentPhase !== undefined) {
|
|
223
|
+
// Tried to run the 'load' phase twice. Aborting
|
|
224
|
+
return;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
if (this.currentPhase === 'navigation' || phase === this.currentPhase) {
|
|
228
|
+
// Already added all the types. No need to continue adding new validations
|
|
229
|
+
this.runApplicableValidations(runner, canDismiss);
|
|
230
|
+
return;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
if (phase === 'load') {
|
|
234
|
+
this.currentPhase = 'load';
|
|
235
|
+
this.applicableValidations = [...this.validationsByState.load];
|
|
236
|
+
} else if (phase === 'change' && this.currentPhase === 'load') {
|
|
237
|
+
this.currentPhase = 'change';
|
|
238
|
+
// The transition to the 'change' type can only come from a 'load' type
|
|
239
|
+
this.applicableValidations = [
|
|
240
|
+
...this.applicableValidations,
|
|
241
|
+
...this.validationsByState.change,
|
|
242
|
+
];
|
|
243
|
+
} else if (
|
|
244
|
+
phase === 'navigation' &&
|
|
245
|
+
(this.currentPhase === 'load' || this.currentPhase === 'change')
|
|
246
|
+
) {
|
|
247
|
+
// Can transition to a nav state from a change or load
|
|
248
|
+
this.applicableValidations = [
|
|
249
|
+
...this.applicableValidations,
|
|
250
|
+
...(this.currentPhase === 'load' ? this.validationsByState.change : []),
|
|
251
|
+
...this.validationsByState.navigation,
|
|
252
|
+
];
|
|
253
|
+
this.currentPhase = 'navigation';
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
this.runApplicableValidations(runner, canDismiss);
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
/**
|
|
261
|
+
* A controller for orchestrating validation within a running player
|
|
262
|
+
*
|
|
263
|
+
* The current validation flow is as follows:
|
|
264
|
+
*
|
|
265
|
+
* - When a binding is first seen, gather all of the possible validations for it from the providers
|
|
266
|
+
* - Schema and Crossfield (view) are both providers of possible validations
|
|
267
|
+
* - Run all of the applicable validations for that binding for the `load` trigger
|
|
268
|
+
*
|
|
269
|
+
* - When a change occurs, set the phase of the binding to `change`.
|
|
270
|
+
* - Run all of the `change` triggered validations for that binding.
|
|
271
|
+
*
|
|
272
|
+
* - When a navigation event occurs, set the phase of the binding to `navigate`.
|
|
273
|
+
* - Run all `change` and `navigate` validations for each tracked binding.
|
|
274
|
+
* - For any warnings, also keep a state of `shown` or `dismissed`.
|
|
275
|
+
* - Set all non-dismissed warnings to `shown`.
|
|
276
|
+
* - Set all `shown` warnings to `dismissed`.
|
|
277
|
+
* - Allow navigation forward if there are no non-dismissed warnings and no valid errors.
|
|
278
|
+
*/
|
|
279
|
+
export class ValidationController implements BindingTracker {
|
|
280
|
+
public readonly hooks = {
|
|
281
|
+
/** A hook called to tap into the validator registry for adding more validators */
|
|
282
|
+
createValidatorRegistry: new SyncHook<ValidatorRegistry>(['registry']),
|
|
283
|
+
|
|
284
|
+
/** A callback/event when a new validation is added to the view */
|
|
285
|
+
onAddValidation: new SyncWaterfallHook<ValidationResponse, BindingInstance>(
|
|
286
|
+
['validation', 'binding']
|
|
287
|
+
),
|
|
288
|
+
|
|
289
|
+
/** The inverse of onAddValidation, this is called when a validation is removed from the list */
|
|
290
|
+
onRemoveValidation: new SyncWaterfallHook<
|
|
291
|
+
ValidationResponse,
|
|
292
|
+
BindingInstance
|
|
293
|
+
>(['validation', 'binding']),
|
|
294
|
+
};
|
|
295
|
+
|
|
296
|
+
private tracker: BindingTracker | undefined;
|
|
297
|
+
private validations = new Map<BindingInstance, ValidatedBinding>();
|
|
298
|
+
private validatorRegistry?: ValidatorRegistry;
|
|
299
|
+
private schema: SchemaController;
|
|
300
|
+
private providers: Array<ValidationProvider>;
|
|
301
|
+
private options?: SimpleValidatorContext;
|
|
302
|
+
private weakBindingTracker = new Set<BindingInstance>();
|
|
303
|
+
private lastActiveBindings = new Set<BindingInstance>();
|
|
304
|
+
|
|
305
|
+
constructor(schema: SchemaController, options?: SimpleValidatorContext) {
|
|
306
|
+
this.schema = schema;
|
|
307
|
+
this.options = options;
|
|
308
|
+
this.providers = [schema];
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
setOptions(options: SimpleValidatorContext) {
|
|
312
|
+
this.options = options;
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
/** Return the middleware for the data-model to stop propagation of invalid data */
|
|
316
|
+
public getDataMiddleware(): Array<DataModelMiddleware> {
|
|
317
|
+
return [
|
|
318
|
+
new ValidationMiddleware(
|
|
319
|
+
(binding) => {
|
|
320
|
+
if (!this.options) {
|
|
321
|
+
return;
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
this.updateValidationsForBinding(binding, 'change', this.options);
|
|
325
|
+
|
|
326
|
+
const strongValidation = this.getValidationForBinding(binding);
|
|
327
|
+
|
|
328
|
+
// return validation issues directly on bindings first
|
|
329
|
+
if (strongValidation?.get()) return strongValidation.get();
|
|
330
|
+
|
|
331
|
+
// if none, check to see any validations this binding may be a weak ref of and return
|
|
332
|
+
const newInvalidBindings: Set<BindingInstance> = new Set();
|
|
333
|
+
for (const [, weakValidation] of Array.from(this.validations)) {
|
|
334
|
+
if (
|
|
335
|
+
caresAboutDataChanges(
|
|
336
|
+
new Set([binding]),
|
|
337
|
+
weakValidation.weakBindings
|
|
338
|
+
) &&
|
|
339
|
+
weakValidation?.get()
|
|
340
|
+
) {
|
|
341
|
+
weakValidation?.weakBindings.forEach(
|
|
342
|
+
newInvalidBindings.add,
|
|
343
|
+
newInvalidBindings
|
|
344
|
+
);
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
if (newInvalidBindings.size > 0) {
|
|
349
|
+
return newInvalidBindings;
|
|
350
|
+
}
|
|
351
|
+
},
|
|
352
|
+
{ logger: new ProxyLogger(() => this.options?.logger) }
|
|
353
|
+
),
|
|
354
|
+
];
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
public onView(view: ViewInstance): void {
|
|
358
|
+
this.validations.clear();
|
|
359
|
+
|
|
360
|
+
if (!this.options) {
|
|
361
|
+
return;
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
const bindingTrackerPlugin = new ValidationBindingTrackerViewPlugin({
|
|
365
|
+
...this.options,
|
|
366
|
+
callbacks: {
|
|
367
|
+
onAdd: (binding) => {
|
|
368
|
+
if (!this.options) {
|
|
369
|
+
return;
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
// Set the default value for the binding if we need to
|
|
373
|
+
const originalValue = this.options.model.get(binding);
|
|
374
|
+
const withoutDefault = this.options.model.get(binding, {
|
|
375
|
+
ignoreDefaultValue: true,
|
|
376
|
+
});
|
|
377
|
+
|
|
378
|
+
if (originalValue !== withoutDefault) {
|
|
379
|
+
this.options.model.set([[binding, originalValue]]);
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
this.updateValidationsForBinding(
|
|
383
|
+
binding,
|
|
384
|
+
'load',
|
|
385
|
+
this.options,
|
|
386
|
+
() => {
|
|
387
|
+
view.update(new Set([binding]));
|
|
388
|
+
}
|
|
389
|
+
);
|
|
390
|
+
},
|
|
391
|
+
},
|
|
392
|
+
});
|
|
393
|
+
|
|
394
|
+
this.tracker = bindingTrackerPlugin;
|
|
395
|
+
this.providers = [this.schema, view];
|
|
396
|
+
|
|
397
|
+
bindingTrackerPlugin.apply(view);
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
private updateValidationsForBinding(
|
|
401
|
+
binding: BindingInstance,
|
|
402
|
+
trigger: Validation.Trigger,
|
|
403
|
+
context: SimpleValidatorContext,
|
|
404
|
+
onDismiss?: () => void
|
|
405
|
+
): void {
|
|
406
|
+
if (trigger === 'load') {
|
|
407
|
+
// Get all of the validations from each provider
|
|
408
|
+
const possibleValidations = this.providers.reduce<
|
|
409
|
+
Array<ValidationObject>
|
|
410
|
+
>(
|
|
411
|
+
(vals, provider) => [
|
|
412
|
+
...vals,
|
|
413
|
+
...(provider.getValidationsForBinding?.(binding) ?? []),
|
|
414
|
+
],
|
|
415
|
+
[]
|
|
416
|
+
);
|
|
417
|
+
|
|
418
|
+
if (possibleValidations.length === 0) {
|
|
419
|
+
return;
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
this.validations.set(
|
|
423
|
+
binding,
|
|
424
|
+
new ValidatedBinding(
|
|
425
|
+
possibleValidations,
|
|
426
|
+
onDismiss,
|
|
427
|
+
this.options?.logger
|
|
428
|
+
)
|
|
429
|
+
);
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
const trackedValidations = this.validations.get(binding);
|
|
433
|
+
trackedValidations?.update(trigger, true, (validationObj) => {
|
|
434
|
+
const response = this.validationRunner(validationObj, context, binding);
|
|
435
|
+
|
|
436
|
+
if (this.weakBindingTracker.size > 0) {
|
|
437
|
+
const t = this.validations.get(binding) as ValidatedBinding;
|
|
438
|
+
this.weakBindingTracker.forEach((b) => t.weakBindings.add(b));
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
return response ? { message: response.message } : undefined;
|
|
442
|
+
});
|
|
443
|
+
|
|
444
|
+
// Also run any validations that binding or sub-binding is a weak binding of
|
|
445
|
+
if (trigger !== 'load') {
|
|
446
|
+
this.validations.forEach((validation, vBinding) => {
|
|
447
|
+
if (
|
|
448
|
+
vBinding !== binding &&
|
|
449
|
+
caresAboutDataChanges(new Set([binding]), validation.weakBindings)
|
|
450
|
+
) {
|
|
451
|
+
validation.update(trigger, true, (validationObj) => {
|
|
452
|
+
const response = this.validationRunner(
|
|
453
|
+
validationObj,
|
|
454
|
+
context,
|
|
455
|
+
binding
|
|
456
|
+
);
|
|
457
|
+
return response ? { message: response.message } : undefined;
|
|
458
|
+
});
|
|
459
|
+
}
|
|
460
|
+
});
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
private validationRunner(
|
|
465
|
+
validationObj: ValidationObject,
|
|
466
|
+
context: SimpleValidatorContext,
|
|
467
|
+
binding: BindingInstance
|
|
468
|
+
) {
|
|
469
|
+
const handler = this.getValidator(validationObj.type);
|
|
470
|
+
const weakBindings = new Set<BindingInstance>();
|
|
471
|
+
|
|
472
|
+
// For any data-gets in the validation runner, default to using the _invalid_ value (since that's what we're testing against)
|
|
473
|
+
const model: DataModelWithParser = {
|
|
474
|
+
get(b, options = { includeInvalid: true }) {
|
|
475
|
+
weakBindings.add(isBinding(b) ? binding : context.parseBinding(b));
|
|
476
|
+
return context.model.get(b, options);
|
|
477
|
+
},
|
|
478
|
+
set: context.model.set,
|
|
479
|
+
};
|
|
480
|
+
|
|
481
|
+
const result = handler?.(
|
|
482
|
+
{
|
|
483
|
+
...context,
|
|
484
|
+
evaluate: (
|
|
485
|
+
exp: ExpressionType,
|
|
486
|
+
options: ExpressionEvaluatorOptions = { model }
|
|
487
|
+
) => context.evaluate(exp, options),
|
|
488
|
+
model,
|
|
489
|
+
validation: validationObj,
|
|
490
|
+
},
|
|
491
|
+
context.model.get(binding, {
|
|
492
|
+
includeInvalid: true,
|
|
493
|
+
formatted: validationObj.dataTarget === 'formatted',
|
|
494
|
+
}),
|
|
495
|
+
validationObj
|
|
496
|
+
);
|
|
497
|
+
|
|
498
|
+
this.weakBindingTracker = weakBindings;
|
|
499
|
+
|
|
500
|
+
if (result) {
|
|
501
|
+
let { message } = result;
|
|
502
|
+
const { parameters } = result;
|
|
503
|
+
|
|
504
|
+
if (validationObj.message) {
|
|
505
|
+
message = resolveDataRefs(validationObj.message, {
|
|
506
|
+
model,
|
|
507
|
+
evaluate: context.evaluate,
|
|
508
|
+
});
|
|
509
|
+
|
|
510
|
+
if (parameters) {
|
|
511
|
+
message = replaceParams(message, parameters);
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
return {
|
|
516
|
+
message,
|
|
517
|
+
};
|
|
518
|
+
}
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
private updateValidationsForView(trigger: Validation.Trigger): void {
|
|
522
|
+
const { activeBindings } = this;
|
|
523
|
+
|
|
524
|
+
const canDismiss =
|
|
525
|
+
trigger !== 'navigation' ||
|
|
526
|
+
this.setCompare(this.lastActiveBindings, activeBindings);
|
|
527
|
+
|
|
528
|
+
this.getBindings().forEach((binding) => {
|
|
529
|
+
this.validations.get(binding)?.update(trigger, canDismiss, (obj) => {
|
|
530
|
+
if (!this.options) {
|
|
531
|
+
return;
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
return this.validationRunner(obj, this.options, binding);
|
|
535
|
+
});
|
|
536
|
+
});
|
|
537
|
+
|
|
538
|
+
if (trigger === 'navigation') {
|
|
539
|
+
this.lastActiveBindings = activeBindings;
|
|
540
|
+
}
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
private setCompare<T>(set1: Set<T>, set2: Set<T>): boolean {
|
|
544
|
+
if (set1.size !== set2.size) return false;
|
|
545
|
+
for (const entry of set1) if (!set2.has(entry)) return false;
|
|
546
|
+
return true;
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
private get activeBindings(): Set<BindingInstance> {
|
|
550
|
+
return new Set(
|
|
551
|
+
Array.from(this.getBindings()).filter(
|
|
552
|
+
(b) => this.validations.get(b)?.get() !== undefined
|
|
553
|
+
)
|
|
554
|
+
);
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
private getValidator(type: string) {
|
|
558
|
+
if (this.validatorRegistry) {
|
|
559
|
+
return this.validatorRegistry.get(type);
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
const registry = new ValidatorRegistry();
|
|
563
|
+
this.hooks.createValidatorRegistry.call(registry);
|
|
564
|
+
this.validatorRegistry = registry;
|
|
565
|
+
|
|
566
|
+
return registry.get(type);
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
getBindings(): Set<BindingInstance> {
|
|
570
|
+
return this.tracker?.getBindings() ?? new Set();
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
/** Executes all known validations for the tracked bindings using the given model */
|
|
574
|
+
validateView(trigger: Validation.Trigger = 'navigation'): {
|
|
575
|
+
/** Indicating if the view can proceed without error */
|
|
576
|
+
canTransition: boolean;
|
|
577
|
+
|
|
578
|
+
/** the validations that are preventing the view from continuing */
|
|
579
|
+
validations?: Map<BindingInstance, ValidationResponse>;
|
|
580
|
+
} {
|
|
581
|
+
this.updateValidationsForView(trigger);
|
|
582
|
+
|
|
583
|
+
const validations = new Map<BindingInstance, ValidationResponse>();
|
|
584
|
+
|
|
585
|
+
for (const b of this.getBindings()) {
|
|
586
|
+
const invalid = this.getValidationForBinding(b)?.get();
|
|
587
|
+
|
|
588
|
+
if (invalid) {
|
|
589
|
+
this.options?.logger.debug(
|
|
590
|
+
`Validation on binding: ${b.asString()} is preventing navigation. ${JSON.stringify(
|
|
591
|
+
invalid
|
|
592
|
+
)}`
|
|
593
|
+
);
|
|
594
|
+
|
|
595
|
+
validations.set(b, invalid);
|
|
596
|
+
}
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
return {
|
|
600
|
+
canTransition: validations.size === 0,
|
|
601
|
+
validations: validations.size ? validations : undefined,
|
|
602
|
+
};
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
public getValidationForBinding(
|
|
606
|
+
binding: BindingInstance
|
|
607
|
+
): ValidatedBinding | undefined {
|
|
608
|
+
return this.validations.get(binding);
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
forView(parser: BindingFactory): Resolve.Validation {
|
|
612
|
+
return {
|
|
613
|
+
_getValidationForBinding: (binding) => {
|
|
614
|
+
return this.getValidationForBinding(
|
|
615
|
+
isBinding(binding) ? binding : parser(binding)
|
|
616
|
+
)?.get();
|
|
617
|
+
},
|
|
618
|
+
getAll: () => {
|
|
619
|
+
const bindings = this.getBindings();
|
|
620
|
+
|
|
621
|
+
if (bindings.size === 0) {
|
|
622
|
+
return undefined;
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
const validationMapping = new Map<
|
|
626
|
+
BindingInstance,
|
|
627
|
+
ValidationResponse
|
|
628
|
+
>();
|
|
629
|
+
|
|
630
|
+
bindings.forEach((b) => {
|
|
631
|
+
const validation = this.getValidationForBinding(b)?.get();
|
|
632
|
+
|
|
633
|
+
if (validation) {
|
|
634
|
+
validationMapping.set(b, validation);
|
|
635
|
+
}
|
|
636
|
+
});
|
|
637
|
+
|
|
638
|
+
return validationMapping.size === 0 ? undefined : validationMapping;
|
|
639
|
+
},
|
|
640
|
+
get() {
|
|
641
|
+
throw new Error('Error Access be provided by the view plugin');
|
|
642
|
+
},
|
|
643
|
+
getChildren() {
|
|
644
|
+
throw new Error('Error rollup should be provided by the view plugin');
|
|
645
|
+
},
|
|
646
|
+
getValidationsForSection() {
|
|
647
|
+
throw new Error('Error rollup should be provided by the view plugin');
|
|
648
|
+
},
|
|
649
|
+
track: () => {
|
|
650
|
+
throw new Error('Tracking should be provided by the view plugin');
|
|
651
|
+
},
|
|
652
|
+
register: () => {
|
|
653
|
+
throw new Error(
|
|
654
|
+
'Section funcationality hould be provided by the view plugin'
|
|
655
|
+
);
|
|
656
|
+
},
|
|
657
|
+
type: (binding) =>
|
|
658
|
+
this.schema.getType(isBinding(binding) ? binding : parser(binding)),
|
|
659
|
+
};
|
|
660
|
+
}
|
|
661
|
+
}
|