diff --git a/src/collections/observableMap/IMapChangedEventHandler.ts b/src/collections/observableMap/IMapChangedEventHandler.ts index 8d4a113..725263d 100644 --- a/src/collections/observableMap/IMapChangedEventHandler.ts +++ b/src/collections/observableMap/IMapChangedEventHandler.ts @@ -1,5 +1,5 @@ -import type { IEventHandler } from "../../events"; -import type { IMapChange } from "./IMapChange"; +import type { IEventHandler } from '../../events'; +import type { IMapChange } from './IMapChange'; /** * A specialized interface for handling map changed events. diff --git a/src/collections/observableMap/IObservableMap.ts b/src/collections/observableMap/IObservableMap.ts index d6bce99..903d4e1 100644 --- a/src/collections/observableMap/IObservableMap.ts +++ b/src/collections/observableMap/IObservableMap.ts @@ -1,4 +1,4 @@ -import type { IReadOnlyObservableMap } from "./IReadOnlyObservableMap"; +import type { IReadOnlyObservableMap } from './IReadOnlyObservableMap'; /** * Represents an observable map based on the [Map](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Map) interface. diff --git a/src/collections/observableMap/IReadOnlyObservableMap.ts b/src/collections/observableMap/IReadOnlyObservableMap.ts index 204efd7..c74fed2 100644 --- a/src/collections/observableMap/IReadOnlyObservableMap.ts +++ b/src/collections/observableMap/IReadOnlyObservableMap.ts @@ -1,5 +1,5 @@ -import type { INotifyPropertiesChanged } from "../../viewModels"; -import type { INotifyMapChanged } from "./INotifyMapChanged"; +import type { INotifyPropertiesChanged } from '../../viewModels'; +import type { INotifyMapChanged } from './INotifyMapChanged'; /** * Represents a read-only observable map based on the [Map](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Map) interface. diff --git a/src/collections/observableMap/ObservableMap.ts b/src/collections/observableMap/ObservableMap.ts index 71217b5..f56545c 100644 --- a/src/collections/observableMap/ObservableMap.ts +++ b/src/collections/observableMap/ObservableMap.ts @@ -1,5 +1,5 @@ -import type { IObservableMap } from "./IObservableMap"; -import { ReadOnlyObservableMap } from "./ReadOnlyObservableMap"; +import type { IObservableMap } from './IObservableMap'; +import { ReadOnlyObservableMap } from './ReadOnlyObservableMap'; export class ObservableMap extends ReadOnlyObservableMap implements IObservableMap { /** diff --git a/src/collections/observableMap/ReadOnlyObservableMap.ts b/src/collections/observableMap/ReadOnlyObservableMap.ts index 1986b8b..34e1bd9 100644 --- a/src/collections/observableMap/ReadOnlyObservableMap.ts +++ b/src/collections/observableMap/ReadOnlyObservableMap.ts @@ -1,8 +1,8 @@ -import type { IMapChange } from "./IMapChange"; -import type { IMapChangedEvent } from "./IMapChangedEvent"; -import type { IReadOnlyObservableMap } from "./IReadOnlyObservableMap"; -import { EventDispatcher } from "../../events"; -import { ViewModel } from "../../viewModels"; +import type { IMapChange } from './IMapChange'; +import type { IMapChangedEvent } from './IMapChangedEvent'; +import type { IReadOnlyObservableMap } from './IReadOnlyObservableMap'; +import { EventDispatcher } from '../../events'; +import { ViewModel } from '../../viewModels'; /** * Represents a read-only observable map based on the [Map](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Map) interface. diff --git a/src/collections/observableMap/index.ts b/src/collections/observableMap/index.ts index 495a56c..6e0ec4d 100644 --- a/src/collections/observableMap/index.ts +++ b/src/collections/observableMap/index.ts @@ -6,5 +6,5 @@ export type { IMapChangedEventHandler } from './IMapChangedEventHandler'; export type { IReadOnlyObservableMap } from './IReadOnlyObservableMap'; export type { IObservableMap } from './IObservableMap'; -export type { ReadOnlyObservableMap } from './ReadOnlyObservableMap'; -export type { ObservableMap } from './ObservableMap'; \ No newline at end of file +export { ReadOnlyObservableMap } from './ReadOnlyObservableMap'; +export { ObservableMap } from './ObservableMap'; \ No newline at end of file diff --git a/src/validation/IReadOnlyValidatable.ts b/src/validation/IReadOnlyValidatable.ts new file mode 100644 index 0000000..283f542 --- /dev/null +++ b/src/validation/IReadOnlyValidatable.ts @@ -0,0 +1,7 @@ +export interface IReadOnlyValidatable { + readonly isValid: boolean; + + readonly isInvalid: boolean; + + readonly error: TValidationError | null; +} \ No newline at end of file diff --git a/src/validation/IValidatable.ts b/src/validation/IValidatable.ts new file mode 100644 index 0000000..483bef8 --- /dev/null +++ b/src/validation/IValidatable.ts @@ -0,0 +1,5 @@ +import type { IReadOnlyValidatable } from './IReadOnlyValidatable'; + +export interface IValidatable extends IReadOnlyValidatable { + error: TValidationError | null; +} \ No newline at end of file diff --git a/src/validation/IValidationTrigger.ts b/src/validation/IValidationTrigger.ts new file mode 100644 index 0000000..7863724 --- /dev/null +++ b/src/validation/IValidationTrigger.ts @@ -0,0 +1,56 @@ +import type { INotifyPropertiesChanged, IPropertiesChangedEventHandler } from '../viewModels'; +import type { INotifyCollectionChanged, INotifyCollectionReordered, INotifySetChanged, INotifyMapChanged, ICollectionChangedEventHandler, ICollectionReorderedEventHandler, ISetChangedEventHandler, IMapChangedEventHandler } from '../collections'; + +export type IValidationTrigger = INotifyPropertiesChanged | INotifyCollectionChanged | INotifyCollectionReordered | INotifySetChanged | INotifyMapChanged; + +export interface IValidationTriggerEventHandlers { + readonly propertiesChanged?: IPropertiesChangedEventHandler; + + readonly collectionChanged?: ICollectionChangedEventHandler, TItem>; + readonly collectionReordered?: ICollectionReorderedEventHandler, TItem>; + + readonly setChanged?: ISetChangedEventHandler, TItem>; + readonly mapChanged?: IMapChangedEventHandler, TKey, TItem>; +} + +export function subscribeToValidationTrigger(validationTrigger: IValidationTrigger, eventHandlers: IValidationTriggerEventHandlers): void { + let isSpecialized = false; + + if (!!eventHandlers.collectionChanged && 'collectionChanged' in validationTrigger) { + isSpecialized = true; + validationTrigger.collectionChanged.subscribe(eventHandlers.collectionChanged); + } + if (!!eventHandlers.collectionReordered && 'collectionReordered' in validationTrigger) { + isSpecialized = true; + validationTrigger.collectionReordered.subscribe(eventHandlers.collectionReordered); + } + + if (!!eventHandlers.setChanged && 'setChanged' in validationTrigger) { + isSpecialized = true; + validationTrigger.setChanged.subscribe(eventHandlers.setChanged); + } + + if (!!eventHandlers.mapChanged && 'mapChanged' in validationTrigger) { + isSpecialized = true; + validationTrigger.mapChanged.subscribe(eventHandlers.mapChanged); + } + + if (!isSpecialized && !!eventHandlers.propertiesChanged && 'propertiesChanged' in validationTrigger) + validationTrigger.propertiesChanged.subscribe(eventHandlers.propertiesChanged) +} + +export function unsubscribeFromValidationTrigger(validationTrigger: IValidationTrigger, eventHandlers: IValidationTriggerEventHandlers): void { + if ('propertiesChanged' in validationTrigger && !!eventHandlers.propertiesChanged) + validationTrigger.propertiesChanged.unsubscribe(eventHandlers.propertiesChanged) + + if ('collectionChanged' in validationTrigger && !!eventHandlers.collectionChanged) + validationTrigger.collectionChanged.unsubscribe(eventHandlers.collectionChanged); + if ('collectionReordered' in validationTrigger && !!eventHandlers.collectionReordered) + validationTrigger.collectionReordered.unsubscribe(eventHandlers.collectionReordered); + + if ('setChanged' in validationTrigger && !!eventHandlers.setChanged) + validationTrigger.setChanged.unsubscribe(eventHandlers.setChanged); + + if ('mapChanged' in validationTrigger && !!eventHandlers.mapChanged) + validationTrigger.mapChanged.unsubscribe(eventHandlers.mapChanged); +} \ No newline at end of file diff --git a/src/validation/IValidator.ts b/src/validation/IValidator.ts new file mode 100644 index 0000000..33dd00e --- /dev/null +++ b/src/validation/IValidator.ts @@ -0,0 +1,10 @@ +import type { IReadOnlyValidatable } from './IReadOnlyValidatable'; + +export interface IValidator, TValidationError = string> { + onAdd?(validatable: TValidatable): void; + onRemove?(validatable: TValidatable): void; + + readonly validate: ValidatorCallback; +} + +export type ValidatorCallback, TValidationError = string> = (object: TValidatable) => TValidationError | null | undefined; \ No newline at end of file diff --git a/src/validation/index.ts b/src/validation/index.ts new file mode 100644 index 0000000..2a57e1a --- /dev/null +++ b/src/validation/index.ts @@ -0,0 +1,12 @@ +export type { IReadOnlyValidatable } from './IReadOnlyValidatable'; +export type { IValidatable } from './IValidatable'; + +export type { IValidationTrigger } from './IValidationTrigger'; + +export type { IValidator, ValidatorCallback } from './IValidator'; + +export { + type IReadOnlyObjectValidator, + type IObjectValidator, + ObjectValidator +} from './objectValidator'; \ No newline at end of file diff --git a/src/validation/objectValidator/IObjectValidator.ts b/src/validation/objectValidator/IObjectValidator.ts new file mode 100644 index 0000000..1f94dd5 --- /dev/null +++ b/src/validation/objectValidator/IObjectValidator.ts @@ -0,0 +1,20 @@ +import type { IObservableCollection, IObservableSet } from '../../collections'; +import type { IValidator, ValidatorCallback } from '../IValidator'; +import type { IValidationTrigger } from '../IValidationTrigger'; +import type { IReadOnlyObjectValidator } from './IReadOnlyObjectValidator'; +import type { IValidatable } from '../IValidatable'; + +export interface IObjectValidator & IValidationTrigger, TValidationError = string> extends IReadOnlyObjectValidator { + readonly validators: IObservableCollection>; + + readonly triggers: IObservableSet; + + add(validator: IValidator): this; + add(validator: IValidator, triggers: readonly IValidationTrigger[]): this; + add(validator: ValidatorCallback): this; + add(validator: ValidatorCallback, triggers: readonly IValidationTrigger[]): this; + + validate(): TValidationError | null; + + reset(): this; +} \ No newline at end of file diff --git a/src/validation/objectValidator/IReadOnlyObjectValidator.ts b/src/validation/objectValidator/IReadOnlyObjectValidator.ts new file mode 100644 index 0000000..d85469c --- /dev/null +++ b/src/validation/objectValidator/IReadOnlyObjectValidator.ts @@ -0,0 +1,14 @@ +import type { IReadOnlyValidatable } from '../IReadOnlyValidatable'; +import type { IReadOnlyObservableCollection, IReadOnlyObservableSet } from '../../collections'; +import type { IValidator } from '../IValidator'; +import type { IValidationTrigger } from '../IValidationTrigger'; + +export interface IReadOnlyObjectValidator & IValidationTrigger, TValidationError = string> { + readonly target: TValidatable; + + readonly validators: IReadOnlyObservableCollection>; + + readonly triggers: IReadOnlyObservableSet; + + validate(): TValidationError | null; +} diff --git a/src/validation/objectValidator/ObjectValidator.ts b/src/validation/objectValidator/ObjectValidator.ts new file mode 100644 index 0000000..6911907 --- /dev/null +++ b/src/validation/objectValidator/ObjectValidator.ts @@ -0,0 +1,159 @@ +import type { IValidatable } from '../IValidatable'; +import type { IValidator, ValidatorCallback } from '../IValidator'; +import type { IObjectValidator } from './IObjectValidator'; +import { type INotifyPropertiesChanged } from '../../viewModels'; +import { type IObservableCollection, type INotifyCollectionChanged, type ICollectionChange, type INotifyCollectionReordered, type ICollectionReorder, type INotifySetChanged, type IObservableSet, type ISetChange, type INotifyMapChanged, type IMapChange, ObservableCollection, ObservableSet } from '../../collections'; +import { type IValidationTrigger, type IValidationTriggerEventHandlers, subscribeToValidationTrigger, unsubscribeFromValidationTrigger } from '../IValidationTrigger'; + +export class ObjectValidator & IValidationTrigger, TValidationError = string> implements IObjectValidator { + private _isValidating: boolean; + private readonly _triggerChangedEventHandlers: IValidationTriggerEventHandlers; + + public constructor(target: TValidatable) { + this.target = target; + this._triggerChangedEventHandlers = { + propertiesChanged: { + handle: (validationTrigger, changedProperties) => { + if (this.shouldValidateViewModelTrigger(validationTrigger, changedProperties)) + this.validate(); + } + }, + + collectionChanged: { + handle: (validationTrigger, collectionChange) => { + if (this.shouldValidateCollectionChangedTrigger(validationTrigger, collectionChange)) + this.validate(); + } + }, + + collectionReordered: { + handle: (validationTrigger, collectionReorder) => { + if (this.shouldValidateCollectionReorderedTrigger(validationTrigger, collectionReorder)) + this.validate(); + } + }, + + setChanged: { + handle: (validationTrigger, setChanged) => { + if (this.shouldValidateSetChangedTrigger(validationTrigger, setChanged)) + this.validate(); + } + }, + + mapChanged: { + handle: (validationTrigger, mapChanged) => { + if (this.shouldValidateMapChangedTrigger(validationTrigger, mapChanged)) + this.validate(); + } + } + }; + + subscribeToValidationTrigger(this.target, this._triggerChangedEventHandlers); + + this.validators = new ObservableCollection>(); + this.validators.collectionChanged.subscribe({ + handle: (_, { addedItems: addedValidators, removedItems: removedValidators }) => { + removedValidators.forEach(removedValidator => removedValidator.onRemove && removedValidator.onRemove(this.target)); + addedValidators.forEach(addedValidator => addedValidator.onAdd && addedValidator.onAdd(this.target)); + + this.validate(); + } + }); + + this.triggers = new ObservableSet(); + this.triggers.setChanged.subscribe({ + handle: (_, { addedItems: addedTriggers, removedItems: removedTriggers }) => { + removedTriggers.forEach(removedTrigger => { + unsubscribeFromValidationTrigger(removedTrigger, this._triggerChangedEventHandlers); + }); + + addedTriggers.forEach(addedTrigger => { + subscribeToValidationTrigger(addedTrigger, this._triggerChangedEventHandlers); + }); + } + }); + + try { + this._isValidating = true; + + this.target.error = null; + } + finally { + this._isValidating = false; + } + } + + public readonly target: TValidatable; + + public readonly validators: IObservableCollection>; + + public readonly triggers: IObservableSet; + + public add(validator: IValidator): this; + public add(validator: IValidator, triggers: readonly IValidationTrigger[]): this; + public add(validator: ValidatorCallback): this; + public add(validator: ValidatorCallback, triggers: readonly IValidationTrigger[]): this; + + public add(validator: IValidator | ValidatorCallback, triggers?: readonly IValidationTrigger[]): this { + if (triggers !== null && triggers !== undefined) + triggers.forEach(trigger => this.triggers.add(trigger)); + + if (typeof validator === 'function') + this.validators.push({ validate: validator }); + else + this.validators.push(validator); + + return this; + } + + public validate(): TValidationError | null { + let error: TValidationError | null = null; + let index: number = 0; + + while (index < this.validators.length && error === null) { + const validationResult = this.validators[index].validate(this.target); + if (validationResult !== null && validationResult !== undefined) + error = validationResult; + else + index++; + } + + try { + this._isValidating = true; + + this.target.error = error; + return error; + } + finally { + this._isValidating = false; + } + + } + + public reset(): this { + this.triggers.clear(); + this.validators.splice(0); + + return this; + } + + protected shouldValidateViewModelTrigger(changedViewModel: INotifyPropertiesChanged, changedProperties: readonly PropertyKey[]): boolean { + return (!this._isValidating || this.target !== changedViewModel); + } + + protected shouldValidateCollectionChangedTrigger(changedCollection: INotifyCollectionChanged, collectionChange: ICollectionChange): boolean { + return true; + } + + protected shouldValidateCollectionReorderedTrigger(changedCollection: INotifyCollectionReordered, collectionReorder: ICollectionReorder): boolean { + return true; + } + + protected shouldValidateSetChangedTrigger(changedSet: INotifySetChanged, setChange: ISetChange): boolean { + return true; + } + + protected shouldValidateMapChangedTrigger(changedMap: INotifyMapChanged, mapChange: IMapChange): boolean { + return true; + } +} diff --git a/src/validation/objectValidator/index.ts b/src/validation/objectValidator/index.ts new file mode 100644 index 0000000..48207b8 --- /dev/null +++ b/src/validation/objectValidator/index.ts @@ -0,0 +1,3 @@ +export type { IReadOnlyObjectValidator } from './IReadOnlyObjectValidator'; +export type { IObjectValidator } from './IObjectValidator'; +export { ObjectValidator } from './ObjectValidator'; \ No newline at end of file diff --git a/src/validation/tests/ObjectValidator.tests.ts b/src/validation/tests/ObjectValidator.tests.ts new file mode 100644 index 0000000..497722c --- /dev/null +++ b/src/validation/tests/ObjectValidator.tests.ts @@ -0,0 +1,289 @@ +import type { INotifyPropertiesChanged } from '../../viewModels'; +import type { IValidatable } from '../IValidatable'; +import type { IValidationTrigger } from '../IValidationTrigger'; +import { type INotifyCollectionChanged, type ICollectionChange, type INotifyCollectionReordered, type ICollectionReorder, type INotifySetChanged, type ISetChange, type INotifyMapChanged, type IMapChange, ObservableCollection, ObservableSet, ObservableMap } from '../../collections'; +import { ObjectValidator } from '../objectValidator/ObjectValidator'; +import { FakeValidatable, FakeViewModelValidationTrigger } from './common'; + +describe('ObjectValidator', (): void => { + it('adding a validator validates the target', (): void => { + let invocationCount = 0; + let checkInvocationCount = 0; + const validatable = new FakeValidatable(); + + const objectValidator = new TestObjectValidator(validatable); + objectValidator.shouldValidateViewModelTriggerCallback = () => { + checkInvocationCount++; + return true; + } + objectValidator.add(() => { + invocationCount++; + return 'test error'; + }); + + expect(checkInvocationCount).toBe(0); + expect(invocationCount).toBe(1); + expect(validatable.error).toBe('test error'); + }); + + it('changing the target triggers a validaiton', (): void => { + let invocationCount = 0; + let checkInvocationCount = 0; + const validatable = new FakeValidatable(); + + const objectValidator = new TestObjectValidator(validatable); + objectValidator.shouldValidateViewModelTriggerCallback = () => { + checkInvocationCount++; + return true; + } + objectValidator.add(() => { + invocationCount++; + return 'test error'; + }); + validatable.triggerValidation(); + + expect(checkInvocationCount).toBe(1); + expect(invocationCount).toBe(2); + expect(validatable.error).toBe('test error'); + }); + + it('using multiple validators executes them until first invalid one', (): void => { + const validatorCalls: string[] = []; + const validatable = new FakeValidatable(); + + const objectValidator = new TestObjectValidator(validatable); + objectValidator.validators.push( + { + validate() { + validatorCalls.push('validator 1'); + return null; + } + }, + { + validate() { + validatorCalls.push('validator 2'); + return undefined; + } + }, + { + validate() { + validatorCalls.push('validator 3'); + return ""; + } + }, + { + validate() { + validatorCalls.push('validator 4'); + return null; + } + } + ); + + expect(validatorCalls).toEqual(['validator 1', 'validator 2', 'validator 3']); + }); + + it('adding a view model trigger validates target when it changes', (): void => { + let invocationCount = 0; + let checkInvocationCount = 0; + const validatable = new FakeValidatable(); + const viewModelValidationTrigger = new FakeViewModelValidationTrigger(); + + const objectValidator = new TestObjectValidator(validatable); + objectValidator.shouldValidateViewModelTriggerCallback = () => { + checkInvocationCount++; + return true; + } + objectValidator.add( + () => { + invocationCount++; + return 'test error'; + }, + [viewModelValidationTrigger] + ); + viewModelValidationTrigger.triggerValidation(); + + expect(checkInvocationCount).toBe(1); + expect(invocationCount).toBe(2); + expect(validatable.error).toBe('test error'); + }); + + it('adding an observable collection trigger validates target when it changes', (): void => { + let invocationCount = 0; + let checkInvocationCount = 0; + const validatable = new FakeValidatable(); + const observableCollectionValidationTrigger = new ObservableCollection(); + + const objectValidator = new TestObjectValidator(validatable); + objectValidator.shouldValidateCollectionChangedTriggerCallback = () => { + checkInvocationCount++; + return true; + }; + objectValidator.add( + () => { + invocationCount++; + return 'test error'; + }, + [observableCollectionValidationTrigger] + ); + observableCollectionValidationTrigger.push(1); + + expect(checkInvocationCount).toBe(1); + expect(invocationCount).toBe(2); + expect(validatable.error).toBe('test error'); + }); + + it('adding an observable collection trigger validates target when it reorders', (): void => { + let invocationCount = 0; + let checkInvocationCount = 0; + const validatable = new FakeValidatable(); + const observableCollectionValidationTrigger = new ObservableCollection([1, 2]); + + const objectValidator = new TestObjectValidator(validatable); + objectValidator.shouldValidateCollectionReorderedTriggerCallback = () => { + checkInvocationCount++; + return true; + } + objectValidator.add( + () => { + invocationCount++; + return 'test error'; + }, + [observableCollectionValidationTrigger] + ); + observableCollectionValidationTrigger.reverse(); + + expect(checkInvocationCount).toBe(1); + expect(invocationCount).toBe(2); + expect(validatable.error).toBe('test error'); + }); + + it('adding an observable set trigger validates target when it changes', (): void => { + let invocationCount = 0; + let checkInvocationCount = 0; + const validatable = new FakeValidatable(); + const observableSetValidationTrigger = new ObservableSet(); + + const objectValidator = new TestObjectValidator(validatable); + objectValidator.shouldValidateSetChangedTriggerCallback = () => { + checkInvocationCount++; + return true; + } + objectValidator.add( + () => { + invocationCount++; + return 'test error'; + }, + [observableSetValidationTrigger] + ); + observableSetValidationTrigger.add(1); + + expect(checkInvocationCount).toBe(1); + expect(invocationCount).toBe(2); + expect(validatable.error).toBe('test error'); + }); + + it('adding an observable map trigger validates target when it changes', (): void => { + let invocationCount = 0; + let checkInvocationCount = 0; + const validatable = new FakeValidatable(); + const observableMapValidationTrigger = new ObservableMap(); + + const objectValidator = new TestObjectValidator(validatable); + objectValidator.shouldValidateMapChangedTriggerCallback = () => { + checkInvocationCount++; + return true; + } + objectValidator.add( + () => { + invocationCount++; + return 'test error'; + }, + [observableMapValidationTrigger] + ); + observableMapValidationTrigger.set(1, 'a'); + + expect(checkInvocationCount).toBe(1); + expect(invocationCount).toBe(2); + expect(validatable.error).toBe('test error'); + }); + + it('adding a validator calls its onAdd hook', (): void => { + let hookInvocationCount = 0; + const validatable = new FakeValidatable(); + + const objectValidator = new TestObjectValidator(validatable); + objectValidator.add({ + onAdd(target) { + expect(target).toBe(validatable); + hookInvocationCount++; + }, + validate() { + return null; + } + }); + + expect(hookInvocationCount).toBe(1); + }); + + it('removing a validator calls its onRemove hook', (): void => { + let hookInvocationCount = 0; + const validatable = new FakeValidatable(); + + const objectValidator = new TestObjectValidator(validatable); + objectValidator.add({ + onRemove(target) { + expect(target).toBe(validatable); + hookInvocationCount++; + }, + validate() { + return null; + } + }); + objectValidator.reset(); + + expect(hookInvocationCount).toBe(1); + }); +}) + +class TestObjectValidator & IValidationTrigger, TValidationError = string> extends ObjectValidator { + public shouldValidateViewModelTriggerCallback?: typeof this.shouldValidateViewModelTrigger; + public shouldValidateCollectionChangedTriggerCallback?: typeof this.shouldValidateCollectionChangedTrigger; + public shouldValidateCollectionReorderedTriggerCallback?: typeof this.shouldValidateCollectionReorderedTrigger; + public shouldValidateSetChangedTriggerCallback?: typeof this.shouldValidateSetChangedTrigger; + public shouldValidateMapChangedTriggerCallback?: typeof this.shouldValidateMapChangedTrigger; + + protected shouldValidateViewModelTrigger(changedViewModel: INotifyPropertiesChanged, changedProperties: readonly PropertyKey[]): boolean { + return ( + super.shouldValidateViewModelTrigger(changedViewModel, changedProperties) + && (!this.shouldValidateViewModelTriggerCallback || this.shouldValidateViewModelTriggerCallback(changedViewModel, changedProperties)) + ); + } + + protected shouldValidateCollectionChangedTrigger(changedCollection: INotifyCollectionChanged, collectionChange: ICollectionChange): boolean { + return ( + super.shouldValidateCollectionChangedTrigger(changedCollection, collectionChange) + && (!this.shouldValidateCollectionChangedTriggerCallback || this.shouldValidateCollectionChangedTriggerCallback(changedCollection, collectionChange)) + ); + } + + protected shouldValidateCollectionReorderedTrigger(changedCollection: INotifyCollectionReordered, collectionReorder: ICollectionReorder): boolean { + return ( + super.shouldValidateCollectionReorderedTrigger(changedCollection, collectionReorder) + && (!this.shouldValidateCollectionReorderedTriggerCallback || this.shouldValidateCollectionReorderedTriggerCallback(changedCollection, collectionReorder)) + ); + } + + protected shouldValidateSetChangedTrigger(changedSet: INotifySetChanged, setChange: ISetChange): boolean { + return ( + super.shouldValidateSetChangedTrigger(changedSet, setChange) + && (!this.shouldValidateSetChangedTriggerCallback || this.shouldValidateSetChangedTriggerCallback(changedSet, setChange)) + ); + } + + protected shouldValidateMapChangedTrigger(changedMap: INotifyMapChanged, mapChange: IMapChange): boolean { + return ( + super.shouldValidateMapChangedTrigger(changedMap, mapChange) + && (!this.shouldValidateMapChangedTriggerCallback || this.shouldValidateMapChangedTriggerCallback(changedMap, mapChange)) + ); + } +} \ No newline at end of file diff --git a/src/validation/tests/common/FakeValidatable.ts b/src/validation/tests/common/FakeValidatable.ts new file mode 100644 index 0000000..768b5ad --- /dev/null +++ b/src/validation/tests/common/FakeValidatable.ts @@ -0,0 +1,25 @@ +import type { IValidatable } from '../../IValidatable'; +import { FakeViewModelValidationTrigger } from './FakeViewModelValidationTrigger'; + +export class FakeValidatable extends FakeViewModelValidationTrigger implements IValidatable { + private _error: string | null = null; + + public get error(): string | null { + return this._error; + } + + public set error(value: string | null) { + if (this._error !== value) { + this._error = value; + this.notifyPropertiesChanged('error'); + } + } + + public get isValid(): boolean { + return this.error === null || this.error === undefined; + } + + public get isInvalid(): boolean { + return this.error !== null && this.error !== undefined; + } +} diff --git a/src/validation/tests/common/FakeViewModelValidationTrigger.ts b/src/validation/tests/common/FakeViewModelValidationTrigger.ts new file mode 100644 index 0000000..43cd56d --- /dev/null +++ b/src/validation/tests/common/FakeViewModelValidationTrigger.ts @@ -0,0 +1,7 @@ +import { ViewModel } from '../../../viewModels'; + +export class FakeViewModelValidationTrigger extends ViewModel { + public triggerValidation() { + this.notifyPropertiesChanged('triggerValidation'); + } +} \ No newline at end of file diff --git a/src/validation/tests/common/index.ts b/src/validation/tests/common/index.ts new file mode 100644 index 0000000..555c777 --- /dev/null +++ b/src/validation/tests/common/index.ts @@ -0,0 +1,2 @@ +export { FakeValidatable } from './FakeValidatable'; +export { FakeViewModelValidationTrigger } from './FakeViewModelValidationTrigger'; \ No newline at end of file