| 
									
										
										
										
											2018-02-28 09:45:11 -08:00
										 |  |  | /** | 
					
						
							|  |  |  |  * @license | 
					
						
							| 
									
										
										
										
											2020-05-19 12:08:49 -07:00
										 |  |  |  * Copyright Google LLC All Rights Reserved. | 
					
						
							| 
									
										
										
										
											2018-02-28 09:45:11 -08:00
										 |  |  |  * | 
					
						
							|  |  |  |  * Use of this source code is governed by an MIT-style license that can be | 
					
						
							|  |  |  |  * found in the LICENSE file at https://angular.io/license
 | 
					
						
							|  |  |  |  */ | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2018-03-06 14:02:25 -08:00
										 |  |  | import {ApplicationRef, ComponentFactory, ComponentFactoryResolver, ComponentRef, EventEmitter, Injector, OnChanges, SimpleChange, SimpleChanges, Type} from '@angular/core'; | 
					
						
							| 
									
										
										
										
											2020-06-13 11:06:35 +03:00
										 |  |  | import {merge, Observable, ReplaySubject} from 'rxjs'; | 
					
						
							|  |  |  | import {map, switchMap} from 'rxjs/operators'; | 
					
						
							| 
									
										
										
										
											2018-02-28 09:45:11 -08:00
										 |  |  | 
 | 
					
						
							|  |  |  | import {NgElementStrategy, NgElementStrategyEvent, NgElementStrategyFactory} from './element-strategy'; | 
					
						
							|  |  |  | import {extractProjectableNodes} from './extract-projectable-nodes'; | 
					
						
							| 
									
										
										
										
											2018-03-06 14:02:25 -08:00
										 |  |  | import {isFunction, scheduler, strictEquals} from './utils'; | 
					
						
							| 
									
										
										
										
											2018-02-28 09:45:11 -08:00
										 |  |  | 
 | 
					
						
							|  |  |  | /** Time in milliseconds to wait before destroying the component ref when disconnected. */ | 
					
						
							|  |  |  | const DESTROY_DELAY = 10; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | /** | 
					
						
							| 
									
										
										
										
											2018-03-06 14:02:25 -08:00
										 |  |  |  * Factory that creates new ComponentNgElementStrategy instance. Gets the component factory with the | 
					
						
							|  |  |  |  * constructor's injector's factory resolver and passes that factory to each strategy. | 
					
						
							| 
									
										
										
										
											2018-02-28 09:45:11 -08:00
										 |  |  |  * | 
					
						
							| 
									
										
										
										
											2018-10-19 12:12:20 +01:00
										 |  |  |  * @publicApi | 
					
						
							| 
									
										
										
										
											2018-02-28 09:45:11 -08:00
										 |  |  |  */ | 
					
						
							| 
									
										
										
										
											2018-03-06 14:02:25 -08:00
										 |  |  | export class ComponentNgElementStrategyFactory implements NgElementStrategyFactory { | 
					
						
							|  |  |  |   componentFactory: ComponentFactory<any>; | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2020-05-20 11:10:23 +03:00
										 |  |  |   constructor(component: Type<any>, injector: Injector) { | 
					
						
							| 
									
										
										
										
											2018-03-06 14:02:25 -08:00
										 |  |  |     this.componentFactory = | 
					
						
							|  |  |  |         injector.get(ComponentFactoryResolver).resolveComponentFactory(component); | 
					
						
							|  |  |  |   } | 
					
						
							| 
									
										
										
										
											2018-02-28 09:45:11 -08:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2018-03-06 14:02:25 -08:00
										 |  |  |   create(injector: Injector) { | 
					
						
							|  |  |  |     return new ComponentNgElementStrategy(this.componentFactory, injector); | 
					
						
							|  |  |  |   } | 
					
						
							| 
									
										
										
										
											2018-02-28 09:45:11 -08:00
										 |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | /** | 
					
						
							|  |  |  |  * Creates and destroys a component ref using a component factory and handles change detection | 
					
						
							|  |  |  |  * in response to input changes. | 
					
						
							|  |  |  |  * | 
					
						
							| 
									
										
										
										
											2018-10-19 12:12:20 +01:00
										 |  |  |  * @publicApi | 
					
						
							| 
									
										
										
										
											2018-02-28 09:45:11 -08:00
										 |  |  |  */ | 
					
						
							| 
									
										
										
										
											2018-03-06 14:02:25 -08:00
										 |  |  | export class ComponentNgElementStrategy implements NgElementStrategy { | 
					
						
							| 
									
										
										
										
											2020-06-13 11:06:35 +03:00
										 |  |  |   // Subject of `NgElementStrategyEvent` observables corresponding to the component's outputs.
 | 
					
						
							|  |  |  |   private eventEmitters = new ReplaySubject<Observable<NgElementStrategyEvent>[]>(1); | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2018-02-28 09:45:11 -08:00
										 |  |  |   /** Merged stream of the component's output events. */ | 
					
						
							| 
									
										
										
										
											2020-06-13 11:06:35 +03:00
										 |  |  |   readonly events = this.eventEmitters.pipe(switchMap(emitters => merge(...emitters))); | 
					
						
							| 
									
										
										
										
											2018-02-28 09:45:11 -08:00
										 |  |  | 
 | 
					
						
							|  |  |  |   /** Reference to the component that was created on connect. */ | 
					
						
							| 
									
										
										
										
											2020-06-05 20:14:16 +03:00
										 |  |  |   private componentRef: ComponentRef<any>|null = null; | 
					
						
							| 
									
										
										
										
											2018-02-28 09:45:11 -08:00
										 |  |  | 
 | 
					
						
							|  |  |  |   /** Changes that have been made to the component ref since the last time onChanges was called. */ | 
					
						
							|  |  |  |   private inputChanges: SimpleChanges|null = null; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   /** Whether the created component implements the onChanges function. */ | 
					
						
							|  |  |  |   private implementsOnChanges = false; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   /** Whether a change detection has been scheduled to run on the component. */ | 
					
						
							|  |  |  |   private scheduledChangeDetectionFn: (() => void)|null = null; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   /** Callback function that when called will cancel a scheduled destruction on the component. */ | 
					
						
							|  |  |  |   private scheduledDestroyFn: (() => void)|null = null; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   /** Initial input values that were set before the component was created. */ | 
					
						
							|  |  |  |   private readonly initialInputValues = new Map<string, any>(); | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2020-03-19 13:50:25 +02:00
										 |  |  |   /** | 
					
						
							|  |  |  |    * Set of component inputs that have not yet changed, i.e. for which `ngOnChanges()` has not | 
					
						
							|  |  |  |    * fired. (This is used to determine the value of `fistChange` in `SimpleChange` instances.) | 
					
						
							|  |  |  |    */ | 
					
						
							|  |  |  |   private readonly unchangedInputs = new Set<string>(); | 
					
						
							| 
									
										
										
										
											2018-02-28 09:45:11 -08:00
										 |  |  | 
 | 
					
						
							|  |  |  |   constructor(private componentFactory: ComponentFactory<any>, private injector: Injector) {} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   /** | 
					
						
							|  |  |  |    * Initializes a new component if one has not yet been created and cancels any scheduled | 
					
						
							|  |  |  |    * destruction. | 
					
						
							|  |  |  |    */ | 
					
						
							|  |  |  |   connect(element: HTMLElement) { | 
					
						
							|  |  |  |     // If the element is marked to be destroyed, cancel the task since the component was reconnected
 | 
					
						
							|  |  |  |     if (this.scheduledDestroyFn !== null) { | 
					
						
							|  |  |  |       this.scheduledDestroyFn(); | 
					
						
							|  |  |  |       this.scheduledDestroyFn = null; | 
					
						
							|  |  |  |       return; | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2020-06-05 20:14:16 +03:00
										 |  |  |     if (this.componentRef === null) { | 
					
						
							| 
									
										
										
										
											2018-02-28 09:45:11 -08:00
										 |  |  |       this.initializeComponent(element); | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  |   } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   /** | 
					
						
							|  |  |  |    * Schedules the component to be destroyed after some small delay in case the element is just | 
					
						
							|  |  |  |    * being moved across the DOM. | 
					
						
							|  |  |  |    */ | 
					
						
							|  |  |  |   disconnect() { | 
					
						
							|  |  |  |     // Return if there is no componentRef or the component is already scheduled for destruction
 | 
					
						
							| 
									
										
										
										
											2020-06-05 20:14:16 +03:00
										 |  |  |     if (this.componentRef === null || this.scheduledDestroyFn !== null) { | 
					
						
							| 
									
										
										
										
											2018-02-28 09:45:11 -08:00
										 |  |  |       return; | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     // Schedule the component to be destroyed after a small timeout in case it is being
 | 
					
						
							|  |  |  |     // moved elsewhere in the DOM
 | 
					
						
							| 
									
										
										
										
											2018-03-01 11:26:29 -08:00
										 |  |  |     this.scheduledDestroyFn = scheduler.schedule(() => { | 
					
						
							| 
									
										
										
										
											2020-06-05 20:14:16 +03:00
										 |  |  |       if (this.componentRef !== null) { | 
					
						
							|  |  |  |         this.componentRef.destroy(); | 
					
						
							| 
									
										
										
										
											2018-03-06 14:02:25 -08:00
										 |  |  |         this.componentRef = null; | 
					
						
							| 
									
										
										
										
											2018-03-01 11:26:29 -08:00
										 |  |  |       } | 
					
						
							|  |  |  |     }, DESTROY_DELAY); | 
					
						
							| 
									
										
										
										
											2018-02-28 09:45:11 -08:00
										 |  |  |   } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   /** | 
					
						
							|  |  |  |    * Returns the component property value. If the component has not yet been created, the value is | 
					
						
							|  |  |  |    * retrieved from the cached initialization values. | 
					
						
							|  |  |  |    */ | 
					
						
							| 
									
										
										
										
											2018-03-02 10:08:16 -08:00
										 |  |  |   getInputValue(property: string): any { | 
					
						
							| 
									
										
										
										
											2020-06-05 20:14:16 +03:00
										 |  |  |     if (this.componentRef === null) { | 
					
						
							| 
									
										
										
										
											2018-02-28 09:45:11 -08:00
										 |  |  |       return this.initialInputValues.get(property); | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2020-06-05 20:14:16 +03:00
										 |  |  |     return this.componentRef.instance[property]; | 
					
						
							| 
									
										
										
										
											2018-02-28 09:45:11 -08:00
										 |  |  |   } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   /** | 
					
						
							|  |  |  |    * Sets the input value for the property. If the component has not yet been created, the value is | 
					
						
							|  |  |  |    * cached and set when the component is created. | 
					
						
							|  |  |  |    */ | 
					
						
							| 
									
										
										
										
											2018-03-02 10:08:16 -08:00
										 |  |  |   setInputValue(property: string, value: any): void { | 
					
						
							| 
									
										
										
										
											2020-06-05 20:14:16 +03:00
										 |  |  |     if (this.componentRef === null) { | 
					
						
							| 
									
										
										
										
											2019-07-17 13:39:54 +02:00
										 |  |  |       this.initialInputValues.set(property, value); | 
					
						
							| 
									
										
										
										
											2018-02-28 09:45:11 -08:00
										 |  |  |       return; | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2020-03-19 13:57:55 +02:00
										 |  |  |     // Ignore the value if it is strictly equal to the current value, except if it is `undefined`
 | 
					
						
							|  |  |  |     // and this is the first change to the value (because an explicit `undefined` _is_ strictly
 | 
					
						
							|  |  |  |     // equal to not having a value set at all, but we still need to record this as a change).
 | 
					
						
							|  |  |  |     if (strictEquals(value, this.getInputValue(property)) && | 
					
						
							|  |  |  |         !((value === undefined) && this.unchangedInputs.has(property))) { | 
					
						
							| 
									
										
										
										
											2018-02-28 09:45:11 -08:00
										 |  |  |       return; | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     this.recordInputChange(property, value); | 
					
						
							| 
									
										
										
										
											2020-06-05 20:14:16 +03:00
										 |  |  |     this.componentRef.instance[property] = value; | 
					
						
							| 
									
										
										
										
											2018-02-28 09:45:11 -08:00
										 |  |  |     this.scheduleDetectChanges(); | 
					
						
							|  |  |  |   } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   /** | 
					
						
							|  |  |  |    * Creates a new component through the component factory with the provided element host and | 
					
						
							|  |  |  |    * sets up its initial inputs, listens for outputs changes, and runs an initial change detection. | 
					
						
							|  |  |  |    */ | 
					
						
							|  |  |  |   protected initializeComponent(element: HTMLElement) { | 
					
						
							|  |  |  |     const childInjector = Injector.create({providers: [], parent: this.injector}); | 
					
						
							|  |  |  |     const projectableNodes = | 
					
						
							|  |  |  |         extractProjectableNodes(element, this.componentFactory.ngContentSelectors); | 
					
						
							|  |  |  |     this.componentRef = this.componentFactory.create(childInjector, projectableNodes, element); | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2020-06-05 20:14:16 +03:00
										 |  |  |     this.implementsOnChanges = isFunction((this.componentRef.instance as OnChanges).ngOnChanges); | 
					
						
							| 
									
										
										
										
											2018-02-28 09:45:11 -08:00
										 |  |  | 
 | 
					
						
							|  |  |  |     this.initializeInputs(); | 
					
						
							| 
									
										
										
										
											2020-06-05 20:14:16 +03:00
										 |  |  |     this.initializeOutputs(this.componentRef); | 
					
						
							| 
									
										
										
										
											2018-02-28 09:45:11 -08:00
										 |  |  | 
 | 
					
						
							|  |  |  |     this.detectChanges(); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     const applicationRef = this.injector.get<ApplicationRef>(ApplicationRef); | 
					
						
							|  |  |  |     applicationRef.attachView(this.componentRef.hostView); | 
					
						
							|  |  |  |   } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   /** Set any stored initial inputs on the component's properties. */ | 
					
						
							|  |  |  |   protected initializeInputs(): void { | 
					
						
							|  |  |  |     this.componentFactory.inputs.forEach(({propName}) => { | 
					
						
							| 
									
										
										
										
											2020-03-19 13:50:25 +02:00
										 |  |  |       if (this.implementsOnChanges) { | 
					
						
							|  |  |  |         // If the component implements `ngOnChanges()`, keep track of which inputs have never
 | 
					
						
							|  |  |  |         // changed so far.
 | 
					
						
							|  |  |  |         this.unchangedInputs.add(propName); | 
					
						
							|  |  |  |       } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2019-07-17 13:39:54 +02:00
										 |  |  |       if (this.initialInputValues.has(propName)) { | 
					
						
							| 
									
										
										
										
											2020-03-19 13:50:25 +02:00
										 |  |  |         // Call `setInputValue()` now that the component has been instantiated to update its
 | 
					
						
							|  |  |  |         // properties and fire `ngOnChanges()`.
 | 
					
						
							| 
									
										
										
										
											2019-07-17 13:39:54 +02:00
										 |  |  |         this.setInputValue(propName, this.initialInputValues.get(propName)); | 
					
						
							| 
									
										
										
										
											2018-02-28 09:45:11 -08:00
										 |  |  |       } | 
					
						
							|  |  |  |     }); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     this.initialInputValues.clear(); | 
					
						
							|  |  |  |   } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   /** Sets up listeners for the component's outputs so that the events stream emits the events. */ | 
					
						
							| 
									
										
										
										
											2020-06-05 20:14:16 +03:00
										 |  |  |   protected initializeOutputs(componentRef: ComponentRef<any>): void { | 
					
						
							| 
									
										
										
										
											2020-06-13 11:06:35 +03:00
										 |  |  |     const eventEmitters: Observable<NgElementStrategyEvent>[] = | 
					
						
							|  |  |  |         this.componentFactory.outputs.map(({propName, templateName}) => { | 
					
						
							|  |  |  |           const emitter: EventEmitter<any> = componentRef.instance[propName]; | 
					
						
							|  |  |  |           return emitter.pipe(map(value => ({name: templateName, value}))); | 
					
						
							|  |  |  |         }); | 
					
						
							| 
									
										
										
										
											2018-02-28 09:45:11 -08:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2020-06-13 11:06:35 +03:00
										 |  |  |     this.eventEmitters.next(eventEmitters); | 
					
						
							| 
									
										
										
										
											2018-02-28 09:45:11 -08:00
										 |  |  |   } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   /** Calls ngOnChanges with all the inputs that have changed since the last call. */ | 
					
						
							| 
									
										
										
										
											2020-06-05 20:14:16 +03:00
										 |  |  |   protected callNgOnChanges(componentRef: ComponentRef<any>): void { | 
					
						
							| 
									
										
										
										
											2018-02-28 09:45:11 -08:00
										 |  |  |     if (!this.implementsOnChanges || this.inputChanges === null) { | 
					
						
							|  |  |  |       return; | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2018-03-02 10:08:16 -08:00
										 |  |  |     // Cache the changes and set inputChanges to null to capture any changes that might occur
 | 
					
						
							|  |  |  |     // during ngOnChanges.
 | 
					
						
							|  |  |  |     const inputChanges = this.inputChanges; | 
					
						
							| 
									
										
										
										
											2018-02-28 09:45:11 -08:00
										 |  |  |     this.inputChanges = null; | 
					
						
							| 
									
										
										
										
											2020-06-05 20:14:16 +03:00
										 |  |  |     (componentRef.instance as OnChanges).ngOnChanges(inputChanges); | 
					
						
							| 
									
										
										
										
											2018-02-28 09:45:11 -08:00
										 |  |  |   } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   /** | 
					
						
							|  |  |  |    * Schedules change detection to run on the component. | 
					
						
							|  |  |  |    * Ignores subsequent calls if already scheduled. | 
					
						
							|  |  |  |    */ | 
					
						
							|  |  |  |   protected scheduleDetectChanges(): void { | 
					
						
							|  |  |  |     if (this.scheduledChangeDetectionFn) { | 
					
						
							|  |  |  |       return; | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     this.scheduledChangeDetectionFn = scheduler.scheduleBeforeRender(() => { | 
					
						
							|  |  |  |       this.scheduledChangeDetectionFn = null; | 
					
						
							| 
									
										
										
										
											2018-03-02 10:08:16 -08:00
										 |  |  |       this.detectChanges(); | 
					
						
							| 
									
										
										
										
											2018-02-28 09:45:11 -08:00
										 |  |  |     }); | 
					
						
							|  |  |  |   } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   /** | 
					
						
							|  |  |  |    * Records input changes so that the component receives SimpleChanges in its onChanges function. | 
					
						
							|  |  |  |    */ | 
					
						
							|  |  |  |   protected recordInputChange(property: string, currentValue: any): void { | 
					
						
							|  |  |  |     // Do not record the change if the component does not implement `OnChanges`.
 | 
					
						
							| 
									
										
										
										
											2020-06-05 20:14:16 +03:00
										 |  |  |     // (We can only determine that after the component has been instantiated.)
 | 
					
						
							|  |  |  |     if (this.componentRef !== null && !this.implementsOnChanges) { | 
					
						
							| 
									
										
										
										
											2018-02-28 09:45:11 -08:00
										 |  |  |       return; | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     if (this.inputChanges === null) { | 
					
						
							|  |  |  |       this.inputChanges = {}; | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     // If there already is a change, modify the current value to match but leave the values for
 | 
					
						
							|  |  |  |     // previousValue and isFirstChange.
 | 
					
						
							|  |  |  |     const pendingChange = this.inputChanges[property]; | 
					
						
							|  |  |  |     if (pendingChange) { | 
					
						
							|  |  |  |       pendingChange.currentValue = currentValue; | 
					
						
							|  |  |  |       return; | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2020-03-19 13:50:25 +02:00
										 |  |  |     const isFirstChange = this.unchangedInputs.has(property); | 
					
						
							|  |  |  |     this.unchangedInputs.delete(property); | 
					
						
							| 
									
										
										
										
											2018-02-28 09:45:11 -08:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2018-03-02 10:08:16 -08:00
										 |  |  |     const previousValue = isFirstChange ? undefined : this.getInputValue(property); | 
					
						
							| 
									
										
										
										
											2018-02-28 09:45:11 -08:00
										 |  |  |     this.inputChanges[property] = new SimpleChange(previousValue, currentValue, isFirstChange); | 
					
						
							|  |  |  |   } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   /** Runs change detection on the component. */ | 
					
						
							|  |  |  |   protected detectChanges(): void { | 
					
						
							| 
									
										
										
										
											2020-06-05 20:14:16 +03:00
										 |  |  |     if (this.componentRef === null) { | 
					
						
							| 
									
										
										
										
											2018-02-28 09:45:11 -08:00
										 |  |  |       return; | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2020-06-05 20:14:16 +03:00
										 |  |  |     this.callNgOnChanges(this.componentRef); | 
					
						
							|  |  |  |     this.componentRef.changeDetectorRef.detectChanges(); | 
					
						
							| 
									
										
										
										
											2018-02-28 09:45:11 -08:00
										 |  |  |   } | 
					
						
							|  |  |  | } |