/**
 * For dynamically linking inputs and outputs.
 */ import { EventEmitter } from '@angular/core' ; import * as _ from 'lodash' ; import { BehaviorSubject , Subscription } from 'rxjs' ; import { AppComponentDefinition , ComponentInstance } from './datatypes' ; import compileDvExpr , { dvToNgName } from './expression.compiler' ; todo export function fullyQualifyName ( ioName , componentInstance ) { return ( `${ componentInstance . from . name }.${ componentInstance . of . name }.${ ioName }` ) ; } export class ScopeIO { private readonly subjects : { [ ioName ] : BehaviorSubject < any > ; } = { } ; /**
     * Get a subject, creating one if it doesn't exist.
     */ getSubject ( ioName ) { if ( ! this . subjects [ ioName ] ) { this . subjects [ ioName ] = new BehaviorSubject ( undefined ) ; } return this . subjects [ ioName ] ; } hasSubject ( ioName ) { return ! ! this . subjects [ ioName ] ; } } export class ChildScopeIO extends ScopeIO { static componentInstanceComponent : any ; private readonly subscriptions : Subscription [ ] = [ ] ; param componentInstance param parentScope param extra constructor ( private readonly componentInstance , private readonly parentScope , private readonly shouldReLink ? , private readonly extra ? ) { super ( ) ; } /**
     * Parses all expressions.
     * Retrieves values from the parent scope to set inputs here.
     * Sets values on the parent scope from outputs here (if app component).
     */ link ( ) { this . unlink ( ) ; this . linkInputs ( ) ; if ( this . componentInstance . of instanceof AppComponentDefinition ) { this . linkOutputsForAppComponent ( ) ; } } /**
     * Remove all links.
     */ unlink ( ) { this . subscriptions . forEach ( ( s ) => s . unsubscribe ( ) ) ; this . subscriptions . length = 0 ; } /**
     * Retrieves values from the parent scope to set inputs here.
     * Cannot be called externally because it does not clean up subs.
     */ private linkInputs ( ) { this . componentInstance . of . inputs . forEach ( ( inputName ) => { const inputValue = this . componentInstance . inputSettings [ inputName ] ; if ( _ . isString ( inputValue ) ) { const toSubject = this . getSubject ( inputName ) ; this . emitExpressionValue ( inputValue , toSubject , this . parentScope ) ; } else if ( inputValue instanceof ComponentInstance ) { this . emitComponent ( inputValue , inputName ) ; } } ) ; } /**
     * Sets values on the parent scope from outputs here (if app component).
     * Cannot be called externally because it does not clean up subs.
     */ private linkOutputsForAppComponent ( ) { if ( ! ( this . componentInstance . of instanceof AppComponentDefinition ) ) { throw new Error ( 'Component is not an app component.' ) ; } this . componentInstance . of . outputSettings . forEach ( ( { name , value } ) => { const fqName = fullyQualifyName ( name , this . componentInstance ) ; const toSubject = this . parentScope . getSubject ( fqName ) ; this . emitExpressionValue ( value , toSubject , this ) ; } ) ; } /**
     * Parse an expression and set its value on the subject
     *   (from this or another ScopeIO probably) using values
     *   from the given scope.
     * It updates the value when any referenced value changes.
     * Cannot be called externally because it does not clean up subs.
     */ private emitExpressionValue ( dvExpression , toSubject , fromScope ) { let compiledDvExpr ; try { compiledDvExpr = compileDvExpr ( dvExpression ) ; } catch ( e ) { console . error ( `Failed to compile ${ dvExpression }` ) ; console . error ( e ) ; return ; } const { names , evaluate } = compiledDvExpr ; const ngScope = { } ; const send = ( ) => toSubject . next ( evaluate ( ngScope ) ) ; names . forEach ( ( refdName ) => { const fromAbove = refdName . startsWith ( '$' ) ; const ioScopeName = fromAbove ? refdName . slice ( 1 ) : refdName ; if ( fromAbove && fromScope === this . parentScope && this . extra && this . extra . inputs . indexOf ( ioScopeName ) >= 0 ) { fromScope = this . extra . scope ; } const refdSubject = fromScope . getSubject ( ioScopeName ) ; const ngScopePath = refdName . split ( '.' ) . map ( dvToNgName ) ; const sub = refdSubject . subscribe ( ( refdValue ) => { _ . set ( ngScope , ngScopePath , refdValue ) ; send ( ) ; } ) ; this . subscriptions . push ( sub ) ; } ) ; send ( ) ; } /**
     * Send a component instance to the given input (on this component).
     * It's less general than emitExpressionValue since components can only
     *   be inputted.
     */ private emitComponent ( componentInstance , toInputName ) { const toSubject = this . getSubject ( toInputName ) ; toSubject . next ( { type : ChildScopeIO . componentInstanceComponent , inputs : { componentInstance : componentInstance , parentScope : this , shouldReLink : this . shouldReLink , extraInputs : this . componentInstance . of . componentInputs [ toInputName ] } } ) ; } }