fix(ivy): component ref injector should support change detector ref (#27107)

PR Close #27107
This commit is contained in:
Kara Erickson 2018-11-14 19:25:03 -08:00 committed by Misko Hevery
parent 3ec7c5081d
commit ee12e725c0
8 changed files with 100 additions and 23 deletions

View File

@ -3,7 +3,7 @@
"master": {
"uncompressed": {
"runtime": 1497,
"main": 181839,
"main": 185238,
"polyfills": 59608
}
}

View File

@ -121,8 +121,8 @@ export function renderComponent<T>(
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;

View File

@ -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<T> extends viewEngine_ComponentFactory<T> {
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<T> extends viewEngine_ComponentFactory<T> {
}
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<T> extends viewEngine_ComponentRef<T> {
destroyCbs: (() => void)[]|null = [];
injector: Injector;
instance: T;
hostView: ViewRef<T>;
changeDetectorRef: ViewEngine_ChangeDetectorRef;
componentType: Type<T>;
constructor(
componentType: Type<T>, instance: T, rootView: LViewData, injector: Injector,
public location: viewEngine_ElementRef) {
componentType: Type<T>, instance: T, public location: viewEngine_ElementRef,
private _rootView: LViewData,
private _tNode: TElementNode|TContainerNode|TElementContainerNode) {
super();
this.instance = instance;
this.hostView = this.changeDetectorRef = new RootViewRef<T>(rootView);
this.hostView._tViewNode = createViewNode(-1, rootView);
this.injector = injector;
this.hostView = this.changeDetectorRef = new RootViewRef<T>(_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());

View File

@ -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<T>(
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,

View File

@ -69,7 +69,7 @@ export interface LViewData extends Array<any> {
* 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<any> {
[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;

View File

@ -275,6 +275,8 @@ export class RootViewRef<T> extends ViewRef<T> {
detectChanges(): void { detectChangesInRootView(this._view); }
checkNoChanges(): void { checkNoChangesInRootView(this._view); }
get context(): T { return null !; }
}
function collectNativeNodes(lView: LViewData, parentTNode: TNode, result: any[]): any[] {

View File

@ -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 {

View File

@ -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,