import { isDraft , isDraftable } from 'immer' ; import { $O , definePrivate , Dictionary , isObject , shallowCopy } from '../shared' ; import { Change } from './Change' ; import { commit } from './commit' ; import { freeze , isFrozen } from './freeze' ; import { getObservable , Observable } from './Observable' ; /** An observable object with observable properties */ export class OProps < T extends Dictionary < any > = any > extends Observable < T > { watched ? : Dictionary < OProp < T > > ; constructor ( source ) { super ( source ) ; } watch < P extends string | number > ( prop ) { const watched = this . watched || ( this . watched = Object . create ( null ) ) ; return watched [ prop ] || ( watched [ prop ] = new OProp < T , P > ( this , prop ) ) ; } internal [ '_onChange' ] ( change ) { if ( change . target !== this ) { const currProps = this . get ( ) ; if ( change . state ) { const { copy , parent } = change . state ; if ( copy == currProps ) return ; if ( parent && parent . base == currProps ) return ; } const target = change . target as OProp < T > ; const nextProps = shallowCopy ( currProps ) ; nextProps [ target . prop ] = change . newValue ; definePrivate ( nextProps , $O , this ) ; this . _rebind ( nextProps ) ; freeze ( nextProps ) ; change . target = this ; change . prop = target . prop ; this . _onChange ( change ) ; change . prop = null ; change . oldValue = currProps ; change . newValue = nextProps ; } else if ( change . prop !== null ) { const { prop , oldValue , newValue } = change ; const oldObservable = oldValue && oldValue [ $O ] ; if ( newValue ) { let observable = getObservable ( newValue ) ; if ( ! observable && ! isFrozen ( newValue ) && isDraftable ( newValue ) ) { observable = bindProps ( newValue ) ; } if ( observable && ! oldObservable ) { this . watch ( prop ) ; } } if ( this . watched ) { const target = this . watched [ prop ] ; if ( target ) { commit ( target , oldValue , newValue , null , false , change . state ) ; } } } super . _onChange ( change ) ; } } export class OProp < T extends Dictionary < any > = any , P extends string | number = any > extends Observable < T [ P ] > { /** Exists when the observed property name has an observable value */ private _observedValue ? : Observable < T [ P ] > = undefined ; constructor ( readonly parent , readonly prop ) { super ( ( ) => parent . get ( ) [ prop ] ) ; this . _observeValue ( this . get ( ) ) ; } internal [ '_onChange' ] ( change ) { if ( change . target === this ) { this . _observeValue ( change . newValue ) ; super . _onChange ( change ) ; } else if ( change . prop === null ) { this . parent . _onChange ( { ... change , target : this } ) ; } } protected activate ( ) { if ( ! this . _observedValue ) { const watched = this . parent . watched ! ; const target = watched [ this . prop ] ; if ( ! target ) { watched [ this . prop ] = this ; } else if ( target !== this ) { throw Error ( 'Cannot observe an old observable' ) ; } } } protected deactivate ( ) { if ( ! this . _observedValue ) { delete this . parent . watched ! [ this . prop ] ; } } private _observeValue ( value ) { const observed = value && value [ $O ] ; if ( observed ) { if ( observed !== this . _observedValue ) { if ( this . _observedValue ) { this . _observedValue . _removeObserver ( this ) ; } if ( ! this . _observers ) { this . activate ( ) ; } observed . _addObserver ( this , true ) ; this . _observedValue = observed ; } } else if ( this . _observedValue ) { this . _observedValue . _removeObserver ( this ) ; this . _observedValue = undefined ; if ( ! this . _observers ) { this . deactivate ( ) ; } } } } export function bindProps < T extends object > ( root ) { if ( isFrozen ( root ) ) { throw Error ( 'Frozen objects cannot become observable' ) ; } if ( isDraft ( root ) ) { throw Error ( 'Immer drafts cannot become observable' ) ; } if ( ! isDraftable ( root ) ) { throw Error ( 'This object has no observable type' ) ; } const rootObservable = new OProps ( root ) ; definePrivate ( root , $O , rootObservable ) ; const observeTree = ( parent , observer ) => { Object . keys ( parent ) . forEach ( prop => { const value = parent [ prop ] ; if ( isObject ( value ) ) { let observable = getObservable ( value ) as OProps ; if ( ! observable ) { if ( isFrozen ( value ) || ! isDraftable ( value ) ) return ; observable = new OProps ( value ) ; definePrivate ( value , $O , observable ) ; observeTree ( value , observable ) ; } observer . watch ( prop ) ; } } ) ; freeze ( parent ) ; } ; observeTree ( root , rootObservable ) ; return rootObservable ; }