Alex Rickabaugh 6472661ae8 refactor(ivy): avoid circular dep with query/di/instructions (#21430)
To prepare for pending ngForOf work, the dep from instructions -> query
should be broken. This will enable a dep from di -> instructions while
avoiding a di -> instructions -> query -> di cycle.

Analyzing this cycle also uncovered another problem: the implementation
of query() breaks tree-shaking through a hard dependency on DI concepts
of TemplateRef, ElementRef, ViewContainerRef. This is fundamentally due
to how query() can query for those values without any configuration.

Instead, this fix introduces the concept by employing the strategy
pattern, and redefining QueryReadType to pass a function which will
return one of the above values. This strategy is then used for 'read'
instead of an enum in cases where special values should be read from
the DI system.

PR Close #21430
2018-01-23 12:54:39 -08:00

294 lines
9.6 KiB
TypeScript

/**
* @license
* Copyright Google Inc. All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
import {ElementRef, TemplateRef, ViewContainerRef} from '@angular/core';
import {bloomAdd, bloomFindPossibleInjector, getOrCreateNodeInjector} from '../../src/render3/di';
import {C, E, PublicFeature, T, V, b, b2, cR, cr, defineDirective, e, inject, injectElementRef, injectTemplateRef, injectViewContainerRef, m, t, v} from '../../src/render3/index';
import {createLNode, createLView, enterView, leaveView} from '../../src/render3/instructions';
import {LInjector} from '../../src/render3/interfaces/injector';
import {LNodeFlags} from '../../src/render3/interfaces/node';
import {renderToHtml} from './render_util';
describe('di', () => {
describe('no dependencies', () => {
it('should create directive with no deps', () => {
class Directive {
value: string = 'Created';
static ngDirectiveDef = defineDirective({factory: () => new Directive});
}
function Template(ctx: any, cm: boolean) {
if (cm) {
E(0, 'div', null, [Directive]);
{ T(2); }
e();
}
t(2, b(m<Directive>(1).value));
}
expect(renderToHtml(Template, {})).toEqual('<div>Created</div>');
});
});
describe('view dependencies', () => {
it('should create directive with inter view dependencies', () => {
class DirectiveA {
value: string = 'A';
static ngDirectiveDef =
defineDirective({factory: () => new DirectiveA, features: [PublicFeature]});
}
class DirectiveB {
value: string = 'B';
static ngDirectiveDef =
defineDirective({factory: () => new DirectiveB, features: [PublicFeature]});
}
class DirectiveC {
value: string;
constructor(a: DirectiveA, b: DirectiveB) { this.value = a.value + b.value; }
static ngDirectiveDef = defineDirective(
{factory: () => new DirectiveC(inject(DirectiveA), inject(DirectiveB))});
}
function Template(ctx: any, cm: boolean) {
if (cm) {
E(0, 'div', null, [DirectiveA]);
{
E(2, 'span', null, [DirectiveB, DirectiveC]);
{ T(5); }
e();
}
e();
}
t(5, b(m<DirectiveC>(4).value));
}
expect(renderToHtml(Template, {})).toEqual('<div><span>AB</span></div>');
});
});
describe('ElementRef', () => {
it('should create directive with ElementRef dependencies', () => {
class Directive {
value: string;
constructor(public elementRef: ElementRef) {
this.value = (elementRef.constructor as any).name;
}
static ngDirectiveDef = defineDirective(
{factory: () => new Directive(injectElementRef()), features: [PublicFeature]});
}
class DirectiveSameInstance {
value: boolean;
constructor(elementRef: ElementRef, directive: Directive) {
this.value = elementRef === directive.elementRef;
}
static ngDirectiveDef = defineDirective(
{factory: () => new DirectiveSameInstance(injectElementRef(), inject(Directive))});
}
function Template(ctx: any, cm: boolean) {
if (cm) {
E(0, 'div', null, [Directive, DirectiveSameInstance]);
{ T(3); }
e();
}
t(3, b2('', m<Directive>(1).value, '-', m<DirectiveSameInstance>(2).value, ''));
}
expect(renderToHtml(Template, {})).toEqual('<div>ElementRef-true</div>');
});
});
describe('TemplateRef', () => {
it('should create directive with TemplateRef dependencies', () => {
class Directive {
value: string;
constructor(public templateRef: TemplateRef<any>) {
this.value = (templateRef.constructor as any).name;
}
static ngDirectiveDef = defineDirective(
{factory: () => new Directive(injectTemplateRef()), features: [PublicFeature]});
}
class DirectiveSameInstance {
value: boolean;
constructor(templateRef: TemplateRef<any>, directive: Directive) {
this.value = templateRef === directive.templateRef;
}
static ngDirectiveDef = defineDirective(
{factory: () => new DirectiveSameInstance(injectTemplateRef(), inject(Directive))});
}
function Template(ctx: any, cm: any) {
if (cm) {
C(0, [Directive, DirectiveSameInstance], function() {});
T(3);
}
t(3, b2('', m<Directive>(1).value, '-', m<DirectiveSameInstance>(2).value, ''));
}
expect(renderToHtml(Template, {})).toEqual('TemplateRef-true');
});
});
describe('ViewContainerRef', () => {
it('should create directive with ViewContainerRef dependencies', () => {
class Directive {
value: string;
constructor(public viewContainerRef: ViewContainerRef) {
this.value = (viewContainerRef.constructor as any).name;
}
static ngDirectiveDef = defineDirective(
{factory: () => new Directive(injectViewContainerRef()), features: [PublicFeature]});
}
class DirectiveSameInstance {
value: boolean;
constructor(viewContainerRef: ViewContainerRef, directive: Directive) {
this.value = viewContainerRef === directive.viewContainerRef;
}
static ngDirectiveDef = defineDirective({
factory: () => new DirectiveSameInstance(injectViewContainerRef(), inject(Directive))
});
}
function Template(ctx: any, cm: boolean) {
if (cm) {
E(0, 'div', null, [Directive, DirectiveSameInstance]);
{ T(3); }
e();
}
t(3, b2('', m<Directive>(1).value, '-', m<DirectiveSameInstance>(2).value, ''));
}
expect(renderToHtml(Template, {})).toEqual('<div>ViewContainerRef-true</div>');
});
});
describe('inject', () => {
describe('bloom filter', () => {
let di: LInjector;
beforeEach(() => {
di = {} as any;
di.bf0 = 0;
di.bf1 = 0;
di.bf2 = 0;
di.bf3 = 0;
di.cbf0 = 0;
di.cbf1 = 0;
di.cbf2 = 0;
di.cbf3 = 0;
});
function bloomState() { return [di.bf3, di.bf2, di.bf1, di.bf0]; }
it('should add values', () => {
bloomAdd(di, { __NG_ELEMENT_ID__: 0 } as any);
expect(bloomState()).toEqual([0, 0, 0, 1]);
bloomAdd(di, { __NG_ELEMENT_ID__: 32 + 1 } as any);
expect(bloomState()).toEqual([0, 0, 2, 1]);
bloomAdd(di, { __NG_ELEMENT_ID__: 64 + 2 } as any);
expect(bloomState()).toEqual([0, 4, 2, 1]);
bloomAdd(di, { __NG_ELEMENT_ID__: 96 + 3 } as any);
expect(bloomState()).toEqual([8, 4, 2, 1]);
});
it('should query values', () => {
bloomAdd(di, { __NG_ELEMENT_ID__: 0 } as any);
bloomAdd(di, { __NG_ELEMENT_ID__: 32 } as any);
bloomAdd(di, { __NG_ELEMENT_ID__: 64 } as any);
bloomAdd(di, { __NG_ELEMENT_ID__: 96 } as any);
expect(bloomFindPossibleInjector(di, 0)).toEqual(di);
expect(bloomFindPossibleInjector(di, 1)).toEqual(null);
expect(bloomFindPossibleInjector(di, 32)).toEqual(di);
expect(bloomFindPossibleInjector(di, 64)).toEqual(di);
expect(bloomFindPossibleInjector(di, 96)).toEqual(di);
});
});
it('should inject from parent view', () => {
class ParentDirective {
static ngDirectiveDef =
defineDirective({factory: () => new ParentDirective(), features: [PublicFeature]});
}
class ChildDirective {
value: string;
constructor(public parent: ParentDirective) {
this.value = (parent.constructor as any).name;
}
static ngDirectiveDef = defineDirective({
factory: () => new ChildDirective(inject(ParentDirective)),
features: [PublicFeature]
});
}
class Child2Directive {
value: boolean;
constructor(parent: ParentDirective, child: ChildDirective) {
this.value = parent === child.parent;
}
static ngDirectiveDef = defineDirective(
{factory: () => new Child2Directive(inject(ParentDirective), inject(ChildDirective))});
}
function Template(ctx: any, cm: boolean) {
if (cm) {
E(0, 'div', null, [ParentDirective]);
{ C(2); }
e();
}
cR(2);
{
if (V(0)) {
E(0, 'span', null, [ChildDirective, Child2Directive]);
{ T(3); }
e();
}
t(3, b2('', m<ChildDirective>(1).value, '-', m<Child2Directive>(2).value, ''));
v();
}
cr();
}
expect(renderToHtml(Template, {})).toEqual('<div><span>ParentDirective-true</span></div>');
});
it('should inject from module Injector', () => {
});
});
describe('getOrCreateNodeInjector', () => {
it('should handle initial undefined state', () => {
const contentView = createLView(-1, null !, {data: []});
const oldView = enterView(contentView, null !);
try {
const parent = createLNode(0, LNodeFlags.Element, null, null);
// Simulate the situation where the previous parent is not initialized.
// This happens on first bootstrap because we don't init existing values
// so that we have smaller HelloWorld.
(parent as{parent: any}).parent = undefined;
const injector = getOrCreateNodeInjector();
expect(injector).not.toBe(null);
} finally {
leaveView(oldView);
}
});
});
});