import { Observable , Subject , Subscription } from "rxjs/Rx" ; import { Scheduler } from "rxjs/Scheduler" ; import { PropertyChangedEventArgs } from "./events/property-changed-event-args" ; import { invokeCommand } from "./operator/invoke-command" ; import { ReactiveCommand } from "./reactive-command" ; import { IViewBindingHelper } from "./view" ; /**
 * Defines a class that represents a reactive object.
 * This is the base class for View Model classes, and it implements an event system that
 * allows notification of property changes, which is the basis of the observable pipeline.
 */ export class ReactiveObject { private _propertyChanged : Subject < PropertyChangedEventArgs < any > > ; /**
     * The property that stores all of the data stored in this object.
     */ private __data : any ; /**
     * Creates a new reactive object.
     */ constructor ( obj ? ) { this . _propertyChanged = new Subject < PropertyChangedEventArgs < any > > ( ) ; this . __data = { } ; if ( obj ) { if ( Array . isArray ( obj ) ) { obj . forEach ( n => this . setupProperty ( n , null ) ) ; } else if ( typeof obj === "object" ) { for ( var key in obj ) { if ( obj . hasOwnProperty ( key ) ) { var val = obj [ key ] ; this . setupProperty ( key , val ) ; } } } } } private setupProperty ( name , val ) { if ( ! this . hasOwnProperty ( name ) ) { Object . defineProperty ( this , name , { configurable : true , enumerable : false , get : ( ) => this . get ( name ) , set : ( v ) => this . set ( name , v ) } ) ; this . set ( name , val ) ; } } /**
     * Gets the observable that represents the stream of "propertyChanged" events from this object.
     */ public get propertyChanged ( ) : Observable < PropertyChangedEventArgs < any > > { return this . _propertyChanged ; } protected createPropertyChangedEventArgs ( propertyName , value ) { return new PropertyChangedEventArgs ( this , propertyName , value ) ; } /**
     * Emits a new property changed event for the given property from this object.
     */ protected emitPropertyChanged ( propertyName , newPropertyValue ? ) { var propValue = newPropertyValue != null ? newPropertyValue : this . get ( propertyName ) ; this . _propertyChanged . next ( this . createPropertyChangedEventArgs ( propertyName , propValue ) ) ; } /**
     * Emits a new property changed event for the given property if it has changed from the previous value.
     */ protected emitIfPropertyChanged ( propertyName , oldPropertyValue ) { var currentValue = ReactiveObject . get ( this , propertyName ) ; if ( currentValue !== oldPropertyValue ) { this . emitPropertyChanged ( propertyName , currentValue ) ; } } /**
     * Records the given property before and after the given function is run and raises a property changed notification
     * if the property value changed.
     */ protected trackPropertyChanges < T > ( prop , callback ) { var val = ReactiveObject . get ( this , prop ) ; var ret = callback ( ) ; this . emitIfPropertyChanged ( prop , val ) ; return ret ; } private static getSingleProperty < TObj , T > ( obj , property ) { if ( typeof obj [ property ] !== "undefined" || ! ( obj instanceof ReactiveObject ) ) { return obj [ property ] ; } else { return ReactiveObject . getReactiveProperty ( obj , property ) ; } } private static getReactiveProperty < TObj , T > ( obj , property ) { var value = ( < ReactiveObject > < any > obj ) . __data [ property ] ; if ( typeof value === "undefined" ) { return null ; } else { return value ; } } private static getDeepProperty < TObj , T > ( obj , evaluated ) { var firstProp = evaluated . children [ 0 ] ; var otherProperties = evaluated . property . substring ( firstProp . length + 1 ) ; var firstVal = ReactiveObject . get ( obj , firstProp ) ; if ( typeof firstVal !== "undefined" ) { if ( firstVal !== null ) { return ReactiveObject . get < TObj , T > ( firstVal , otherProperties ) ; } else { return null ; } } else { return undefined ; } } private static get < TObj , T > ( obj , property ) { var evaluated = ReactiveObject . evaluateLambdaOrString ( obj , property ) ; if ( evaluated . children . length === 1 ) { return ReactiveObject . getSingleProperty ( obj , evaluated . property ) ; } else { return ReactiveObject . getDeepProperty ( obj , evaluated ) ; } } param property public get < T > ( property ) { var evaluated = ReactiveObject . evaluateLambdaOrString ( this , property ) ; if ( evaluated . children . length === 1 ) { return ReactiveObject . getReactiveProperty ( this , evaluated . property ) ; } else { return ReactiveObject . getDeepProperty ( this , evaluated ) ; } } private static setSingleProperty < TObj , T > ( obj , property , value ) { if ( typeof obj [ property ] !== "undefined" || ! ( obj instanceof ReactiveObject ) ) { obj [ property ] = value ; } else { ReactiveObject . setReactiveProperty ( < ReactiveObject > < any > obj , property , value ) ; } } private static setReactiveProperty ( obj , property , value ) { var rObj = < any > obj ; var oldValue = rObj . __data [ property ] ; if ( value !== oldValue ) { rObj . __data [ property ] = value ; rObj . emitPropertyChanged ( property , value ) ; } } private static setDeepProperty < TObj , T > ( obj , evaluated , value ) { var firstProp = evaluated . children [ 0 ] ; var otherProperties = evaluated . property . substring ( firstProp . length + 1 ) ; var firstVal = ReactiveObject . get ( obj , firstProp ) ; if ( firstVal !== null ) { ReactiveObject . set ( firstVal , otherProperties , value ) ; } else { throw new Error ( "Null Reference Exception. Cannot set a child property on a null or undefined property of this object." ) ; } } private static traverse < TObj , T > ( obj , property , setSingle , setDeep ) { var evaluated = ReactiveObject . evaluateLambdaOrString ( obj , property ) ; if ( evaluated . children . length === 1 ) { setSingle ( evaluated ) ; } else { setDeep ( evaluated ) ; } } private static set < TObj , T > ( obj , property , value ) { ReactiveObject . traverse ( obj , property , ( evaluated ) => { ReactiveObject . setSingleProperty ( obj , evaluated . property , value ) ; } , ( evaluated ) => { ReactiveObject . setDeepProperty ( obj , evaluated , value ) ; } ) ; } param property param value public set < T > ( property , value ) { ReactiveObject . traverse ( this , property , ( evaluated ) => { ReactiveObject . setReactiveProperty ( this , evaluated . property , value ) ; } , ( evaluated ) => { ReactiveObject . setDeepProperty ( this , evaluated , value ) ; } ) ; } /**
     * Builds a proxy object that adds accessed property names to the given array if proxies are supported.
     * Returns null if proxies are not supported.
     */ private static buildGhostObject ( arr ) { function buildProxy ( ) { return new Proxy ( { } , { get ( target , prop , reciever ) { arr . push ( prop ) ; return buildProxy ( ) ; } } ) ; } if ( typeof Proxy !== 'undefined' ) { return buildProxy ( ) ; } return null ; } param expr private static evaluateLambdaExpression < TObj > ( obj , expr ) { var path = [ ] ; var ghost = ReactiveObject . buildGhostObject ( path ) ; if ( ghost ) { expr ( ghost ) ; } else { ReactiveObject . evaluateLambdaErrors ( path , expr ) ; } return path . join ( "." ) ; } private static evaluateLambdaErrors ( path , expr , currentObj = null ) { try { expr ( currentObj ) ; } catch ( ex ) { if ( ex instanceof TypeError ) { var error = < TypeError > ex ; var propertyName = null ; var match = ( /property\s+'(\w+)'/g ) . exec ( error . message ) ; if ( match ) { propertyName = match [ 1 ] ; } if ( ! propertyName ) { match = ( /evaluating \'([\w]+\.?)+\'/g ) . exec ( error . message ) ; if ( match ) { propertyName = match [ match . length - 1 ] ; } } if ( propertyName ) { path . push ( propertyName ) ; currentObj = currentObj || { } ; var currentPath = currentObj ; path . forEach ( ( p , i ) => { currentPath [ p ] = i < path . length - 1 ? { } : null ; currentPath = currentPath [ p ] ; } ) ; ReactiveObject . evaluateLambdaErrors ( path , expr , currentObj ) ; return ; } } throw ex ; } } private static evaluateLambdaOrString < TObj > ( obj , expression ) { var property ; if ( typeof expression === "function" ) { property = ReactiveObject . evaluateLambdaExpression ( obj , expression ) ; } else { property = < string > expression ; } var children = property . split ( "." ) ; return { children , property } ; } private static whenSingleProp ( obj , prop , emitCurrentVal = false ) { if ( obj instanceof ReactiveObject ) { var reactive = < ReactiveObject > obj ; var observable = reactive . propertyChanged . filter ( e => { return e . propertyName == prop ; } ) ; if ( emitCurrentVal ) { return Observable . of ( reactive . createPropertyChangedEventArgs ( prop , reactive . get ( prop ) ) ) . concat ( observable ) ; } else { return observable ; } } else { if ( obj . __viewBindingHelper ) { var helper = obj . __viewBindingHelper ; return Observable . create ( ( observer ) => { return helper . observeProp ( obj , prop , emitCurrentVal , e => { observer . next ( e ) ; } ) ; } ) ; } else { throw new Error ( "Unable to bind to objects that do not inherit from ReactiveObject or provide __viewBindingHelper" ) ; } } } private static whenSingle < TObj , TProp > ( obj , expression , emitCurrentVal = false ) { var evaulatedExpression = ReactiveObject . evaluateLambdaOrString ( obj , expression ) ; var children = evaulatedExpression . children ; var prop = evaulatedExpression . property ; if ( children . length === 1 ) { return ReactiveObject . whenSingleProp ( obj , prop , emitCurrentVal ) ; } else { var firstProp = children [ 0 ] ; var propertiesWithoutFirst = prop . substring ( firstProp . length + 1 ) ; var observationCount = 0 ; return ReactiveObject . whenSingleProp ( obj , firstProp , true ) . map ( change => { var obj = change . newPropertyValue ; observationCount ++ ; if ( obj ) { return ReactiveObject . whenSingle ( obj , propertiesWithoutFirst , emitCurrentVal || observationCount > 1 ) ; } else if ( emitCurrentVal ) { return Observable . of ( change ) ; } else { return Observable . empty < PropertyChangedEventArgs < any > > ( ) ; } } ) . switch ( ) ; } } /**
     * Gets an observable that resolves with the related property changed event whenever the given property updates.
     */ public whenSingle ( expression , emitCurrentVal = false ) { return ReactiveObject . whenSingle ( this , expression , emitCurrentVal ) ; } param first public whenAny < T1 > ( first ) ; param first param map public whenAny < T1 , TResult > ( first , map ) ; param properties public whenAny < T1 , T2 > ( first , second ) ; param properties param map public whenAny < T1 , T2 , TResult > ( first , second , map ) ; param properties public whenAny < T1 , T2 , T3 > ( first , second , third ) ; param properties param map public whenAny < T1 , T2 , T3 , TResult > ( first , second , third , map ) ; param properties param map public whenAny < T1 , T2 , T3 , T4 > ( first , second , third , fourth ) ; param properties param map public whenAny < T1 , T2 , T3 , T4 , TResult > ( first , second , third , fourth , map ) ; param properties param map public whenAny < T1 , T2 , T3 , T4 , T5 , TResult > ( first , second , third , fourth , fifth , map ) ; param properties param map public whenAny < T1 , T2 , T3 , T4 , T5 , T6 , TResult > ( first , second , third , fourth , fifth , sixth , map ) ; param properties param map public whenAny < T1 , T2 , T3 , T4 , T5 , T6 , T7 , TResult > ( first , second , third , fourth , fifth , sixth , seventh , map ) ; param properties param map public whenAny < TResult > ( ... args ) { var finalProperties = [ ] ; var map = this . getMapFunction ( args ) ; function iterateProperties ( properties ) { properties . forEach ( ( p , i ) => { var type = typeof p ; if ( type === "string" || type === "function" ) { finalProperties . push ( p ) ; } else if ( Array . isArray ( p ) ) { iterateProperties ( p ) ; } } ) ; } iterateProperties ( map ? args . slice ( 0 , args . length - 1 ) : args ) ; var observableList = finalProperties . map ( prop => { return this . whenSingle ( prop , true ) ; } ) . filter ( o => o != null ) ; if ( map ) { return Observable . combineLatest < TResult > ( ... observableList , map ) ; } else { return Observable . combineLatest ( ... observableList , ( ... events ) : any => { if ( events . length == 1 ) { return events [ 0 ] ; } else { return events ; } } ) ; } } private getMapFunction ( values ) { var mapFunction = null ; var lastArg = values [ values . length - 1 ] ; if ( values . length > 1 && typeof lastArg === "function" ) { mapFunction = lastArg ; } return mapFunction ; } /**
     * Gets an observable that resolves with the related property value(s) whenever the given properties update.
     */ public whenAnyValue < T1 > ( first ) ; map public whenAnyValue < T1 , TResult > ( first , map ) ; param map public whenAnyValue < T1 , T2 > ( first , second ) ; param map public whenAnyValue < T1 , T2 , TResult > ( first , second , map ) ; map public whenAnyValue < T1 , T2 , T3 > ( first , second , third ) ; map public whenAnyValue < T1 , T2 , T3 , TResult > ( first , second , third , map ) ; map public whenAnyValue < T1 , T2 , T3 , T4 > ( first , second , third , fourth ) ; map public whenAnyValue < T1 , T2 , T3 , T4 , TResult > ( first , second , third , fourth , map ) ; map public whenAnyValue < T1 , T2 , T3 , T4 , T5 , TResult > ( first , second , third , fourth , fifth , map ) ; map public whenAnyValue < T1 , T2 , T3 , T4 , T5 , T6 , TResult > ( first , second , third , fourth , fifth , sixth , map ) ; map public whenAnyValue < T1 , T2 , T3 , T4 , T5 , T6 , T7 , TResult > ( first , second , third , fourth , fifth , sixth , seventh , map ) ; map public whenAnyValue < TResult > ( ... args ) { var mapFunction = this . getMapFunction ( args ) ; var whenAnyArgs = mapFunction ? args . slice ( 0 , args . length - 1 ) : args ; var whenAny = this . whenAny . bind ( this ) ; return whenAny ( ... whenAnyArgs , ( ... events ) => { var eventValues = events . map ( e => e . newPropertyValue ) ; if ( mapFunction ) { return mapFunction ( ... eventValues ) ; } else if ( eventValues . length == 1 ) { return eventValues [ 0 ] ; } else { return eventValues ; } } ) ; } private mergeChanges < TChanges extends { value : any ; } > ( ... changes ) { return Observable . merge ( ... changes ) . distinctUntilChanged ( ( v1 , v2 ) => v1 === v2 , c => c . value ) ; } param view param viewModelProp param viewProp param scheduler public bind < TView , TViewModelProp , TViewProp > ( view , viewModelProp , viewProp , scheduler ? ) { var viewChanges = ReactiveObject . whenSingle ( view , viewProp ) . map ( c => ( { fromVm : false , value : c . newPropertyValue } ) ) ; var viewModelChanges = this . whenAny ( viewModelProp ) . map ( c => ( { fromVm : true , value : c . newPropertyValue } ) ) ; var changes = this . mergeChanges ( viewChanges , viewModelChanges ) . startWith ( { fromVm : true , value : this . get < TViewModelProp > ( viewModelProp ) } ) ; return changes . subscribe ( c => { if ( c . fromVm ) { ReactiveObject . set ( view , viewProp , c . value ) ; } else { this . set ( viewModelProp , c . value ) ; } } ) ; } param view param viewModelProp param viewProp return public oneWayBind < TView , TViewModelProp , TViewProp > ( view , viewModelProp , viewProp , scheduler ? ) { var viewModelChanges = this . whenAny ( viewModelProp ) . map ( c => ( { fromVm : true , value : c . newPropertyValue } ) ) ; var changes = this . mergeChanges ( viewModelChanges ) . startWith ( { fromVm : true , value : this . get < TViewModelProp > ( viewModelProp ) } ) ; if ( scheduler ) { changes = changes . observeOn ( scheduler ) ; } return changes . subscribe ( c => { if ( c . fromVm ) { ReactiveObject . set ( view , viewProp , c . value ) ; } } ) ; } param observable param view param viewProp public static bindObservable < TObserved , TView , TViewProp > ( observable , view , viewProp , scheduler ? ) { var o = observable . distinctUntilChanged ( ) ; if ( scheduler ) { o = o . observeOn ( scheduler ) ; } return o . subscribe ( value => { ReactiveObject . set ( view , viewProp , < any > value ) ; } ) ; } public when < T > ( observable ) { if ( typeof observable === "string" ) { return this . whenSingle ( observable , true ) . map ( e => < Observable < T > > e . newPropertyValue ) . switch ( ) ; } else { return observable ; } } param observable param command public invokeCommandWhen < T > ( observable , command ) { return invokeCommand ( this . when ( observable ) , this , command ) ; } param observable param property param scheduler public toProperty < TObservable , TProp > ( observable , property , scheduler ? ) { return ReactiveObject . bindObservable ( observable , this , property , scheduler ) ; } /**
     * Returns the data that should be used to convert this reactive object into a JSON string.
     */ public toJSON ( ) { return ReactiveObject . clone ( this . __data ) ; } /**
     * Returns the string representation of this reactive object.
     */ public toString ( ) { return JSON . stringify ( this ) ; } param obj public static keys ( obj ) { if ( obj instanceof ReactiveObject ) { return Object . keys ( obj . __data ) ; } else { return Object . keys ( obj ) ; } } private static clone ( obj ) { var clone = { } ; for ( var key in obj ) { if ( obj . hasOwnProperty ( key ) ) { clone [ key ] = obj [ key ] ; } } return clone ; } }