diff --git a/goldens/size-tracking/aio-payloads.json b/goldens/size-tracking/aio-payloads.json index 4dbcab3374..9d240aea2f 100755 --- a/goldens/size-tracking/aio-payloads.json +++ b/goldens/size-tracking/aio-payloads.json @@ -12,7 +12,7 @@ "master": { "uncompressed": { "runtime-es2015": 2987, - "main-es2015": 450301, + "main-es2015": 450596, "polyfills-es2015": 52630 } } @@ -21,7 +21,7 @@ "master": { "uncompressed": { "runtime-es2015": 3097, - "main-es2015": 429200, + "main-es2015": 429885, "polyfills-es2015": 52195 } } diff --git a/packages/elements/src/component-factory-strategy.ts b/packages/elements/src/component-factory-strategy.ts index 799cfba683..35db08958a 100644 --- a/packages/elements/src/component-factory-strategy.ts +++ b/packages/elements/src/component-factory-strategy.ts @@ -6,7 +6,7 @@ * found in the LICENSE file at https://angular.io/license */ -import {ApplicationRef, ComponentFactory, ComponentFactoryResolver, ComponentRef, EventEmitter, Injector, OnChanges, SimpleChange, SimpleChanges, Type} from '@angular/core'; +import {ApplicationRef, ComponentFactory, ComponentFactoryResolver, ComponentRef, EventEmitter, Injector, NgZone, OnChanges, SimpleChange, SimpleChanges, Type} from '@angular/core'; import {merge, Observable, ReplaySubject} from 'rxjs'; import {map, switchMap} from 'rxjs/operators'; @@ -73,6 +73,13 @@ export class ComponentNgElementStrategy implements NgElementStrategy { */ private readonly unchangedInputs = new Set(); + /** Service for setting zone context. */ + private readonly ngZone = this.injector.get(NgZone); + + /** The zone the element was created in or `null` if Zone.js is not loaded. */ + private readonly elementZone = + (typeof Zone === 'undefined') ? null : this.ngZone.run(() => Zone.current); + constructor(private componentFactory: ComponentFactory, private injector: Injector) {} /** @@ -80,16 +87,19 @@ export class ComponentNgElementStrategy implements NgElementStrategy { * 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; - } + this.runInZone(() => { + // 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; + } - if (this.componentRef === null) { - this.initializeComponent(element); - } + if (this.componentRef === null) { + this.initializeComponent(element); + } + }); } /** @@ -97,19 +107,21 @@ export class ComponentNgElementStrategy implements NgElementStrategy { * being moved across the DOM. */ disconnect() { - // Return if there is no componentRef or the component is already scheduled for destruction - if (this.componentRef === null || this.scheduledDestroyFn !== null) { - return; - } - - // Schedule the component to be destroyed after a small timeout in case it is being - // moved elsewhere in the DOM - this.scheduledDestroyFn = scheduler.schedule(() => { - if (this.componentRef !== null) { - this.componentRef.destroy(); - this.componentRef = null; + this.runInZone(() => { + // Return if there is no componentRef or the component is already scheduled for destruction + if (this.componentRef === null || this.scheduledDestroyFn !== null) { + return; } - }, DESTROY_DELAY); + + // Schedule the component to be destroyed after a small timeout in case it is being + // moved elsewhere in the DOM + this.scheduledDestroyFn = scheduler.schedule(() => { + if (this.componentRef !== null) { + this.componentRef.destroy(); + this.componentRef = null; + } + }, DESTROY_DELAY); + }); } /** @@ -117,11 +129,13 @@ export class ComponentNgElementStrategy implements NgElementStrategy { * retrieved from the cached initialization values. */ getInputValue(property: string): any { - if (this.componentRef === null) { - return this.initialInputValues.get(property); - } + return this.runInZone(() => { + if (this.componentRef === null) { + return this.initialInputValues.get(property); + } - return this.componentRef.instance[property]; + return this.componentRef.instance[property]; + }); } /** @@ -129,22 +143,24 @@ export class ComponentNgElementStrategy implements NgElementStrategy { * cached and set when the component is created. */ setInputValue(property: string, value: any): void { - if (this.componentRef === null) { - this.initialInputValues.set(property, value); - return; - } + this.runInZone(() => { + if (this.componentRef === null) { + this.initialInputValues.set(property, value); + return; + } - // 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))) { - return; - } + // 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))) { + return; + } - this.recordInputChange(property, value); - this.componentRef.instance[property] = value; - this.scheduleDetectChanges(); + this.recordInputChange(property, value); + this.componentRef.instance[property] = value; + this.scheduleDetectChanges(); + }); } /** @@ -264,4 +280,9 @@ export class ComponentNgElementStrategy implements NgElementStrategy { this.callNgOnChanges(this.componentRef); this.componentRef.changeDetectorRef.detectChanges(); } + + /** Runs in the angular zone, if present. */ + private runInZone(fn: () => unknown) { + return (this.elementZone && Zone.current !== this.elementZone) ? this.ngZone.run(fn) : fn(); + } } diff --git a/packages/elements/test/component-factory-strategy_spec.ts b/packages/elements/test/component-factory-strategy_spec.ts index 53c9fbd0fc..577392fff2 100644 --- a/packages/elements/test/component-factory-strategy_spec.ts +++ b/packages/elements/test/component-factory-strategy_spec.ts @@ -6,7 +6,7 @@ * found in the LICENSE file at https://angular.io/license */ -import {ComponentFactory, ComponentRef, Injector, NgModuleRef, SimpleChange, SimpleChanges, Type} from '@angular/core'; +import {ApplicationRef, ComponentFactory, ComponentFactoryResolver, ComponentRef, Injector, NgModuleRef, NgZone, SimpleChange, SimpleChanges, Type} from '@angular/core'; import {fakeAsync, tick} from '@angular/core/testing'; import {Subject} from 'rxjs'; @@ -20,22 +20,40 @@ describe('ComponentFactoryNgElementStrategy', () => { let injector: any; let componentRef: any; let applicationRef: any; + let ngZone: any; + + let injectables: Map; beforeEach(() => { factory = new FakeComponentFactory(); componentRef = factory.componentRef; applicationRef = jasmine.createSpyObj('applicationRef', ['attachView']); + + ngZone = jasmine.createSpyObj('ngZone', ['run']); + ngZone.run.and.callFake((fn: () => unknown) => fn()); + injector = jasmine.createSpyObj('injector', ['get']); - injector.get.and.returnValue(applicationRef); + injector.get.and.callFake((token: unknown) => { + if (!injectables.has(token)) { + throw new Error(`Failed to get injectable from mock injector: ${token}`); + } + return injectables.get(token); + }); + + injectables = new Map([ + [ApplicationRef, applicationRef], + [NgZone, ngZone], + ]); strategy = new ComponentNgElementStrategy(factory, injector); + ngZone.run.calls.reset(); }); it('should create a new strategy from the factory', () => { const factoryResolver = jasmine.createSpyObj('factoryResolver', ['resolveComponentFactory']); factoryResolver.resolveComponentFactory.and.returnValue(factory); - injector.get.and.returnValue(factoryResolver); + injectables.set(ComponentFactoryResolver, factoryResolver); const strategyFactory = new ComponentNgElementStrategyFactory(FakeComponent, injector); expect(strategyFactory.create(injector)).toBeTruthy(); @@ -266,6 +284,30 @@ describe('ComponentFactoryNgElementStrategy', () => { expect(componentRef.destroy).toHaveBeenCalledTimes(1); })); }); + + describe('runInZone', () => { + const param = 'foofoo'; + const fn = () => param; + + it('should run the callback directly when invoked in element\'s zone', () => { + expect(strategy['runInZone'](fn)).toEqual('foofoo'); + expect(ngZone.run).not.toHaveBeenCalled(); + }); + + it('should run the callback inside the element\'s zone when invoked in a different zone', + () => { + expect(Zone.root.run(() => (strategy['runInZone'](fn)))).toEqual('foofoo'); + expect(ngZone.run).toHaveBeenCalledWith(fn); + }); + + it('should run the callback directly when called without zone.js loaded', () => { + // simulate no zone.js loaded + (strategy as any)['elementZone'] = null; + + expect(Zone.root.run(() => (strategy['runInZone'](fn)))).toEqual('foofoo'); + expect(ngZone.run).not.toHaveBeenCalled(); + }); + }); }); export class FakeComponentWithoutNgOnChanges {