fix(ivy): component ref injector should support change detector ref (#27107)
PR Close #27107
This commit is contained in:
parent
3ec7c5081d
commit
ee12e725c0
|
@ -3,7 +3,7 @@
|
||||||
"master": {
|
"master": {
|
||||||
"uncompressed": {
|
"uncompressed": {
|
||||||
"runtime": 1497,
|
"runtime": 1497,
|
||||||
"main": 181839,
|
"main": 185238,
|
||||||
"polyfills": 59608
|
"polyfills": 59608
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -121,8 +121,8 @@ export function renderComponent<T>(
|
||||||
|
|
||||||
const renderer = rendererFactory.createRenderer(hostRNode, componentDef);
|
const renderer = rendererFactory.createRenderer(hostRNode, componentDef);
|
||||||
const rootView: LViewData = createLViewData(
|
const rootView: LViewData = createLViewData(
|
||||||
renderer, createTView(-1, null, 1, 0, null, null, null), rootContext, rootFlags);
|
renderer, createTView(-1, null, 1, 0, null, null, null), rootContext, rootFlags, undefined,
|
||||||
rootView[INJECTOR] = opts.injector || null;
|
opts.injector || null);
|
||||||
|
|
||||||
const oldView = enterView(rootView, null);
|
const oldView = enterView(rootView, null);
|
||||||
let component: T;
|
let component: T;
|
||||||
|
|
|
@ -20,9 +20,10 @@ import {Type} from '../type';
|
||||||
import {assertComponentType, assertDefined} from './assert';
|
import {assertComponentType, assertDefined} from './assert';
|
||||||
import {LifecycleHooksFeature, createRootComponent, createRootComponentView, createRootContext} from './component';
|
import {LifecycleHooksFeature, createRootComponent, createRootComponentView, createRootContext} from './component';
|
||||||
import {getComponentDef} from './definition';
|
import {getComponentDef} from './definition';
|
||||||
|
import {NodeInjector} from './di';
|
||||||
import {createLViewData, createNodeAtIndex, createTView, createViewNode, elementCreate, locateHostElement, refreshDescendantViews} from './instructions';
|
import {createLViewData, createNodeAtIndex, createTView, createViewNode, elementCreate, locateHostElement, refreshDescendantViews} from './instructions';
|
||||||
import {ComponentDef, RenderFlags} from './interfaces/definition';
|
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 {RElement, RendererFactory3, domRendererFactory3} from './interfaces/renderer';
|
||||||
import {FLAGS, HEADER_OFFSET, INJECTOR, LViewData, LViewFlags, RootContext, TVIEW} from './interfaces/view';
|
import {FLAGS, HEADER_OFFSET, INJECTOR, LViewData, LViewFlags, RootContext, TVIEW} from './interfaces/view';
|
||||||
import {enterView, leaveView} from './state';
|
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();
|
ngModule && !isInternalRootView ? ngModule.injector.get(ROOT_CONTEXT) : createRootContext();
|
||||||
|
|
||||||
const renderer = rendererFactory.createRenderer(hostRNode, this.componentDef);
|
const renderer = rendererFactory.createRenderer(hostRNode, this.componentDef);
|
||||||
|
const rootViewInjector =
|
||||||
|
ngModule ? createChainedInjector(injector, ngModule.injector) : injector;
|
||||||
// Create the root view. Uses empty TView and ContentTemplate.
|
// Create the root view. Uses empty TView and ContentTemplate.
|
||||||
const rootView: LViewData = createLViewData(
|
const rootView: LViewData = createLViewData(
|
||||||
renderer, createTView(-1, null, 1, 0, null, null, null), rootContext, rootFlags);
|
renderer, createTView(-1, null, 1, 0, null, null, null), rootContext, rootFlags, undefined,
|
||||||
rootView[INJECTOR] = ngModule ? createChainedInjector(injector, ngModule.injector) : injector;
|
rootViewInjector);
|
||||||
|
|
||||||
// rootView is the parent when bootstrapping
|
// rootView is the parent when bootstrapping
|
||||||
const oldView = enterView(rootView, null);
|
const oldView = enterView(rootView, null);
|
||||||
|
@ -198,8 +201,8 @@ export class ComponentFactory<T> extends viewEngine_ComponentFactory<T> {
|
||||||
}
|
}
|
||||||
|
|
||||||
const componentRef = new ComponentRef(
|
const componentRef = new ComponentRef(
|
||||||
this.componentType, component, rootView, injector,
|
this.componentType, component,
|
||||||
createElementRef(viewEngine_ElementRef, tElementNode, rootView));
|
createElementRef(viewEngine_ElementRef, tElementNode, rootView), rootView, tElementNode);
|
||||||
|
|
||||||
if (isInternalRootView) {
|
if (isInternalRootView) {
|
||||||
// The host element of the internal root view is attached to the component's host view node
|
// 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> {
|
export class ComponentRef<T> extends viewEngine_ComponentRef<T> {
|
||||||
destroyCbs: (() => void)[]|null = [];
|
destroyCbs: (() => void)[]|null = [];
|
||||||
injector: Injector;
|
|
||||||
instance: T;
|
instance: T;
|
||||||
hostView: ViewRef<T>;
|
hostView: ViewRef<T>;
|
||||||
changeDetectorRef: ViewEngine_ChangeDetectorRef;
|
changeDetectorRef: ViewEngine_ChangeDetectorRef;
|
||||||
componentType: Type<T>;
|
componentType: Type<T>;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
componentType: Type<T>, instance: T, rootView: LViewData, injector: Injector,
|
componentType: Type<T>, instance: T, public location: viewEngine_ElementRef,
|
||||||
public location: viewEngine_ElementRef) {
|
private _rootView: LViewData,
|
||||||
|
private _tNode: TElementNode|TContainerNode|TElementContainerNode) {
|
||||||
super();
|
super();
|
||||||
this.instance = instance;
|
this.instance = instance;
|
||||||
this.hostView = this.changeDetectorRef = new RootViewRef<T>(rootView);
|
this.hostView = this.changeDetectorRef = new RootViewRef<T>(_rootView);
|
||||||
this.hostView._tViewNode = createViewNode(-1, rootView);
|
this.hostView._tViewNode = createViewNode(-1, _rootView);
|
||||||
this.injector = injector;
|
|
||||||
this.componentType = componentType;
|
this.componentType = componentType;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get injector(): Injector { return new NodeInjector(this._tNode, this._rootView); }
|
||||||
|
|
||||||
destroy(): void {
|
destroy(): void {
|
||||||
ngDevMode && assertDefined(this.destroyCbs, 'NgModule already destroyed');
|
ngDevMode && assertDefined(this.destroyCbs, 'NgModule already destroyed');
|
||||||
this.destroyCbs !.forEach(fn => fn());
|
this.destroyCbs !.forEach(fn => fn());
|
||||||
|
|
|
@ -9,6 +9,7 @@
|
||||||
import './ng_dev_mode';
|
import './ng_dev_mode';
|
||||||
import {resolveForwardRef} from '../di/forward_ref';
|
import {resolveForwardRef} from '../di/forward_ref';
|
||||||
import {InjectionToken} from '../di/injection_token';
|
import {InjectionToken} from '../di/injection_token';
|
||||||
|
import {Injector} from '../di/injector';
|
||||||
import {InjectFlags} from '../di/injector_compatibility';
|
import {InjectFlags} from '../di/injector_compatibility';
|
||||||
import {QueryList} from '../linker';
|
import {QueryList} from '../linker';
|
||||||
import {Sanitizer} from '../sanitization/security';
|
import {Sanitizer} from '../sanitization/security';
|
||||||
|
@ -156,13 +157,14 @@ function refreshChildComponents(
|
||||||
|
|
||||||
export function createLViewData<T>(
|
export function createLViewData<T>(
|
||||||
renderer: Renderer3, tView: TView, context: T | null, flags: LViewFlags,
|
renderer: Renderer3, tView: TView, context: T | null, flags: LViewFlags,
|
||||||
sanitizer?: Sanitizer | null): LViewData {
|
sanitizer?: Sanitizer | null, injector?: Injector | null): LViewData {
|
||||||
const viewData = getViewData();
|
const viewData = getViewData();
|
||||||
const instance = tView.blueprint.slice() as LViewData;
|
const instance = tView.blueprint.slice() as LViewData;
|
||||||
instance[FLAGS] = flags | LViewFlags.CreationMode | LViewFlags.Attached | LViewFlags.RunInit;
|
instance[FLAGS] = flags | LViewFlags.CreationMode | LViewFlags.Attached | LViewFlags.RunInit;
|
||||||
instance[PARENT] = instance[DECLARATION_VIEW] = viewData;
|
instance[PARENT] = instance[DECLARATION_VIEW] = viewData;
|
||||||
instance[CONTEXT] = context;
|
instance[CONTEXT] = context;
|
||||||
instance[INJECTOR] = viewData ? viewData[INJECTOR] : null;
|
instance[INJECTOR as any] =
|
||||||
|
injector === undefined ? (viewData ? viewData[INJECTOR] : null) : injector;
|
||||||
instance[RENDERER] = renderer;
|
instance[RENDERER] = renderer;
|
||||||
instance[SANITIZER] = sanitizer || null;
|
instance[SANITIZER] = sanitizer || null;
|
||||||
return instance;
|
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.
|
// that has a host binding, we will update the blueprint with that def's hostVars count.
|
||||||
const initialViewLength = bindingStartIndex + vars;
|
const initialViewLength = bindingStartIndex + vars;
|
||||||
const blueprint = createViewBlueprint(bindingStartIndex, initialViewLength);
|
const blueprint = createViewBlueprint(bindingStartIndex, initialViewLength);
|
||||||
return blueprint[TVIEW] = {
|
return blueprint[TVIEW as any] = {
|
||||||
id: viewIndex,
|
id: viewIndex,
|
||||||
blueprint: blueprint,
|
blueprint: blueprint,
|
||||||
template: templateFn,
|
template: templateFn,
|
||||||
|
|
|
@ -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
|
* node tree in DI and get the TView.data array associated with a node (where the
|
||||||
* directive defs are stored).
|
* directive defs are stored).
|
||||||
*/
|
*/
|
||||||
[TVIEW]: TView;
|
readonly[TVIEW]: TView;
|
||||||
|
|
||||||
/** Flags for this view. See LViewFlags for more info. */
|
/** Flags for this view. See LViewFlags for more info. */
|
||||||
[FLAGS]: LViewFlags;
|
[FLAGS]: LViewFlags;
|
||||||
|
@ -147,7 +147,7 @@ export interface LViewData extends Array<any> {
|
||||||
[CONTEXT]: {}|RootContext|null;
|
[CONTEXT]: {}|RootContext|null;
|
||||||
|
|
||||||
/** An optional Module Injector to be used as fall back after Element Injectors are consulted. */
|
/** 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 to be used for this view. */
|
||||||
[RENDERER]: Renderer3;
|
[RENDERER]: Renderer3;
|
||||||
|
|
|
@ -275,6 +275,8 @@ export class RootViewRef<T> extends ViewRef<T> {
|
||||||
detectChanges(): void { detectChangesInRootView(this._view); }
|
detectChanges(): void { detectChangesInRootView(this._view); }
|
||||||
|
|
||||||
checkNoChanges(): void { checkNoChangesInRootView(this._view); }
|
checkNoChanges(): void { checkNoChangesInRootView(this._view); }
|
||||||
|
|
||||||
|
get context(): T { return null !; }
|
||||||
}
|
}
|
||||||
|
|
||||||
function collectNativeNodes(lView: LViewData, parentTNode: TNode, result: any[]): any[] {
|
function collectNativeNodes(lView: LViewData, parentTNode: TNode, result: any[]): any[] {
|
||||||
|
@ -289,4 +291,4 @@ function collectNativeNodes(lView: LViewData, parentTNode: TNode, result: any[])
|
||||||
}
|
}
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
|
@ -67,11 +67,14 @@ export class Testability implements PublicTestability {
|
||||||
private _didWork: boolean = false;
|
private _didWork: boolean = false;
|
||||||
private _callbacks: WaitCallback[] = [];
|
private _callbacks: WaitCallback[] = [];
|
||||||
|
|
||||||
private taskTrackingZone: any;
|
private taskTrackingZone: {macroTasks: Task[]}|null = null;
|
||||||
|
|
||||||
constructor(private _ngZone: NgZone) {
|
constructor(private _ngZone: NgZone) {
|
||||||
this._watchAngularEvents();
|
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 {
|
private _watchAngularEvents(): void {
|
||||||
|
|
|
@ -6,7 +6,7 @@
|
||||||
* found in the LICENSE file at https://angular.io/license
|
* 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 {ViewEncapsulation} from '../../src/metadata';
|
||||||
import {AttributeMarker, NO_CHANGE, NgOnChangesFeature, defineComponent, defineDirective, definePipe, injectComponentFactoryResolver, load, query, queryRefresh} from '../../src/render3/index';
|
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);
|
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 {
|
class EmbeddedComponentWithNgContent {
|
||||||
static ngComponentDef = defineComponent({
|
static ngComponentDef = defineComponent({
|
||||||
type: EmbeddedComponentWithNgContent,
|
type: EmbeddedComponentWithNgContent,
|
||||||
|
|
Loading…
Reference in New Issue