import { Binding , Expression } from 'aurelia-binding' ; import { GlobalValidationConfiguration } from './config' ; import { Validator } from './validator' ; import { validateTrigger } from './validate-trigger' ; import { getPropertyInfo } from './property-info' ; import { ValidationRenderer , RenderInstruction } from './validation-renderer' ; import { ValidateResult } from './validate-result' ; import { ValidateInstruction } from './validate-instruction' ; import { ControllerValidateResult } from './controller-validate-result' ; import { PropertyAccessorParser , PropertyAccessor } from './property-accessor-parser' ; import { ValidateEvent } from './validate-event' ; /**
 * Orchestrates validation.
 * Manages a set of bindings, renderers and objects.
 * Exposes the current list of validation results for binding purposes.
 */ export class ValidationController { public static inject = [ Validator , PropertyAccessorParser , GlobalValidationConfiguration ] ; private bindings = new Map < Binding , BindingInfo > ( ) ; private renderers : ValidationRenderer [ ] = [ ] ; /**
     * Validation results that have been rendered by the controller.
     */ private results : ValidateResult [ ] = [ ] ; /**
     * Validation errors that have been rendered by the controller.
     */ public errors : ValidateResult [ ] = [ ] ; /**
     *  Whether the controller is currently validating.
     */ public validating : boolean = false ; private elements = new Map < ValidateResult , Element [ ] > ( ) ; private objects = new Map < any , any > ( ) ; /**
     * The trigger that will invoke automatic validation of a property used in a binding.
     */ public validateTrigger : validateTrigger ; private finishValidating : Promise < any > = Promise . resolve ( ) ; private eventCallbacks : ( ( event : ValidateEvent ) => void ) [ ] = [ ] ; constructor ( private validator , private propertyParser , config ? ) { this . validateTrigger = config instanceof GlobalValidationConfiguration ? config . getDefaultValidationTrigger ( ) : GlobalValidationConfiguration . DEFAULT_VALIDATION_TRIGGER ; } param callback public subscribe ( callback ) { this . eventCallbacks . push ( callback ) ; return { dispose : ( ) => { const index = this . eventCallbacks . indexOf ( callback ) ; if ( index === - 1 ) { return ; } this . eventCallbacks . splice ( index , 1 ) ; } } ; } param object param rules public addObject ( object , rules ? ) { this . objects . set ( object , rules ) ; } param object public removeObject ( object ) { this . objects . delete ( object ) ; this . processResultDelta ( 'reset' , this . results . filter ( result => result . object === object ) , [ ] ) ; } /**
     * Adds and renders an error.
     */ public addError < TObject > ( message , object , propertyName = null ) { let resolvedPropertyName ; if ( propertyName === null ) { resolvedPropertyName = propertyName ; } else { resolvedPropertyName = this . propertyParser . parse ( propertyName ) ; } const result = new ValidateResult ( { __manuallyAdded__ : true } , object , resolvedPropertyName , false , message ) ; this . processResultDelta ( 'validate' , [ ] , [ result ] ) ; return result ; } /**
     * Removes and unrenders an error.
     */ public removeError ( result ) { if ( this . results . indexOf ( result ) !== - 1 ) { this . processResultDelta ( 'reset' , [ result ] , [ ] ) ; } } param renderer public addRenderer ( renderer ) { this . renderers . push ( renderer ) ; renderer . render ( { kind : 'validate' , render : this . results . map ( result => ( { result , elements : this . elements . get ( result ) as Element [ ] } ) ) , unrender : [ ] } ) ; } param renderer public removeRenderer ( renderer ) { this . renderers . splice ( this . renderers . indexOf ( renderer ) , 1 ) ; renderer . render ( { kind : 'reset' , render : [ ] , unrender : this . results . map ( result => ( { result , elements : this . elements . get ( result ) as Element [ ] } ) ) } ) ; } param binding param target param rules public registerBinding ( binding , target , rules ? ) { this . bindings . set ( binding , { target , rules , propertyInfo : null } ) ; } param binding public unregisterBinding ( binding ) { this . resetBinding ( binding ) ; this . bindings . delete ( binding ) ; } /**
     * Interprets the instruction and returns a predicate that will identify
     * relevant results in the list of rendered validation results.
     */ private getInstructionPredicate ( instruction ? ) { if ( instruction ) { const { object , propertyName , rules } = instruction ; let predicate ; if ( instruction . propertyName ) { predicate = x => x . object === object && x . propertyName === propertyName ; } else { predicate = x => x . object === object ; } if ( rules ) { return x => predicate ( x ) && this . validator . ruleExists ( rules , x . rule ) ; } return predicate ; } else { return ( ) => true ; } } param instruction public validate ( instruction ? ) { let execute ; if ( instruction ) { let { object , propertyName , rules } = instruction ; rules = rules || this . objects . get ( object ) ; if ( instruction . propertyName === undefined ) { execute = ( ) => this . validator . validateObject ( object , rules ) ; } else { execute = ( ) => this . validator . validateProperty ( object , propertyName , rules ) ; } } else { execute = ( ) => { const promises = [ ] ; for ( const [ object , rules ] of Array . from ( this . objects ) ) { promises . push ( this . validator . validateObject ( object , rules ) ) ; } for ( const [ binding , { rules } ] of Array . from ( this . bindings ) ) { const propertyInfo = getPropertyInfo ( binding . sourceExpression as Expression , binding . source ) ; if ( ! propertyInfo || this . objects . has ( propertyInfo . object ) ) { continue ; } promises . push ( this . validator . validateProperty ( propertyInfo . object , propertyInfo . propertyName , rules ) ) ; } return Promise . all ( promises ) . then ( resultSets => resultSets . reduce ( ( a , b ) => a . concat ( b ) , [ ] ) ) ; } ; } this . validating = true ; const returnPromise = this . finishValidating . then ( execute ) . then ( ( newResults ) => { const predicate = this . getInstructionPredicate ( instruction ) ; const oldResults = this . results . filter ( predicate ) ; this . processResultDelta ( 'validate' , oldResults , newResults ) ; if ( returnPromise === this . finishValidating ) { this . validating = false ; } const result = { instruction , valid : newResults . find ( x => ! x . valid ) === undefined , results : newResults } ; this . invokeCallbacks ( instruction , result ) ; return result ; } ) . catch ( exception => { this . validating = false ; this . finishValidating = Promise . resolve ( ) ; return Promise . reject ( exception ) ; } ) ; this . finishValidating = returnPromise ; return returnPromise ; } param instruction public reset ( instruction ? ) { const predicate = this . getInstructionPredicate ( instruction ) ; const oldResults = this . results . filter ( predicate ) ; this . processResultDelta ( 'reset' , oldResults , [ ] ) ; this . invokeCallbacks ( instruction , null ) ; } /**
     * Gets the elements associated with an object and propertyName (if any).
     */ private getAssociatedElements ( { object , propertyName } ) { const elements = [ ] ; for ( const [ binding , { target } ] of Array . from ( this . bindings ) ) { const propertyInfo = getPropertyInfo ( binding . sourceExpression as Expression , binding . source ) ; if ( propertyInfo && propertyInfo . object === object && propertyInfo . propertyName === propertyName ) { elements . push ( target ) ; } } return elements ; } private processResultDelta ( kind , oldResults , newResults ) { const instruction = { kind , render : [ ] , unrender : [ ] } ; newResults = newResults . slice ( 0 ) ; for ( const oldResult of oldResults ) { const elements = this . elements . get ( oldResult ) as Element [ ] ; this . elements . delete ( oldResult ) ; instruction . unrender . push ( { result : oldResult , elements } ) ; const newResultIndex = newResults . findIndex ( x => x . rule === oldResult . rule && x . object === oldResult . object && x . propertyName === oldResult . propertyName ) ; if ( newResultIndex === - 1 ) { this . results . splice ( this . results . indexOf ( oldResult ) , 1 ) ; if ( ! oldResult . valid ) { this . errors . splice ( this . errors . indexOf ( oldResult ) , 1 ) ; } } else { const newResult = newResults . splice ( newResultIndex , 1 ) [ 0 ] ; const elements = this . getAssociatedElements ( newResult ) ; this . elements . set ( newResult , elements ) ; instruction . render . push ( { result : newResult , elements } ) ; this . results . splice ( this . results . indexOf ( oldResult ) , 1 , newResult ) ; if ( ! oldResult . valid && newResult . valid ) { this . errors . splice ( this . errors . indexOf ( oldResult ) , 1 ) ; } else if ( ! oldResult . valid && ! newResult . valid ) { this . errors . splice ( this . errors . indexOf ( oldResult ) , 1 , newResult ) ; } else if ( ! newResult . valid ) { this . errors . push ( newResult ) ; } } } for ( const result of newResults ) { const elements = this . getAssociatedElements ( result ) ; instruction . render . push ( { result , elements } ) ; this . elements . set ( result , elements ) ; this . results . push ( result ) ; if ( ! result . valid ) { this . errors . push ( result ) ; } } for ( const renderer of this . renderers ) { renderer . render ( instruction ) ; } } /**
     * Validates the property associated with a binding.
     */ public validateBinding ( binding ) { if ( ! binding . isBound ) { return ; } const propertyInfo = getPropertyInfo ( binding . sourceExpression as Expression , binding . source ) ; let rules ; const registeredBinding = this . bindings . get ( binding ) ; if ( registeredBinding ) { rules = registeredBinding . rules ; registeredBinding . propertyInfo = propertyInfo ; } if ( ! propertyInfo ) { return ; } const { object , propertyName } = propertyInfo ; this . validate ( { object , propertyName , rules } ) ; } /**
     * Resets the results for a property associated with a binding.
     */ public resetBinding ( binding ) { const registeredBinding = this . bindings . get ( binding ) ; let propertyInfo = getPropertyInfo ( binding . sourceExpression as Expression , binding . source ) ; if ( ! propertyInfo && registeredBinding ) { propertyInfo = registeredBinding . propertyInfo ; } if ( registeredBinding ) { registeredBinding . propertyInfo = null ; } if ( ! propertyInfo ) { return ; } const { object , propertyName } = propertyInfo ; this . reset ( { object , propertyName } ) ; } param newTrigger public changeTrigger ( newTrigger ) { this . validateTrigger = newTrigger ; const bindings = Array . from ( this . bindings . keys ( ) ) ; for ( const binding of bindings ) { const source = binding . source ; binding . unbind ( ) ; binding . bind ( source ) ; } } /**
     * Revalidates the controller's current set of errors.
     */ public revalidateErrors ( ) { for ( const { object , propertyName , rule } of this . errors ) { if ( rule . __manuallyAdded__ ) { continue ; } const rules = [ [ rule ] ] ; this . validate ( { object , propertyName , rules } ) ; } } private invokeCallbacks ( instruction , result ) { if ( this . eventCallbacks . length === 0 ) { return ; } const event = new ValidateEvent ( result ? 'validate' : 'reset' , this . errors , this . results , instruction || null , result ) ; for ( let i = 0 ; i < this . eventCallbacks . length ; i ++ ) { this . eventCallbacks [ i ] ( event ) ; } } } /**
 * Information related to an "& validate" decorated binding.
 */ interface BindingInfo { /**
     * The DOM element associated with the binding.
     */ target : Element ; /**
     * The rules associated with the binding via the validate binding behavior's rules parameter.
     */ rules ? : any ; /**
     * The object and property associated with the binding.
     */ propertyInfo : { object : any ; propertyName : string ; } | null ; }