Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Closed
wants to merge 3 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
2 changes: 1 addition & 1 deletion integration/_payload-limits.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
"master": {
"uncompressed": {
"runtime": 1497,
"main": 181839,
"main": 185238,
"polyfills": 59608
}
}
Expand Down
4 changes: 2 additions & 2 deletions packages/core/src/render3/component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
26 changes: 15 additions & 11 deletions packages/core/src/render3/component_ref.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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());
Expand Down
8 changes: 5 additions & 3 deletions packages/core/src/render3/instructions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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;
kara marked this conversation as resolved.
Show resolved Hide resolved
instance[RENDERER] = renderer;
instance[SANITIZER] = sanitizer || null;
return instance;
Expand Down Expand Up @@ -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,
Expand Down
4 changes: 2 additions & 2 deletions packages/core/src/render3/interfaces/view.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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;
kara marked this conversation as resolved.
Show resolved Hide resolved

/** Renderer to be used for this view. */
[RENDERER]: Renderer3;
Expand Down
4 changes: 3 additions & 1 deletion packages/core/src/render3/view_ref.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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[] {
Expand All @@ -289,4 +291,4 @@ function collectNativeNodes(lView: LViewData, parentTNode: TNode, result: any[])
}

return result;
}
}
7 changes: 5 additions & 2 deletions packages/core/src/testability/testability.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
68 changes: 67 additions & 1 deletion packages/core/test/render3/view_container_ref_spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand Down Expand Up @@ -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,
Expand Down