import { Injector , ɵComponentType as ComponentType , ɵComponentDef as ComponentDef } from '@angular/core' ; import { Observable } from 'rxjs' ; import { NgElementStrategy , NgElementStrategyFactory } from './element-strategy' ; import { camelToDashCase } from './utils' ; import { isNode , getComponentId } from '../utils/utils' ; import { EventContract } from '../tsaction/event_contract' ; import { RESOLVERS } from '../runtime' ; /** Time in milliseconds to wait before destroying the component ref when disconnected. */ const DESTROY_DELAY = 10 ; const PENDING_RESOLVES = '__pending_resolves__' ; const WAIT_RESOLVE = '__wait_resolve__' ; interface Resolver { getInitialInputs ? : ( properties : { } ) => Promise < { } > ; } export class LazyIvyElementStrategyFactory < T > implements NgElementStrategyFactory { constructor ( private doc , private ngBitsLoader , private componentType , private rendererFactory ? , private moduleLoader ? , private contract ? ) { } create ( ) { return new LazyIvyElementStrategy ( this . doc , this . ngBitsLoader , this . componentType , this . rendererFactory , this . moduleLoader , this . contract ) ; } } interface NgBits < T > { createStyle ( doc , styles , compId ) ; render < T > ( doc , componentType , element , hostFeatures , injector ? , rendererFactory ? ) : T ; markDirty ( component ) : void ; initializeOutputs < T > ( component , componentType , registerEventType ) : Array < { templateName : string ; output : any ; } > ; } /**
 * An Ivy Element that lazily bootstraps when one of the following events occur
 * 1) If ComponentType(non-string) is passed into the constructor (ELSE)
 * 2) If Element is not server-side rendered then load when IntersectionObserver
 *    goes off (ELSE)
 * 3) If Element is SSR-ed:
 *   a) If an event handler inside the Element goes off (ELSE)
 *   b) If the input properties to the Element changes
 */ export class LazyIvyElementStrategy < T > implements NgElementStrategy { private isLazy : boolean ; private isConnected = false ; /** Merged stream of the component's output events. */ events : Array < { templateName : string ; output : Observable < any > ; } > ; /**
     * Module containing Angular related bits.
     * Will be loaded lazily in case the Element is loaded lazily.
     */ private ngBits : NgBits < T > | null = null ; /** Reference to the component that was created on connect. */ private component ! : T | null ; /** Store backing Element for lazy initialization later */ private element : HTMLElement ; /** Callback to tell the parent custom element component is fully ready */ private upgradedCallback : Function ; private loading = false ; /** Reference number returned by setTimeout when scheduling to destroy. */ private destroyTimeoutRef : number | null = null ; /** Initial properties that were set before the element connected. */ private readonly initialProperties = new Map < string , any > ( ) ; /** Properties that were set after the element connected. */ private newProperties : Map < string , any > | null = null ; constructor ( private doc , private ngBitsLoader , private componentType , private rendererFactory ? , private moduleLoader ? , private contract ? ) { this . isLazy = typeof this . componentType === 'string' ; } /**
     * Initializes a new component if one has not yet been created and cancels any scheduled
     * destruction.
     */ connect ( element , upgradeCallback ) { if ( this . destroyTimeoutRef !== null ) { clearTimeout ( this . destroyTimeoutRef ) ; this . destroyTimeoutRef = null ; return ; } this . isConnected = true ; this . element = element ; this . upgradedCallback = upgradeCallback ; if ( ! this . isLazy ) { if ( ! this . component ) { this . ngBits = this . ngBitsLoader ( ) as NgBits < T > ; if ( isNode ( ) ) { const propNames = Object . keys ( ( this . componentType as ComponentType < T > ) . ngComponentDef [ 'inputs' ] ) ; for ( const propName of propNames ) { const attributeName = camelToDashCase ( propName ) ; const value = element [ propName ] || this . initialProperties . get ( propName ) ; if ( value != null ) { const jsonValue = JSON . stringify ( value ) . replace ( /\"/g , '\'' ) ; element . setAttribute ( attributeName , jsonValue ) ; } } } const init = ( ) => { this . initializeComponent ( element , this . componentType as ComponentType < T > ) ; } ; this . runResolvers ( ) . then ( init ) . catch ( init ) ; } } else { if ( this . initialProperties . get ( '_boot' ) != null || ! this . isServerSideRendered ( ) ) { this . loadAndInitializeComponent ( ) ; } } } /**
     * Schedules the component to be destroyed after some small delay in case the element is just
     * being moved across the DOM.
     */ disconnect ( ) { if ( ! this . component || this . destroyTimeoutRef !== null ) { return ; } this . scheduleDestroy ( ) ; } private getTemplateNameFromPropertyName ( prop ) { return ( this . componentType as ComponentType < T > ) . ngComponentDef [ 'inputs' ] [ prop ] ; } private isServerSideRendered ( ) { return this . element . getAttribute ( '_s' ) != null ; } /**
     * Returns the component property value.
     */ getInputValue ( propName ) { if ( ! this . component ) { return this . initialProperties . get ( propName ) ; } const templateName = this . getTemplateNameFromPropertyName ( propName ) ; return this . component [ templateName ] ; } /**
     * Sets the input value for the property.
     */ setInputValue ( propName , value , attributeChanged ) { if ( attributeChanged ) { if ( ! this . isConnected ) { let parsedValue = value ; if ( ! isNode ( ) ) { if ( value ) { try { parsedValue = JSON . parse ( value . replace ( /\'/g , '"' ) ) ; } catch ( e ) { } } } this . initialProperties . set ( propName , parsedValue ) ; } else if ( propName === '_boot' ) { this . loadAndInitializeComponent ( ) ; } return ; } const originalValue = this . getInputValue ( propName ) ; if ( strictEquals ( value , originalValue ) ) { return ; } if ( ! this . isLazy ) { if ( ! this . isConnected ) { this . initialProperties . set ( propName , value ) ; } else { const templateName = this . getTemplateNameFromPropertyName ( propName ) ; this . component [ templateName ] = value ; if ( this . ngBits ) { this . ngBits . markDirty ( this . component ) ; } } return ; } if ( ! this . isConnected || ! this . isServerSideRendered ( ) ) { this . initialProperties . set ( propName , value ) ; } else { if ( ! this . newProperties ) { this . newProperties = new Map ( this . initialProperties ) ; } this . newProperties . set ( propName , value ) ; this . loadAndInitializeComponent ( ) ; } } private loadAndInitializeComponent ( ) { if ( this . loading ) { return ; } this . loading = true ; const moduleName = this . componentType as string ; return Promise . all ( [ this . ngBitsLoader ( ) , this . moduleLoader ( moduleName ) ] ) . then ( ( [ ngBits , module ] ) => { const exports = Object . keys ( module ) ; let compType = null ; if ( typeof exports === 'function' ) { compType = exports ; } else { for ( const exp of exports ) { if ( module [ exp ] [ 'ngComponentDef' ] != null ) { compType = module [ exp ] ; break ; } } } if ( ! compType ) { console . error ( `Did not find any exported components in ${ moduleName }` ) ; } else { this . componentType = compType as ComponentType < T > ; this . isLazy = false ; this . ngBits = ngBits ; const init = ( ) => this . initializeLoadedComponent ( ) ; this . runResolvers ( ) . then ( init ) . catch ( init ) ; } } ) . catch ( e => { console . error ( `Failed to load ${ moduleName }` , e ) ; } ) . finally ( ( ) => { this . loading = false ; } ) ; } private incrementPendingResolves ( ) { if ( this . doc [ PENDING_RESOLVES ] == null ) { this . doc [ PENDING_RESOLVES ] = 0 ; } this . doc [ PENDING_RESOLVES ] ++ ; } private decrementPendingResolves ( ) { if ( -- this . doc [ PENDING_RESOLVES ] == 0 ) { if ( this . doc [ WAIT_RESOLVE ] != null ) { this . doc [ WAIT_RESOLVE ] ( ) ; } } } private runResolvers ( ) { const compType = this . componentType as ComponentType < T > & Resolver ; const resolvers = compType [ RESOLVERS ] ; if ( resolvers ) { const props = { } ; for ( const propName of Object . keys ( compType . ngComponentDef [ 'inputs' ] ) ) { props [ propName ] = this . element [ propName ] || this . initialProperties . get ( propName ) ; } props [ '__doc' ] = this . doc ; let promises = [ ] as Array < Promise < void > > ; for ( const resolver of Array . from ( resolvers . keys ( ) ) ) { try { this . incrementPendingResolves ( ) ; promises . push ( resolvers . get ( resolver ) ( { ... props } ) . then ( value => { this . decrementPendingResolves ( ) ; this . initialProperties . set ( resolver , value ) ; } ) . catch ( ( ) => { this . decrementPendingResolves ( ) ; } ) ) ; } catch ( e ) { this . decrementPendingResolves ( ) ; } } return Promise . all ( promises ) ; } return Promise . resolve ( ) ; } private initializeLoadedComponent ( ) { this . initializeComponent ( this . element , this . componentType as ComponentType < T > ) ; if ( this . contract ) { this . contract . boot ( this . element ) ; } let changed = false ; if ( this . newProperties && this . newProperties . size > 0 ) { for ( const propName of Array . from ( this . newProperties . keys ( ) ) ) { const templateName = this . getTemplateNameFromPropertyName ( propName ) ; this . component [ templateName ] = this . newProperties . get ( propName ) ; } changed = true ; this . newProperties . clear ( ) ; } if ( this . contract ) { this . contract . replay ( this . element ) ; } if ( changed ) { this . ngBits . markDirty ( this . component ) ; } } param element private getInheritedElementInjector ( element ) { let injector = element [ '__injector__' ] ; while ( injector == null && element != null ) { element = element . parentElement ; } return injector ; } /**
     * Renders the component on the host element and initializes the inputs and outputs.
     */ protected initializeComponent ( element , componentType ) { const name = element . localName ; const doc = this . doc as any ; if ( ! doc . _seenElements . has ( name ) ) { const compId = getComponentId ( doc , name ) ; const styles = componentType . ngComponentDef [ 'styles' ] ; if ( styles && styles . length > 0 ) { this . ngBits . createStyle ( doc , styles , compId ) ; } } this . component = this . ngBits . render ( this . doc , componentType , element , [ this . initializeInputs . bind ( this , element ) , ] , this . getInheritedElementInjector ( element ) , this . rendererFactory ) ; this . events = this . ngBits . initializeOutputs ( this . component , componentType , type => { if ( this . contract ) { this . contract . listenToCustomEvent ( type ) ; } } ) ; this . upgradedCallback ( ) ; } /** Set any stored initial inputs on the component's properties. */ protected initializeInputs ( element , component , componentDef ) { const inputs = Object . keys ( componentDef [ 'inputs' ] ) ; inputs . forEach ( prop => { const templateName = componentDef [ 'inputs' ] [ prop ] ; if ( templateName == null ) { return ; } const value = isNode ( ) ? element [ prop ] || this . initialProperties . get ( prop ) : this . initialProperties . get ( prop ) ; if ( value !== undefined ) { component [ templateName ] = value ; } } ) ; this . initialProperties . clear ( ) ; } private scheduleDestroy ( ) { this . destroyTimeoutRef = setTimeout ( ( ) => { if ( this . component ) { const onDestroy = this . component [ 'ngOnDestroy' ] ; if ( onDestroy ) { onDestroy ( ) ; } this . component = null ; } } , DESTROY_DELAY ) as any ; } } /**
 * Test two values for strict equality, accounting for the fact that `NaN !== NaN`.
 */ export function strictEquals ( value1 , value2 ) { return value1 === value2 || ( value1 !== value1 && value2 !== value2 ) ; }