Skip to content

Commit ee12e72

Browse files
karamhevery
authored andcommitted
fix(ivy): component ref injector should support change detector ref (#27107)
PR Close #27107
1 parent 3ec7c50 commit ee12e72

File tree

8 files changed

+100
-23
lines changed

8 files changed

+100
-23
lines changed

integration/_payload-limits.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
"master": {
44
"uncompressed": {
55
"runtime": 1497,
6-
"main": 181839,
6+
"main": 185238,
77
"polyfills": 59608
88
}
99
}

packages/core/src/render3/component.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -121,8 +121,8 @@ export function renderComponent<T>(
121121

122122
const renderer = rendererFactory.createRenderer(hostRNode, componentDef);
123123
const rootView: LViewData = createLViewData(
124-
renderer, createTView(-1, null, 1, 0, null, null, null), rootContext, rootFlags);
125-
rootView[INJECTOR] = opts.injector || null;
124+
renderer, createTView(-1, null, 1, 0, null, null, null), rootContext, rootFlags, undefined,
125+
opts.injector || null);
126126

127127
const oldView = enterView(rootView, null);
128128
let component: T;

packages/core/src/render3/component_ref.ts

+15-11
Original file line numberDiff line numberDiff line change
@@ -20,9 +20,10 @@ import {Type} from '../type';
2020
import {assertComponentType, assertDefined} from './assert';
2121
import {LifecycleHooksFeature, createRootComponent, createRootComponentView, createRootContext} from './component';
2222
import {getComponentDef} from './definition';
23+
import {NodeInjector} from './di';
2324
import {createLViewData, createNodeAtIndex, createTView, createViewNode, elementCreate, locateHostElement, refreshDescendantViews} from './instructions';
2425
import {ComponentDef, RenderFlags} from './interfaces/definition';
25-
import {TElementNode, TNode, TNodeType, TViewNode} from './interfaces/node';
26+
import {TContainerNode, TElementContainerNode, TElementNode, TNode, TNodeType, TViewNode} from './interfaces/node';
2627
import {RElement, RendererFactory3, domRendererFactory3} from './interfaces/renderer';
2728
import {FLAGS, HEADER_OFFSET, INJECTOR, LViewData, LViewFlags, RootContext, TVIEW} from './interfaces/view';
2829
import {enterView, leaveView} from './state';
@@ -138,10 +139,12 @@ export class ComponentFactory<T> extends viewEngine_ComponentFactory<T> {
138139
ngModule && !isInternalRootView ? ngModule.injector.get(ROOT_CONTEXT) : createRootContext();
139140

140141
const renderer = rendererFactory.createRenderer(hostRNode, this.componentDef);
142+
const rootViewInjector =
143+
ngModule ? createChainedInjector(injector, ngModule.injector) : injector;
141144
// Create the root view. Uses empty TView and ContentTemplate.
142145
const rootView: LViewData = createLViewData(
143-
renderer, createTView(-1, null, 1, 0, null, null, null), rootContext, rootFlags);
144-
rootView[INJECTOR] = ngModule ? createChainedInjector(injector, ngModule.injector) : injector;
146+
renderer, createTView(-1, null, 1, 0, null, null, null), rootContext, rootFlags, undefined,
147+
rootViewInjector);
145148

146149
// rootView is the parent when bootstrapping
147150
const oldView = enterView(rootView, null);
@@ -198,8 +201,8 @@ export class ComponentFactory<T> extends viewEngine_ComponentFactory<T> {
198201
}
199202

200203
const componentRef = new ComponentRef(
201-
this.componentType, component, rootView, injector,
202-
createElementRef(viewEngine_ElementRef, tElementNode, rootView));
204+
this.componentType, component,
205+
createElementRef(viewEngine_ElementRef, tElementNode, rootView), rootView, tElementNode);
203206

204207
if (isInternalRootView) {
205208
// 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
232235
*/
233236
export class ComponentRef<T> extends viewEngine_ComponentRef<T> {
234237
destroyCbs: (() => void)[]|null = [];
235-
injector: Injector;
236238
instance: T;
237239
hostView: ViewRef<T>;
238240
changeDetectorRef: ViewEngine_ChangeDetectorRef;
239241
componentType: Type<T>;
240242

241243
constructor(
242-
componentType: Type<T>, instance: T, rootView: LViewData, injector: Injector,
243-
public location: viewEngine_ElementRef) {
244+
componentType: Type<T>, instance: T, public location: viewEngine_ElementRef,
245+
private _rootView: LViewData,
246+
private _tNode: TElementNode|TContainerNode|TElementContainerNode) {
244247
super();
245248
this.instance = instance;
246-
this.hostView = this.changeDetectorRef = new RootViewRef<T>(rootView);
247-
this.hostView._tViewNode = createViewNode(-1, rootView);
248-
this.injector = injector;
249+
this.hostView = this.changeDetectorRef = new RootViewRef<T>(_rootView);
250+
this.hostView._tViewNode = createViewNode(-1, _rootView);
249251
this.componentType = componentType;
250252
}
251253

254+
get injector(): Injector { return new NodeInjector(this._tNode, this._rootView); }
255+
252256
destroy(): void {
253257
ngDevMode && assertDefined(this.destroyCbs, 'NgModule already destroyed');
254258
this.destroyCbs !.forEach(fn => fn());

packages/core/src/render3/instructions.ts

+5-3
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
import './ng_dev_mode';
1010
import {resolveForwardRef} from '../di/forward_ref';
1111
import {InjectionToken} from '../di/injection_token';
12+
import {Injector} from '../di/injector';
1213
import {InjectFlags} from '../di/injector_compatibility';
1314
import {QueryList} from '../linker';
1415
import {Sanitizer} from '../sanitization/security';
@@ -156,13 +157,14 @@ function refreshChildComponents(
156157

157158
export function createLViewData<T>(
158159
renderer: Renderer3, tView: TView, context: T | null, flags: LViewFlags,
159-
sanitizer?: Sanitizer | null): LViewData {
160+
sanitizer?: Sanitizer | null, injector?: Injector | null): LViewData {
160161
const viewData = getViewData();
161162
const instance = tView.blueprint.slice() as LViewData;
162163
instance[FLAGS] = flags | LViewFlags.CreationMode | LViewFlags.Attached | LViewFlags.RunInit;
163164
instance[PARENT] = instance[DECLARATION_VIEW] = viewData;
164165
instance[CONTEXT] = context;
165-
instance[INJECTOR] = viewData ? viewData[INJECTOR] : null;
166+
instance[INJECTOR as any] =
167+
injector === undefined ? (viewData ? viewData[INJECTOR] : null) : injector;
166168
instance[RENDERER] = renderer;
167169
instance[SANITIZER] = sanitizer || null;
168170
return instance;
@@ -680,7 +682,7 @@ export function createTView(
680682
// that has a host binding, we will update the blueprint with that def's hostVars count.
681683
const initialViewLength = bindingStartIndex + vars;
682684
const blueprint = createViewBlueprint(bindingStartIndex, initialViewLength);
683-
return blueprint[TVIEW] = {
685+
return blueprint[TVIEW as any] = {
684686
id: viewIndex,
685687
blueprint: blueprint,
686688
template: templateFn,

packages/core/src/render3/interfaces/view.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,7 @@ export interface LViewData extends Array<any> {
6969
* node tree in DI and get the TView.data array associated with a node (where the
7070
* directive defs are stored).
7171
*/
72-
[TVIEW]: TView;
72+
readonly[TVIEW]: TView;
7373

7474
/** Flags for this view. See LViewFlags for more info. */
7575
[FLAGS]: LViewFlags;
@@ -147,7 +147,7 @@ export interface LViewData extends Array<any> {
147147
[CONTEXT]: {}|RootContext|null;
148148

149149
/** An optional Module Injector to be used as fall back after Element Injectors are consulted. */
150-
[INJECTOR]: Injector|null;
150+
readonly[INJECTOR]: Injector|null;
151151

152152
/** Renderer to be used for this view. */
153153
[RENDERER]: Renderer3;

packages/core/src/render3/view_ref.ts

+3-1
Original file line numberDiff line numberDiff line change
@@ -275,6 +275,8 @@ export class RootViewRef<T> extends ViewRef<T> {
275275
detectChanges(): void { detectChangesInRootView(this._view); }
276276

277277
checkNoChanges(): void { checkNoChangesInRootView(this._view); }
278+
279+
get context(): T { return null !; }
278280
}
279281

280282
function collectNativeNodes(lView: LViewData, parentTNode: TNode, result: any[]): any[] {
@@ -289,4 +291,4 @@ function collectNativeNodes(lView: LViewData, parentTNode: TNode, result: any[])
289291
}
290292

291293
return result;
292-
}
294+
}

packages/core/src/testability/testability.ts

+5-2
Original file line numberDiff line numberDiff line change
@@ -67,11 +67,14 @@ export class Testability implements PublicTestability {
6767
private _didWork: boolean = false;
6868
private _callbacks: WaitCallback[] = [];
6969

70-
private taskTrackingZone: any;
70+
private taskTrackingZone: {macroTasks: Task[]}|null = null;
7171

7272
constructor(private _ngZone: NgZone) {
7373
this._watchAngularEvents();
74-
_ngZone.run(() => { this.taskTrackingZone = Zone.current.get('TaskTrackingZone'); });
74+
_ngZone.run(() => {
75+
this.taskTrackingZone =
76+
typeof Zone == 'undefined' ? null : Zone.current.get('TaskTrackingZone');
77+
});
7578
}
7679

7780
private _watchAngularEvents(): void {

packages/core/test/render3/view_container_ref_spec.ts

+67-1
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
* found in the LICENSE file at https://angular.io/license
77
*/
88

9-
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';
9+
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';
1010
import {ViewEncapsulation} from '../../src/metadata';
1111
import {AttributeMarker, NO_CHANGE, NgOnChangesFeature, defineComponent, defineDirective, definePipe, injectComponentFactoryResolver, load, query, queryRefresh} from '../../src/render3/index';
1212

@@ -1035,6 +1035,72 @@ describe('ViewContainerRef', () => {
10351035
expect(templateExecutionCounter).toEqual(5);
10361036
});
10371037

1038+
describe('ComponentRef', () => {
1039+
let dynamicComp !: DynamicComp;
1040+
1041+
class AppComp {
1042+
constructor(public vcr: ViewContainerRef, public cfr: ComponentFactoryResolver) {}
1043+
1044+
static ngComponentDef = defineComponent({
1045+
type: AppComp,
1046+
selectors: [['app-comp']],
1047+
factory:
1048+
() => new AppComp(
1049+
directiveInject(ViewContainerRef as any), injectComponentFactoryResolver()),
1050+
consts: 0,
1051+
vars: 0,
1052+
template: (rf: RenderFlags, cmp: AppComp) => {}
1053+
});
1054+
}
1055+
1056+
class DynamicComp {
1057+
doCheckCount = 0;
1058+
1059+
ngDoCheck() { this.doCheckCount++; }
1060+
1061+
static ngComponentDef = defineComponent({
1062+
type: DynamicComp,
1063+
selectors: [['dynamic-comp']],
1064+
factory: () => dynamicComp = new DynamicComp(),
1065+
consts: 0,
1066+
vars: 0,
1067+
template: (rf: RenderFlags, cmp: DynamicComp) => {}
1068+
});
1069+
}
1070+
1071+
it('should return ComponentRef with ChangeDetectorRef attached to root view', () => {
1072+
const fixture = new ComponentFixture(AppComp);
1073+
1074+
const dynamicCompFactory = fixture.component.cfr.resolveComponentFactory(DynamicComp);
1075+
const ref = fixture.component.vcr.createComponent(dynamicCompFactory);
1076+
fixture.update();
1077+
expect(dynamicComp.doCheckCount).toEqual(1);
1078+
1079+
// The change detector ref should be attached to the root view that contains
1080+
// DynamicComp, so the doCheck hook for DynamicComp should run upon ref.detectChanges().
1081+
ref.changeDetectorRef.detectChanges();
1082+
expect(dynamicComp.doCheckCount).toEqual(2);
1083+
expect((ref.changeDetectorRef as any).context).toBeNull();
1084+
});
1085+
1086+
it('should return ComponentRef that can retrieve component ChangeDetectorRef through its injector',
1087+
() => {
1088+
const fixture = new ComponentFixture(AppComp);
1089+
1090+
const dynamicCompFactory = fixture.component.cfr.resolveComponentFactory(DynamicComp);
1091+
const ref = fixture.component.vcr.createComponent(dynamicCompFactory);
1092+
fixture.update();
1093+
expect(dynamicComp.doCheckCount).toEqual(1);
1094+
1095+
// The injector should retrieve the change detector ref for DynamicComp. As such,
1096+
// the doCheck hook for DynamicComp should NOT run upon ref.detectChanges().
1097+
const changeDetector = ref.injector.get(ChangeDetectorRef);
1098+
changeDetector.detectChanges();
1099+
expect(dynamicComp.doCheckCount).toEqual(1);
1100+
expect(changeDetector.context).toEqual(dynamicComp);
1101+
});
1102+
});
1103+
10381104
class EmbeddedComponentWithNgContent {
10391105
static ngComponentDef = defineComponent({
10401106
type: EmbeddedComponentWithNgContent,

0 commit comments

Comments
 (0)