@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.
Files changed (47) hide show
  1. package/dist/index.cjs.js +899 -336
  2. package/dist/index.d.ts +275 -93
  3. package/dist/index.esm.js +890 -334
  4. package/dist/player.dev.js +11429 -0
  5. package/dist/player.prod.js +2 -0
  6. package/package.json +16 -5
  7. package/src/binding/binding.ts +8 -0
  8. package/src/binding/index.ts +14 -4
  9. package/src/binding/resolver.ts +2 -4
  10. package/src/binding-grammar/custom/index.ts +17 -9
  11. package/src/controllers/constants/index.ts +9 -5
  12. package/src/controllers/{data.ts → data/controller.ts} +62 -61
  13. package/src/controllers/data/index.ts +1 -0
  14. package/src/controllers/data/utils.ts +42 -0
  15. package/src/controllers/flow/controller.ts +16 -12
  16. package/src/controllers/flow/flow.ts +6 -1
  17. package/src/controllers/index.ts +1 -1
  18. package/src/controllers/validation/binding-tracker.ts +42 -19
  19. package/src/controllers/validation/controller.ts +375 -148
  20. package/src/controllers/view/asset-transform.ts +4 -1
  21. package/src/controllers/view/controller.ts +20 -3
  22. package/src/data/dependency-tracker.ts +14 -0
  23. package/src/data/local-model.ts +25 -1
  24. package/src/data/model.ts +60 -8
  25. package/src/data/noop-model.ts +2 -0
  26. package/src/expressions/evaluator-functions.ts +24 -2
  27. package/src/expressions/evaluator.ts +38 -34
  28. package/src/expressions/index.ts +1 -0
  29. package/src/expressions/parser.ts +116 -44
  30. package/src/expressions/types.ts +50 -17
  31. package/src/expressions/utils.ts +143 -1
  32. package/src/player.ts +60 -46
  33. package/src/plugins/default-exp-plugin.ts +57 -0
  34. package/src/plugins/flow-exp-plugin.ts +2 -2
  35. package/src/schema/schema.ts +28 -9
  36. package/src/string-resolver/index.ts +26 -9
  37. package/src/types.ts +6 -3
  38. package/src/validator/binding-map-splice.ts +59 -0
  39. package/src/validator/index.ts +1 -0
  40. package/src/validator/types.ts +11 -3
  41. package/src/validator/validation-middleware.ts +58 -6
  42. package/src/view/parser/index.ts +51 -3
  43. package/src/view/plugins/applicability.ts +1 -1
  44. package/src/view/plugins/string-resolver.ts +35 -9
  45. package/src/view/plugins/template-plugin.ts +1 -6
  46. package/src/view/resolver/index.ts +119 -54
  47. 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
- type SimpleValidatorContext = Omit<ValidatorContext, 'validation'>;
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: ValidationObject;
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: ValidationObject;
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: ValidationObject
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: ValidationObject) =>
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
- private currentPhase?: Validation.Trigger;
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<ValidationObject>,
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
- this.validationsByState[trigger].push(
126
- createStatefulValidationObject(vObj)
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 firstError = this.applicableValidations.find((statefulObj) => {
137
- const blocking =
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 (firstError?.state === 'active') {
143
- return firstError.response;
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((obj) => {
153
- if (obj.state === 'dismissed') {
154
- // Don't rerun any dismissed warnings
155
- return obj;
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
- const blocking =
159
- obj.value.blocking ??
160
- ((obj.value.severity === 'warning' && 'once') ||
161
- (obj.value.severity === 'error' && true));
162
-
163
- const dismissable = canDismiss && blocking === 'once';
164
-
165
- if (
166
- this.currentPhase === 'navigation' &&
167
- obj.state === 'active' &&
168
- dismissable
169
- ) {
170
- if (obj.value.severity === 'warning') {
171
- const warn = obj as ActiveWarning;
172
- if (warn.dismissable && warn.response.dismiss) {
173
- warn.response.dismiss();
174
- } else {
175
- warn.dismissable = true;
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
- return obj;
258
+ warn.dismissable = true;
259
+ }
260
+
261
+ return warn as StatefulValidationObject;
262
+ }
179
263
  }
180
264
 
181
- if (obj.value.severity === 'error') {
182
- const err = obj as StatefulError;
183
- err.state = 'none';
184
- return obj;
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
- const response = runner(obj.value);
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
- ...this.applicableValidations,
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
- private providers: Array<ValidationProvider>;
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.providers = [schema];
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()) return 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<BindingInstance> = new Set();
333
- for (const [, weakValidation] of Array.from(this.validations)) {
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
- newInvalidBindings.add,
343
- newInvalidBindings
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
- public onView(view: ViewInstance): void {
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 (!this.options) {
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
- this.options.model.set([[binding, originalValue]]);
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.providers = [this.schema, view];
586
+ this.viewValidationProvider = view;
396
587
 
397
588
  bindingTrackerPlugin.apply(view);
398
589
  }
399
590
 
400
- private updateValidationsForBinding(
591
+ updateValidationsForBinding(
401
592
  binding: BindingInstance,
402
593
  trigger: Validation.Trigger,
403
- context: SimpleValidatorContext,
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.providers.reduce<
409
- Array<ValidationObject>
605
+ const possibleValidations = this.getValidationProviders().reduce<
606
+ Array<ValidationObjectWithSource>
410
607
  >(
411
608
  (vals, provider) => [
412
609
  ...vals,
413
- ...(provider.getValidationsForBinding?.(binding) ?? []),
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, context, binding);
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
- context,
455
- binding
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
- private validationRunner(
465
- validationObj: ValidationObject,
466
- context: SimpleValidatorContext,
467
- binding: BindingInstance
666
+ validationRunner(
667
+ validationObj: ValidationObjectWithHandler,
668
+ binding: BindingInstance,
669
+ context: SimpleValidatorContext | undefined = this.options
468
670
  ) {
469
- const handler = this.getValidator(validationObj.type);
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 = { includeInvalid: true }) {
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 { 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
- }
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
- return this.validationRunner(obj, this.options, binding);
744
+ return this.validationRunner(obj, binding, this.options);
745
+ });
535
746
  });
536
- });
747
+ };
537
748
 
538
- if (trigger === 'navigation') {
539
- this.lastActiveBindings = activeBindings;
540
- }
541
- }
749
+ // Should dismiss for non-navigation triggers.
750
+ updateValidations(!isNavigationTrigger);
542
751
 
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;
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
- for (const b of this.getBindings()) {
586
- const invalid = this.getValidationForBinding(b)?.get();
801
+ let canTransition = true;
587
802
 
588
- if (invalid) {
589
- this.options?.logger.debug(
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
- validations.set(b, invalid);
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: validations.size === 0,
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
- )?.get();
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 funcationality hould be provided by the view plugin'
881
+ 'Section functionality should be provided by the view plugin'
655
882
  );
656
883
  },
657
884
  type: (binding) =>