From ee12e725c03479a63226248ee5e80337a54a31c5 Mon Sep 17 00:00:00 2001 From: Kara Erickson Date: Wed, 14 Nov 2018 19:25:03 -0800 Subject: [PATCH] fix(ivy): component ref injector should support change detector ref (#27107) PR Close #27107 --- integration/_payload-limits.json | 2 +- packages/core/src/render3/component.ts | 4 +- packages/core/src/render3/component_ref.ts | 26 ++++--- packages/core/src/render3/instructions.ts | 8 ++- packages/core/src/render3/interfaces/view.ts | 4 +- packages/core/src/render3/view_ref.ts | 4 +- packages/core/src/testability/testability.ts | 7 +- .../test/render3/view_container_ref_spec.ts | 68 ++++++++++++++++++- 8 files changed, 100 insertions(+), 23 deletions(-) diff --git a/integration/_payload-limits.json b/integration/_payload-limits.json index 9fcce452d0..893916586f 100644 --- a/integration/_payload-limits.json +++ b/integration/_payload-limits.json @@ -3,7 +3,7 @@ "master": { "uncompressed": { "runtime": 1497, - "main": 181839, + "main": 185238, "polyfills": 59608 } } diff --git a/packages/core/src/render3/component.ts b/packages/core/src/render3/component.ts index 3f879bb545..c900f17589 100644 --- a/packages/core/src/render3/component.ts +++ b/packages/core/src/render3/component.ts @@ -121,8 +121,8 @@ export function renderComponent( const renderer = rendererFactory.createRenderer(hostRNode, componentDef); const rootView: LViewData = createLViewData( - renderer, createTView(-1, null, 1, 0, null, null, null), rootContext, rootFlags); - rootView[INJECTOR] = opts.injector || null; + renderer, createTView(-1, null, 1, 0, null, null, null), rootContext, rootFlags, undefined, + opts.injector || null); const oldView = enterView(rootView, null); let component: T; diff --git a/packages/core/src/render3/component_ref.ts b/packages/core/src/render3/component_ref.ts index b7cda56062..03ff592959 100644 --- a/packages/core/src/render3/component_ref.ts +++ b/packages/core/src/render3/component_ref.ts @@ -20,9 +20,10 @@ import {Type} from '../type'; import {assertComponentType, assertDefined} from './assert'; import {LifecycleHooksFeature, createRootComponent, createRootComponentView, createRootContext} from './component'; import {getComponentDef} from './definition'; +import {NodeInjector} from './di'; import {createLViewData, createNodeAtIndex, createTView, createViewNode, elementCreate, locateHostElement, refreshDescendantViews} from './instructions'; import {ComponentDef, RenderFlags} from './interfaces/definition'; -import {TElementNode, TNode, TNodeType, TViewNode} from './interfaces/node'; +import {TContainerNode, TElementContainerNode, TElementNode, TNode, TNodeType, TViewNode} from './interfaces/node'; import {RElement, RendererFactory3, domRendererFactory3} from './interfaces/renderer'; import {FLAGS, HEADER_OFFSET, INJECTOR, LViewData, LViewFlags, RootContext, TVIEW} from './interfaces/view'; import {enterView, leaveView} from './state'; @@ -138,10 +139,12 @@ export class ComponentFactory extends viewEngine_ComponentFactory { ngModule && !isInternalRootView ? ngModule.injector.get(ROOT_CONTEXT) : createRootContext(); const renderer = rendererFactory.createRenderer(hostRNode, this.componentDef); + const rootViewInjector = + ngModule ? createChainedInjector(injector, ngModule.injector) : injector; // Create the root view. Uses empty TView and ContentTemplate. const rootView: LViewData = createLViewData( - renderer, createTView(-1, null, 1, 0, null, null, null), rootContext, rootFlags); - rootView[INJECTOR] = ngModule ? createChainedInjector(injector, ngModule.injector) : injector; + renderer, createTView(-1, null, 1, 0, null, null, null), rootContext, rootFlags, undefined, + rootViewInjector); // rootView is the parent when bootstrapping const oldView = enterView(rootView, null); @@ -198,8 +201,8 @@ export class ComponentFactory extends viewEngine_ComponentFactory { } const componentRef = new ComponentRef( - this.componentType, component, rootView, injector, - createElementRef(viewEngine_ElementRef, tElementNode, rootView)); + this.componentType, component, + createElementRef(viewEngine_ElementRef, tElementNode, rootView), rootView, tElementNode); if (isInternalRootView) { // The host element of the internal root view is attached to the component's host view node @@ -232,23 +235,24 @@ export function injectComponentFactoryResolver(): viewEngine_ComponentFactoryRes */ export class ComponentRef extends viewEngine_ComponentRef { destroyCbs: (() => void)[]|null = []; - injector: Injector; instance: T; hostView: ViewRef; changeDetectorRef: ViewEngine_ChangeDetectorRef; componentType: Type; constructor( - componentType: Type, instance: T, rootView: LViewData, injector: Injector, - public location: viewEngine_ElementRef) { + componentType: Type, instance: T, public location: viewEngine_ElementRef, + private _rootView: LViewData, + private _tNode: TElementNode|TContainerNode|TElementContainerNode) { super(); this.instance = instance; - this.hostView = this.changeDetectorRef = new RootViewRef(rootView); - this.hostView._tViewNode = createViewNode(-1, rootView); - this.injector = injector; + this.hostView = this.changeDetectorRef = new RootViewRef(_rootView); + this.hostView._tViewNode = createViewNode(-1, _rootView); this.componentType = componentType; } + get injector(): Injector { return new NodeInjector(this._tNode, this._rootView); } + destroy(): void { ngDevMode && assertDefined(this.destroyCbs, 'NgModule already destroyed'); this.destroyCbs !.forEach(fn => fn()); diff --git a/packages/core/src/render3/instructions.ts b/packages/core/src/render3/instructions.ts index c4b34fab27..20a8b3cb20 100644 --- a/packages/core/src/render3/instructions.ts +++ b/packages/core/src/render3/instructions.ts @@ -9,6 +9,7 @@ import './ng_dev_mode'; import {resolveForwardRef} from '../di/forward_ref'; import {InjectionToken} from '../di/injection_token'; +import {Injector} from '../di/injector'; import {InjectFlags} from '../di/injector_compatibility'; import {QueryList} from '../linker'; import {Sanitizer} from '../sanitization/security'; @@ -156,13 +157,14 @@ function refreshChildComponents( export function createLViewData( renderer: Renderer3, tView: TView, context: T | null, flags: LViewFlags, - sanitizer?: Sanitizer | null): LViewData { + sanitizer?: Sanitizer | null, injector?: Injector | null): LViewData { const viewData = getViewData(); const instance = tView.blueprint.slice() as LViewData; instance[FLAGS] = flags | LViewFlags.CreationMode | LViewFlags.Attached | LViewFlags.RunInit; instance[PARENT] = instance[DECLARATION_VIEW] = viewData; instance[CONTEXT] = context; - instance[INJECTOR] = viewData ? viewData[INJECTOR] : null; + instance[INJECTOR as any] = + injector === undefined ? (viewData ? viewData[INJECTOR] : null) : injector; instance[RENDERER] = renderer; instance[SANITIZER] = sanitizer || null; return instance; @@ -680,7 +682,7 @@ export function createTView( // that has a host binding, we will update the blueprint with that def's hostVars count. const initialViewLength = bindingStartIndex + vars; const blueprint = createViewBlueprint(bindingStartIndex, initialViewLength); - return blueprint[TVIEW] = { + return blueprint[TVIEW as any] = { id: viewIndex, blueprint: blueprint, template: templateFn, diff --git a/packages/core/src/render3/interfaces/view.ts b/packages/core/src/render3/interfaces/view.ts index 1b2a6162ed..64e7d32a82 100644 --- a/packages/core/src/render3/interfaces/view.ts +++ b/packages/core/src/render3/interfaces/view.ts @@ -69,7 +69,7 @@ export interface LViewData extends Array { * node tree in DI and get the TView.data array associated with a node (where the * directive defs are stored). */ - [TVIEW]: TView; + readonly[TVIEW]: TView; /** Flags for this view. See LViewFlags for more info. */ [FLAGS]: LViewFlags; @@ -147,7 +147,7 @@ export interface LViewData extends Array { [CONTEXT]: {}|RootContext|null; /** An optional Module Injector to be used as fall back after Element Injectors are consulted. */ - [INJECTOR]: Injector|null; + readonly[INJECTOR]: Injector|null; /** Renderer to be used for this view. */ [RENDERER]: Renderer3; diff --git a/packages/core/src/render3/view_ref.ts b/packages/core/src/render3/view_ref.ts index ec1066dab0..a5551fb7aa 100644 --- a/packages/core/src/render3/view_ref.ts +++ b/packages/core/src/render3/view_ref.ts @@ -275,6 +275,8 @@ export class RootViewRef extends ViewRef { detectChanges(): void { detectChangesInRootView(this._view); } checkNoChanges(): void { checkNoChangesInRootView(this._view); } + + get context(): T { return null !; } } function collectNativeNodes(lView: LViewData, parentTNode: TNode, result: any[]): any[] { @@ -289,4 +291,4 @@ function collectNativeNodes(lView: LViewData, parentTNode: TNode, result: any[]) } return result; -} \ No newline at end of file +} diff --git a/packages/core/src/testability/testability.ts b/packages/core/src/testability/testability.ts index f40bdd889f..229ca4f484 100644 --- a/packages/core/src/testability/testability.ts +++ b/packages/core/src/testability/testability.ts @@ -67,11 +67,14 @@ export class Testability implements PublicTestability { private _didWork: boolean = false; private _callbacks: WaitCallback[] = []; - private taskTrackingZone: any; + private taskTrackingZone: {macroTasks: Task[]}|null = null; constructor(private _ngZone: NgZone) { this._watchAngularEvents(); - _ngZone.run(() => { this.taskTrackingZone = Zone.current.get('TaskTrackingZone'); }); + _ngZone.run(() => { + this.taskTrackingZone = + typeof Zone == 'undefined' ? null : Zone.current.get('TaskTrackingZone'); + }); } private _watchAngularEvents(): void { diff --git a/packages/core/test/render3/view_container_ref_spec.ts b/packages/core/test/render3/view_container_ref_spec.ts index 009396bdae..8e43454e8f 100644 --- a/packages/core/test/render3/view_container_ref_spec.ts +++ b/packages/core/test/render3/view_container_ref_spec.ts @@ -6,7 +6,7 @@ * found in the LICENSE file at https://angular.io/license */ -import {Component as _Component, ComponentFactoryResolver, ElementRef, EmbeddedViewRef, NgModuleRef, Pipe, PipeTransform, QueryList, RendererFactory2, TemplateRef, ViewContainerRef, createInjector, defineInjector, ɵAPP_ROOT as APP_ROOT, ɵNgModuleDef as NgModuleDef} from '../../src/core'; +import {ChangeDetectorRef, Component as _Component, ComponentFactoryResolver, ElementRef, EmbeddedViewRef, NgModuleRef, Pipe, PipeTransform, QueryList, RendererFactory2, TemplateRef, ViewContainerRef, createInjector, defineInjector, ɵAPP_ROOT as APP_ROOT, ɵNgModuleDef as NgModuleDef} from '../../src/core'; import {ViewEncapsulation} from '../../src/metadata'; import {AttributeMarker, NO_CHANGE, NgOnChangesFeature, defineComponent, defineDirective, definePipe, injectComponentFactoryResolver, load, query, queryRefresh} from '../../src/render3/index'; @@ -1035,6 +1035,72 @@ describe('ViewContainerRef', () => { expect(templateExecutionCounter).toEqual(5); }); + describe('ComponentRef', () => { + let dynamicComp !: DynamicComp; + + class AppComp { + constructor(public vcr: ViewContainerRef, public cfr: ComponentFactoryResolver) {} + + static ngComponentDef = defineComponent({ + type: AppComp, + selectors: [['app-comp']], + factory: + () => new AppComp( + directiveInject(ViewContainerRef as any), injectComponentFactoryResolver()), + consts: 0, + vars: 0, + template: (rf: RenderFlags, cmp: AppComp) => {} + }); + } + + class DynamicComp { + doCheckCount = 0; + + ngDoCheck() { this.doCheckCount++; } + + static ngComponentDef = defineComponent({ + type: DynamicComp, + selectors: [['dynamic-comp']], + factory: () => dynamicComp = new DynamicComp(), + consts: 0, + vars: 0, + template: (rf: RenderFlags, cmp: DynamicComp) => {} + }); + } + + it('should return ComponentRef with ChangeDetectorRef attached to root view', () => { + const fixture = new ComponentFixture(AppComp); + + const dynamicCompFactory = fixture.component.cfr.resolveComponentFactory(DynamicComp); + const ref = fixture.component.vcr.createComponent(dynamicCompFactory); + fixture.update(); + expect(dynamicComp.doCheckCount).toEqual(1); + + // The change detector ref should be attached to the root view that contains + // DynamicComp, so the doCheck hook for DynamicComp should run upon ref.detectChanges(). + ref.changeDetectorRef.detectChanges(); + expect(dynamicComp.doCheckCount).toEqual(2); + expect((ref.changeDetectorRef as any).context).toBeNull(); + }); + + it('should return ComponentRef that can retrieve component ChangeDetectorRef through its injector', + () => { + const fixture = new ComponentFixture(AppComp); + + const dynamicCompFactory = fixture.component.cfr.resolveComponentFactory(DynamicComp); + const ref = fixture.component.vcr.createComponent(dynamicCompFactory); + fixture.update(); + expect(dynamicComp.doCheckCount).toEqual(1); + + // The injector should retrieve the change detector ref for DynamicComp. As such, + // the doCheck hook for DynamicComp should NOT run upon ref.detectChanges(). + const changeDetector = ref.injector.get(ChangeDetectorRef); + changeDetector.detectChanges(); + expect(dynamicComp.doCheckCount).toEqual(1); + expect(changeDetector.context).toEqual(dynamicComp); + }); + }); + class EmbeddedComponentWithNgContent { static ngComponentDef = defineComponent({ type: EmbeddedComponentWithNgContent,