-
Notifications
You must be signed in to change notification settings - Fork 2
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
181e8a9
commit c113cc5
Showing
19 changed files
with
623 additions
and
14 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,7 @@ | ||
export interface IReadOnlyValidatable<TValidationError = string> { | ||
readonly isValid: boolean; | ||
|
||
readonly isInvalid: boolean; | ||
|
||
readonly error: TValidationError | null; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
import type { IReadOnlyValidatable } from './IReadOnlyValidatable'; | ||
|
||
export interface IValidatable<TValidationError = string> extends IReadOnlyValidatable<TValidationError> { | ||
error: TValidationError | null; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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<unknown> | INotifyCollectionReordered<unknown> | INotifySetChanged<unknown> | INotifyMapChanged<unknown, unknown>; | ||
|
||
export interface IValidationTriggerEventHandlers<TKey = unknown, TItem = unknown> { | ||
readonly propertiesChanged?: IPropertiesChangedEventHandler<INotifyPropertiesChanged>; | ||
|
||
readonly collectionChanged?: ICollectionChangedEventHandler<INotifyCollectionChanged<TItem>, TItem>; | ||
readonly collectionReordered?: ICollectionReorderedEventHandler<INotifyCollectionReordered<TItem>, TItem>; | ||
|
||
readonly setChanged?: ISetChangedEventHandler<INotifySetChanged<TItem>, TItem>; | ||
readonly mapChanged?: IMapChangedEventHandler<INotifyMapChanged<TItem, TKey>, TKey, TItem>; | ||
} | ||
|
||
export function subscribeToValidationTrigger<TKey = unknown, TItem = unknown>(validationTrigger: IValidationTrigger, eventHandlers: IValidationTriggerEventHandlers<TKey, TItem>): 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<TKey = unknown, TItem = unknown>(validationTrigger: IValidationTrigger, eventHandlers: IValidationTriggerEventHandlers<TKey, TItem>): 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); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,10 @@ | ||
import type { IReadOnlyValidatable } from './IReadOnlyValidatable'; | ||
|
||
export interface IValidator<TValidatable extends IReadOnlyValidatable<TValidationError>, TValidationError = string> { | ||
onAdd?(validatable: TValidatable): void; | ||
onRemove?(validatable: TValidatable): void; | ||
|
||
readonly validate: ValidatorCallback<TValidatable, TValidationError>; | ||
} | ||
|
||
export type ValidatorCallback<TValidatable extends IReadOnlyValidatable<TValidationError>, TValidationError = string> = (object: TValidatable) => TValidationError | null | undefined; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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'; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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<TValidatable extends IValidatable<TValidationError> & IValidationTrigger, TValidationError = string> extends IReadOnlyObjectValidator<TValidatable, TValidationError> { | ||
readonly validators: IObservableCollection<IValidator<TValidatable, TValidationError>>; | ||
|
||
readonly triggers: IObservableSet<IValidationTrigger>; | ||
|
||
add(validator: IValidator<TValidatable, TValidationError>): this; | ||
add(validator: IValidator<TValidatable, TValidationError>, triggers: readonly IValidationTrigger[]): this; | ||
add(validator: ValidatorCallback<TValidatable, TValidationError>): this; | ||
add(validator: ValidatorCallback<TValidatable, TValidationError>, triggers: readonly IValidationTrigger[]): this; | ||
|
||
validate(): TValidationError | null; | ||
|
||
reset(): this; | ||
} |
14 changes: 14 additions & 0 deletions
14
src/validation/objectValidator/IReadOnlyObjectValidator.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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<TValidatable extends IReadOnlyValidatable<TValidationError> & IValidationTrigger, TValidationError = string> { | ||
readonly target: TValidatable; | ||
|
||
readonly validators: IReadOnlyObservableCollection<IValidator<TValidatable, TValidationError>>; | ||
|
||
readonly triggers: IReadOnlyObservableSet<IValidationTrigger>; | ||
|
||
validate(): TValidationError | null; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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<TValidatable extends IValidatable<TValidationError> & IValidationTrigger, TValidationError = string> implements IObjectValidator<TValidatable, TValidationError> { | ||
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<IValidator<TValidatable, TValidationError>>(); | ||
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<INotifyPropertiesChanged>(); | ||
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<IValidator<TValidatable, TValidationError>>; | ||
|
||
public readonly triggers: IObservableSet<IValidationTrigger>; | ||
|
||
public add(validator: IValidator<TValidatable, TValidationError>): this; | ||
public add(validator: IValidator<TValidatable, TValidationError>, triggers: readonly IValidationTrigger[]): this; | ||
public add(validator: ValidatorCallback<TValidatable, TValidationError>): this; | ||
public add(validator: ValidatorCallback<TValidatable, TValidationError>, triggers: readonly IValidationTrigger[]): this; | ||
|
||
public add(validator: IValidator<TValidatable, TValidationError> | ValidatorCallback<TValidatable, TValidationError>, 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<unknown>, collectionChange: ICollectionChange<unknown>): boolean { | ||
return true; | ||
} | ||
|
||
protected shouldValidateCollectionReorderedTrigger(changedCollection: INotifyCollectionReordered<unknown>, collectionReorder: ICollectionReorder<unknown>): boolean { | ||
return true; | ||
} | ||
|
||
protected shouldValidateSetChangedTrigger(changedSet: INotifySetChanged<unknown>, setChange: ISetChange<unknown>): boolean { | ||
return true; | ||
} | ||
|
||
protected shouldValidateMapChangedTrigger(changedMap: INotifyMapChanged<unknown, unknown>, mapChange: IMapChange<unknown, unknown>): boolean { | ||
return true; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
export type { IReadOnlyObjectValidator } from './IReadOnlyObjectValidator'; | ||
export type { IObjectValidator } from './IObjectValidator'; | ||
export { ObjectValidator } from './ObjectValidator'; |
Oops, something went wrong.