2341 lines
80 KiB
TypeScript
2341 lines
80 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 {Attribute, ChangeDetectorRef, ElementRef, Host, INJECTOR, Inject, InjectFlags, Injector, Optional, Renderer2, Self, SkipSelf, TemplateRef, ViewContainerRef, createInjector, defineInjectable, defineInjector} from '@angular/core';
|
|
import {ComponentType, RenderFlags} from '@angular/core/src/render3/interfaces/definition';
|
|
|
|
import {defineComponent} from '../../src/render3/definition';
|
|
import {bloomAdd, bloomHasToken, bloomHashBitOrFactory as bloomHash, getOrCreateNodeInjectorForNode} from '../../src/render3/di';
|
|
import {ProvidersFeature, defineDirective, elementProperty, load, templateRefExtractor} from '../../src/render3/index';
|
|
|
|
import {allocHostVars, bind, container, containerRefreshEnd, containerRefreshStart, createNodeAtIndex, createLView, createTView, directiveInject, element, elementEnd, elementStart, embeddedViewEnd, embeddedViewStart, injectAttribute, interpolation2, projection, projectionDef, reference, template, text, textBinding, elementContainerStart, elementContainerEnd} from '../../src/render3/instructions';
|
|
import {isProceduralRenderer, RElement} from '../../src/render3/interfaces/renderer';
|
|
import {AttributeMarker, TNodeType} from '../../src/render3/interfaces/node';
|
|
import {getNativeByIndex} from '../../src/render3/util';
|
|
import {LViewFlags} from '../../src/render3/interfaces/view';
|
|
import {enterView, leaveView, getLView} from '../../src/render3/state';
|
|
import {ViewRef} from '../../src/render3/view_ref';
|
|
|
|
import {getRendererFactory2} from './imported_renderer2';
|
|
import {ComponentFixture, createComponent, createDirective, getDirectiveOnNode, renderComponent, toHtml} from './render_util';
|
|
import {NgIf} from './common_with_def';
|
|
import {TNODE} from '../../src/render3/interfaces/injector';
|
|
import {LContainer, NATIVE} from '../../src/render3/interfaces/container';
|
|
|
|
describe('di', () => {
|
|
describe('no dependencies', () => {
|
|
it('should create directive with no deps', () => {
|
|
class Directive {
|
|
value: string = 'Created';
|
|
static ngDirectiveDef = defineDirective({
|
|
type: Directive,
|
|
selectors: [['', 'dir', '']],
|
|
factory: () => new Directive,
|
|
exportAs: ['dir']
|
|
});
|
|
}
|
|
|
|
/** <div dir #dir="dir"> {{ dir.value }} </div> */
|
|
const App = createComponent('app', function(rf: RenderFlags, ctx: any) {
|
|
if (rf & RenderFlags.Create) {
|
|
elementStart(0, 'div', ['dir', ''], ['dir', 'dir']);
|
|
{ text(2); }
|
|
elementEnd();
|
|
}
|
|
if (rf & RenderFlags.Update) {
|
|
const tmp = reference(1) as any;
|
|
textBinding(2, bind(tmp.value));
|
|
}
|
|
}, 3, 1, [Directive]);
|
|
|
|
const fixture = new ComponentFixture(App);
|
|
expect(fixture.html).toEqual('<div dir="">Created</div>');
|
|
});
|
|
});
|
|
|
|
describe('directive injection', () => {
|
|
let log: string[] = [];
|
|
|
|
class DirB {
|
|
value = 'DirB';
|
|
constructor() { log.push(this.value); }
|
|
|
|
static ngDirectiveDef = defineDirective({
|
|
selectors: [['', 'dirB', '']],
|
|
type: DirB,
|
|
factory: () => new DirB(),
|
|
inputs: {value: 'value'}
|
|
});
|
|
}
|
|
|
|
beforeEach(() => log = []);
|
|
|
|
it('should create directive with intra view dependencies', () => {
|
|
class DirA {
|
|
value: string = 'DirA';
|
|
static ngDirectiveDef =
|
|
defineDirective({type: DirA, selectors: [['', 'dirA', '']], factory: () => new DirA()});
|
|
}
|
|
|
|
class DirC {
|
|
value: string;
|
|
constructor(a: DirA, b: DirB) { this.value = a.value + b.value; }
|
|
static ngDirectiveDef = defineDirective({
|
|
type: DirC,
|
|
selectors: [['', 'dirC', '']],
|
|
factory: () => new DirC(directiveInject(DirA), directiveInject(DirB)),
|
|
exportAs: ['dirC']
|
|
});
|
|
}
|
|
|
|
/**
|
|
* <div dirA>
|
|
* <span dirB dirC #dir="dirC"> {{ dir.value }} </span>
|
|
* </div>
|
|
*/
|
|
const App = createComponent('app', function(rf: RenderFlags, ctx: any) {
|
|
if (rf & RenderFlags.Create) {
|
|
elementStart(0, 'div', ['dirA', '']);
|
|
{
|
|
elementStart(1, 'span', ['dirB', '', 'dirC', ''], ['dir', 'dirC']);
|
|
{ text(3); }
|
|
elementEnd();
|
|
}
|
|
elementEnd();
|
|
}
|
|
if (rf & RenderFlags.Update) {
|
|
const tmp = reference(2) as any;
|
|
textBinding(3, bind(tmp.value));
|
|
}
|
|
}, 4, 1, [DirA, DirB, DirC]);
|
|
|
|
const fixture = new ComponentFixture(App);
|
|
expect(fixture.html).toEqual('<div dira=""><span dirb="" dirc="">DirADirB</span></div>');
|
|
});
|
|
|
|
it('should instantiate injected directives in dependency order', () => {
|
|
class DirA {
|
|
constructor(dir: DirB) { log.push(`DirA (dep: ${dir.value})`); }
|
|
|
|
static ngDirectiveDef = defineDirective({
|
|
selectors: [['', 'dirA', '']],
|
|
type: DirA,
|
|
factory: () => new DirA(directiveInject(DirB)),
|
|
});
|
|
}
|
|
|
|
/** <div dirA dirB></div> */
|
|
const App = createComponent('app', function(rf: RenderFlags, ctx: any) {
|
|
if (rf & RenderFlags.Create) {
|
|
element(0, 'div', ['dirA', '', 'dirB', '']);
|
|
}
|
|
}, 1, 0, [DirA, DirB]);
|
|
|
|
new ComponentFixture(App);
|
|
expect(log).toEqual(['DirB', 'DirA (dep: DirB)']);
|
|
});
|
|
|
|
it('should fallback to the module injector', () => {
|
|
class DirA {
|
|
constructor(dir: DirB) { log.push(`DirA (dep: ${dir.value})`); }
|
|
|
|
static ngDirectiveDef = defineDirective({
|
|
selectors: [['', 'dirA', '']],
|
|
type: DirA,
|
|
factory: () => new DirA(directiveInject(DirB)),
|
|
});
|
|
}
|
|
|
|
// `<div dirB></div><div dirA></div>`
|
|
// - dirB is know to the node injectors (it uses the diPublic feature)
|
|
// - then when dirA tries to inject dirB, it will check the node injector first tree
|
|
// - if not found, it will check the module injector tree
|
|
const App = createComponent('app', function(rf: RenderFlags, ctx: any) {
|
|
if (rf & RenderFlags.Create) {
|
|
element(0, 'div', ['dirB', '']);
|
|
element(1, 'div', ['dirA', '']);
|
|
}
|
|
}, 2, 0, [DirA, DirB]);
|
|
|
|
const fakeModuleInjector: any = {
|
|
get: function(token: any) {
|
|
const value = token === DirB ? 'module' : 'fail';
|
|
return {value: value};
|
|
}
|
|
};
|
|
|
|
new ComponentFixture(App, {injector: fakeModuleInjector});
|
|
expect(log).toEqual(['DirB', 'DirA (dep: module)']);
|
|
});
|
|
|
|
it('should instantiate injected directives before components', () => {
|
|
class Comp {
|
|
constructor(dir: DirB) { log.push(`Comp (dep: ${dir.value})`); }
|
|
|
|
static ngComponentDef = defineComponent({
|
|
selectors: [['comp']],
|
|
type: Comp,
|
|
consts: 0,
|
|
vars: 0,
|
|
factory: () => new Comp(directiveInject(DirB)),
|
|
template: (ctx: any, fm: boolean) => {}
|
|
});
|
|
}
|
|
|
|
/** <comp dirB></comp> */
|
|
const App = createComponent('app', function(rf: RenderFlags, ctx: any) {
|
|
if (rf & RenderFlags.Create) {
|
|
element(0, 'comp', ['dirB', '']);
|
|
}
|
|
}, 1, 0, [Comp, DirB]);
|
|
|
|
new ComponentFixture(App);
|
|
expect(log).toEqual(['DirB', 'Comp (dep: DirB)']);
|
|
});
|
|
|
|
it('should inject directives in the correct order in a for loop', () => {
|
|
class DirA {
|
|
constructor(dir: DirB) { log.push(`DirA (dep: ${dir.value})`); }
|
|
|
|
static ngDirectiveDef = defineDirective({
|
|
selectors: [['', 'dirA', '']],
|
|
type: DirA,
|
|
factory: () => new DirA(directiveInject(DirB))
|
|
});
|
|
}
|
|
|
|
/**
|
|
* % for(let i = 0; i < 3; i++) {
|
|
* <div dirA dirB></div>
|
|
* % }
|
|
*/
|
|
const App = createComponent('app', function(rf: RenderFlags, ctx: any) {
|
|
if (rf & RenderFlags.Create) {
|
|
container(0);
|
|
}
|
|
if (rf & RenderFlags.Update) {
|
|
containerRefreshStart(0);
|
|
{
|
|
for (let i = 0; i < 3; i++) {
|
|
if (embeddedViewStart(0, 1, 0)) {
|
|
element(0, 'div', ['dirA', '', 'dirB', '']);
|
|
}
|
|
embeddedViewEnd();
|
|
}
|
|
}
|
|
containerRefreshEnd();
|
|
}
|
|
}, 1, 0, [DirA, DirB]);
|
|
|
|
new ComponentFixture(App);
|
|
expect(log).toEqual(
|
|
['DirB', 'DirA (dep: DirB)', 'DirB', 'DirA (dep: DirB)', 'DirB', 'DirA (dep: DirB)']);
|
|
});
|
|
|
|
it('should instantiate directives with multiple out-of-order dependencies', () => {
|
|
class DirA {
|
|
value = 'DirA';
|
|
constructor() { log.push(this.value); }
|
|
|
|
static ngDirectiveDef =
|
|
defineDirective({selectors: [['', 'dirA', '']], type: DirA, factory: () => new DirA()});
|
|
}
|
|
|
|
class DirB {
|
|
constructor(dirA: DirA, dirC: DirC) {
|
|
log.push(`DirB (deps: ${dirA.value} and ${dirC.value})`);
|
|
}
|
|
|
|
static ngDirectiveDef = defineDirective({
|
|
selectors: [['', 'dirB', '']],
|
|
type: DirB,
|
|
factory: () => new DirB(directiveInject(DirA), directiveInject(DirC))
|
|
});
|
|
}
|
|
|
|
class DirC {
|
|
value = 'DirC';
|
|
constructor() { log.push(this.value); }
|
|
|
|
static ngDirectiveDef =
|
|
defineDirective({selectors: [['', 'dirC', '']], type: DirC, factory: () => new DirC()});
|
|
}
|
|
|
|
/** <div dirA dirB dirC></div> */
|
|
const App = createComponent('app', function(rf: RenderFlags, ctx: any) {
|
|
if (rf & RenderFlags.Create) {
|
|
element(0, 'div', ['dirA', '', 'dirB', '', 'dirC', '']);
|
|
}
|
|
}, 1, 0, [DirA, DirB, DirC]);
|
|
|
|
new ComponentFixture(App);
|
|
expect(log).toEqual(['DirA', 'DirC', 'DirB (deps: DirA and DirC)']);
|
|
});
|
|
|
|
it('should instantiate in the correct order for complex case', () => {
|
|
class Comp {
|
|
constructor(dir: DirD) { log.push(`Comp (dep: ${dir.value})`); }
|
|
|
|
static ngComponentDef = defineComponent({
|
|
selectors: [['comp']],
|
|
type: Comp,
|
|
consts: 0,
|
|
vars: 0,
|
|
factory: () => new Comp(directiveInject(DirD)),
|
|
template: (ctx: any, fm: boolean) => {}
|
|
});
|
|
}
|
|
|
|
class DirA {
|
|
value = 'DirA';
|
|
constructor(dir: DirC) { log.push(`DirA (dep: ${dir.value})`); }
|
|
|
|
static ngDirectiveDef = defineDirective({
|
|
selectors: [['', 'dirA', '']],
|
|
type: DirA,
|
|
factory: () => new DirA(directiveInject(DirC))
|
|
});
|
|
}
|
|
|
|
class DirC {
|
|
value = 'DirC';
|
|
constructor(dir: DirB) { log.push(`DirC (dep: ${dir.value})`); }
|
|
|
|
static ngDirectiveDef = defineDirective({
|
|
selectors: [['', 'dirC', '']],
|
|
type: DirC,
|
|
factory: () => new DirC(directiveInject(DirB))
|
|
});
|
|
}
|
|
|
|
class DirD {
|
|
value = 'DirD';
|
|
constructor(dir: DirA) { log.push(`DirD (dep: ${dir.value})`); }
|
|
|
|
static ngDirectiveDef = defineDirective({
|
|
selectors: [['', 'dirD', '']],
|
|
type: DirD,
|
|
factory: () => new DirD(directiveInject(DirA))
|
|
});
|
|
}
|
|
|
|
/** <comp dirA dirB dirC dirD></comp> */
|
|
const App = createComponent('app', function(rf: RenderFlags, ctx: any) {
|
|
if (rf & RenderFlags.Create) {
|
|
element(0, 'comp', ['dirA', '', 'dirB', '', 'dirC', '', 'dirD', '']);
|
|
}
|
|
}, 1, 0, [Comp, DirA, DirB, DirC, DirD]);
|
|
|
|
new ComponentFixture(App);
|
|
expect(log).toEqual(
|
|
['DirB', 'DirC (dep: DirB)', 'DirA (dep: DirC)', 'DirD (dep: DirA)', 'Comp (dep: DirD)']);
|
|
});
|
|
|
|
it('should instantiate in correct order with mixed parent and peer dependencies', () => {
|
|
class DirA {
|
|
constructor(dirB: DirB, app: App) {
|
|
log.push(`DirA (deps: ${dirB.value} and ${app.value})`);
|
|
}
|
|
|
|
static ngDirectiveDef = defineDirective({
|
|
selectors: [['', 'dirA', '']],
|
|
type: DirA,
|
|
factory: () => new DirA(directiveInject(DirB), directiveInject(App)),
|
|
});
|
|
}
|
|
|
|
class App {
|
|
value = 'App';
|
|
|
|
static ngComponentDef = defineComponent({
|
|
selectors: [['app']],
|
|
type: App,
|
|
factory: () => new App(),
|
|
consts: 1,
|
|
vars: 0,
|
|
/** <div dirA dirB dirC></div> */
|
|
template: (rf: RenderFlags, ctx: any) => {
|
|
if (rf & RenderFlags.Create) {
|
|
element(0, 'div', ['dirA', '', 'dirB', '', 'dirC', 'dirC']);
|
|
}
|
|
},
|
|
directives: [DirA, DirB]
|
|
});
|
|
}
|
|
|
|
new ComponentFixture(App);
|
|
expect(log).toEqual(['DirB', 'DirA (deps: DirB and App)']);
|
|
});
|
|
|
|
it('should not use a parent when peer dep is available', () => {
|
|
let count = 1;
|
|
|
|
class DirA {
|
|
constructor(dirB: DirB) { log.push(`DirA (dep: DirB - ${dirB.count})`); }
|
|
|
|
static ngDirectiveDef = defineDirective({
|
|
selectors: [['', 'dirA', '']],
|
|
type: DirA,
|
|
factory: () => new DirA(directiveInject(DirB)),
|
|
});
|
|
}
|
|
|
|
class DirB {
|
|
count: number;
|
|
|
|
constructor() {
|
|
log.push(`DirB`);
|
|
this.count = count++;
|
|
}
|
|
|
|
static ngDirectiveDef =
|
|
defineDirective({selectors: [['', 'dirB', '']], type: DirB, factory: () => new DirB()});
|
|
}
|
|
|
|
/** <div dirA dirB></div> */
|
|
const Parent = createComponent('parent', function(rf: RenderFlags, ctx: any) {
|
|
if (rf & RenderFlags.Create) {
|
|
element(0, 'div', ['dirA', '', 'dirB', '']);
|
|
}
|
|
}, 1, 0, [DirA, DirB]);
|
|
|
|
/** <parent dirB></parent> */
|
|
const App = createComponent('app', function(rf: RenderFlags, ctx: any) {
|
|
if (rf & RenderFlags.Create) {
|
|
element(0, 'parent', ['dirB', '']);
|
|
}
|
|
}, 1, 0, [Parent, DirB]);
|
|
|
|
new ComponentFixture(App);
|
|
expect(log).toEqual(['DirB', 'DirB', 'DirA (dep: DirB - 2)']);
|
|
});
|
|
|
|
describe('dependencies in parent views', () => {
|
|
|
|
class DirA {
|
|
injector: Injector;
|
|
constructor(public dirB: DirB, public vcr: ViewContainerRef) {
|
|
this.injector = vcr.injector;
|
|
}
|
|
|
|
static ngDirectiveDef = defineDirective({
|
|
type: DirA,
|
|
selectors: [['', 'dirA', '']],
|
|
factory: () => new DirA(directiveInject(DirB), directiveInject(ViewContainerRef as any)),
|
|
exportAs: ['dirA']
|
|
});
|
|
}
|
|
|
|
/**
|
|
* <div dirA #dir="dirA">
|
|
* {{ dir.dirB.value }}
|
|
* </div>
|
|
*/
|
|
const Comp = createComponent('comp', (rf: RenderFlags, ctx: any) => {
|
|
if (rf & RenderFlags.Create) {
|
|
elementStart(0, 'div', ['dirA', ''], ['dir', 'dirA']);
|
|
{ text(2); }
|
|
elementEnd();
|
|
}
|
|
if (rf & RenderFlags.Update) {
|
|
const dir = reference(1) as DirA;
|
|
textBinding(2, bind(dir.dirB.value));
|
|
}
|
|
}, 3, 1, [DirA]);
|
|
|
|
it('should find dependencies on component hosts', () => {
|
|
/** <comp dirB>/comp> */
|
|
const App = createComponent('app', (rf: RenderFlags, ctx: any) => {
|
|
if (rf & RenderFlags.Create) {
|
|
element(0, 'comp', ['dirB', '']);
|
|
}
|
|
}, 1, 0, [Comp, DirB]);
|
|
|
|
const fixture = new ComponentFixture(App);
|
|
expect(fixture.hostElement.textContent).toEqual(`DirB`);
|
|
});
|
|
|
|
it('should find dependencies for directives in embedded views', () => {
|
|
|
|
function IfTemplate(rf: RenderFlags, ctx: any) {
|
|
if (rf & RenderFlags.Create) {
|
|
elementStart(0, 'div');
|
|
{
|
|
elementStart(1, 'div', ['dirA', ''], ['dir', 'dirA']);
|
|
{ text(3); }
|
|
elementEnd();
|
|
}
|
|
elementEnd();
|
|
}
|
|
|
|
if (rf & RenderFlags.Update) {
|
|
const dir = reference(2) as DirA;
|
|
textBinding(3, bind(dir.dirB.value));
|
|
}
|
|
}
|
|
|
|
/**
|
|
* <div dirB>
|
|
* <div *ngIf="showing">
|
|
* <div dirA #dir="dirA"> {{ dir.dirB.value }} </div>
|
|
* </div>
|
|
* </div>
|
|
*/
|
|
const App = createComponent('app', (rf: RenderFlags, ctx: any) => {
|
|
if (rf & RenderFlags.Create) {
|
|
elementStart(0, 'div', ['dirB', '']);
|
|
{ template(1, IfTemplate, 4, 1, 'div', [AttributeMarker.SelectOnly, 'ngIf', '']); }
|
|
elementEnd();
|
|
}
|
|
if (rf & RenderFlags.Update) {
|
|
elementProperty(1, 'ngIf', bind(ctx.showing));
|
|
}
|
|
}, 2, 1, [DirA, DirB, NgIf]);
|
|
|
|
const fixture = new ComponentFixture(App);
|
|
fixture.component.showing = true;
|
|
fixture.update();
|
|
|
|
expect(fixture.hostElement.textContent).toEqual(`DirB`);
|
|
});
|
|
|
|
it('should find dependencies of directives nested deeply in inline views', () => {
|
|
/**
|
|
* <div dirB>
|
|
* % if (!skipContent) {
|
|
* % if (!skipContent2) {
|
|
* <div dirA #dir="dirA"> {{ dir.dirB.value }} </div>
|
|
* % }
|
|
* % }
|
|
* </div>
|
|
*/
|
|
const App = createComponent('app', (rf: RenderFlags, ctx: any) => {
|
|
if (rf & RenderFlags.Create) {
|
|
elementStart(0, 'div', ['dirB', '']);
|
|
{ container(1); }
|
|
elementEnd();
|
|
}
|
|
if (rf & RenderFlags.Update) {
|
|
containerRefreshStart(1);
|
|
{
|
|
if (!ctx.skipContent) {
|
|
let rf1 = embeddedViewStart(0, 1, 0);
|
|
{
|
|
if (rf1 & RenderFlags.Create) {
|
|
container(0);
|
|
}
|
|
if (rf1 & RenderFlags.Update) {
|
|
containerRefreshStart(0);
|
|
{
|
|
if (!ctx.skipContent2) {
|
|
let rf2 = embeddedViewStart(0, 3, 1);
|
|
{
|
|
if (rf2 & RenderFlags.Create) {
|
|
elementStart(0, 'div', ['dirA', ''], ['dir', 'dirA']);
|
|
{ text(2); }
|
|
elementEnd();
|
|
}
|
|
if (rf2 & RenderFlags.Update) {
|
|
const dir = reference(1) as DirA;
|
|
textBinding(2, bind(dir.dirB.value));
|
|
}
|
|
}
|
|
embeddedViewEnd();
|
|
}
|
|
}
|
|
containerRefreshEnd();
|
|
}
|
|
}
|
|
embeddedViewEnd();
|
|
}
|
|
}
|
|
containerRefreshEnd();
|
|
}
|
|
}, 2, 0, [DirA, DirB]);
|
|
|
|
const fixture = new ComponentFixture(App);
|
|
expect(fixture.hostElement.textContent).toEqual(`DirB`);
|
|
});
|
|
|
|
it('should find dependencies in declaration tree of ng-template (not insertion tree)', () => {
|
|
let structuralDir !: StructuralDir;
|
|
|
|
class StructuralDir {
|
|
// @Input()
|
|
tmp !: TemplateRef<any>;
|
|
|
|
constructor(public vcr: ViewContainerRef) {}
|
|
|
|
create() { this.vcr.createEmbeddedView(this.tmp); }
|
|
|
|
static ngDirectiveDef = defineDirective({
|
|
type: StructuralDir,
|
|
selectors: [['', 'structuralDir', '']],
|
|
factory: () => structuralDir =
|
|
new StructuralDir(directiveInject(ViewContainerRef as any)),
|
|
inputs: {tmp: 'tmp'}
|
|
});
|
|
}
|
|
|
|
function FooTemplate(rf: RenderFlags, ctx: any) {
|
|
if (rf & RenderFlags.Create) {
|
|
elementStart(0, 'div', ['dirA', ''], ['dir', 'dirA']);
|
|
{ text(2); }
|
|
elementEnd();
|
|
}
|
|
if (rf & RenderFlags.Update) {
|
|
const dir = reference(1) as DirA;
|
|
textBinding(2, bind(dir.dirB.value));
|
|
}
|
|
}
|
|
|
|
/**
|
|
* <div dirB value="declaration">
|
|
* <ng-template #foo>
|
|
* <div dirA dir="dirA"> {{ dir.dirB.value }} </div>
|
|
* </ng-template>
|
|
* </div>
|
|
*
|
|
* <div dirB value="insertion">
|
|
* <div structuralDir [tmp]="foo"></div>
|
|
* // insertion point
|
|
* </div>
|
|
*/
|
|
const App = createComponent('app', (rf: RenderFlags, ctx: any) => {
|
|
if (rf & RenderFlags.Create) {
|
|
elementStart(0, 'div', ['dirB', '', 'value', 'declaration']);
|
|
{
|
|
template(
|
|
1, FooTemplate, 3, 1, 'ng-template', null, ['foo', ''], templateRefExtractor);
|
|
}
|
|
elementEnd();
|
|
elementStart(3, 'div', ['dirB', '', 'value', 'insertion']);
|
|
{ element(4, 'div', ['structuralDir', '']); }
|
|
elementEnd();
|
|
}
|
|
if (rf & RenderFlags.Update) {
|
|
const foo = reference(2) as any;
|
|
elementProperty(4, 'tmp', bind(foo));
|
|
}
|
|
}, 5, 1, [DirA, DirB, StructuralDir]);
|
|
|
|
const fixture = new ComponentFixture(App);
|
|
structuralDir.create();
|
|
fixture.update();
|
|
expect(fixture.hostElement.textContent).toEqual(`declaration`);
|
|
});
|
|
|
|
it('should create injectors on second template pass', () => {
|
|
/**
|
|
* <comp dirB></comp>
|
|
* <comp dirB></comp>
|
|
*/
|
|
const App = createComponent('app', (rf: RenderFlags, ctx: any) => {
|
|
if (rf & RenderFlags.Create) {
|
|
element(0, 'comp', ['dirB', '']);
|
|
element(1, 'comp', ['dirB', '']);
|
|
}
|
|
}, 2, 0, [Comp, DirB]);
|
|
|
|
const fixture = new ComponentFixture(App);
|
|
expect(fixture.hostElement.textContent).toEqual(`DirBDirB`);
|
|
});
|
|
|
|
it('should create injectors and host bindings in same view', () => {
|
|
let hostBindingDir !: HostBindingDir;
|
|
|
|
class HostBindingDir {
|
|
// @HostBinding('id')
|
|
id = 'foo';
|
|
|
|
static ngDirectiveDef = defineDirective({
|
|
type: HostBindingDir,
|
|
selectors: [['', 'hostBindingDir', '']],
|
|
factory: () => hostBindingDir = new HostBindingDir(),
|
|
hostBindings: (rf: RenderFlags, ctx: any, elementIndex: number) => {
|
|
if (rf & RenderFlags.Create) {
|
|
allocHostVars(1);
|
|
}
|
|
if (rf & RenderFlags.Update) {
|
|
elementProperty(elementIndex, 'id', bind(ctx.id));
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
let dir !: DirA;
|
|
/**
|
|
* <div dirB hostBindingDir>
|
|
* <p dirA #dir="dirA">
|
|
* {{ dir.dirB.value }}
|
|
* </p>
|
|
* </div>
|
|
*/
|
|
const App = createComponent('app', (rf: RenderFlags, ctx: any) => {
|
|
if (rf & RenderFlags.Create) {
|
|
elementStart(0, 'div', [
|
|
'dirB',
|
|
'',
|
|
'hostBindingDir',
|
|
'',
|
|
]);
|
|
{
|
|
elementStart(1, 'p', ['dirA', ''], ['dir', 'dirA']);
|
|
{ text(3); }
|
|
elementEnd();
|
|
}
|
|
elementEnd();
|
|
}
|
|
if (rf & RenderFlags.Update) {
|
|
dir = reference(2) as DirA;
|
|
textBinding(3, bind(dir.dirB.value));
|
|
}
|
|
}, 4, 1, [HostBindingDir, DirA, DirB]);
|
|
|
|
const fixture = new ComponentFixture(App);
|
|
expect(fixture.hostElement.textContent).toEqual(`DirB`);
|
|
const hostDirEl = fixture.hostElement.querySelector('div') as HTMLElement;
|
|
expect(hostDirEl.id).toEqual('foo');
|
|
// The injector should not be overwritten by host bindings
|
|
expect(dir.vcr.injector).toEqual(dir.injector);
|
|
|
|
hostBindingDir.id = 'bar';
|
|
fixture.update();
|
|
expect(hostDirEl.id).toEqual('bar');
|
|
});
|
|
});
|
|
|
|
it('should create instance even when no injector present', () => {
|
|
class MyService {
|
|
value = 'MyService';
|
|
static ngInjectableDef =
|
|
defineInjectable({providedIn: 'root', factory: () => new MyService()});
|
|
}
|
|
|
|
class MyComponent {
|
|
constructor(public myService: MyService) {}
|
|
static ngComponentDef = defineComponent({
|
|
type: MyComponent,
|
|
selectors: [['my-component']],
|
|
consts: 1,
|
|
vars: 1,
|
|
factory: () => new MyComponent(directiveInject(MyService)),
|
|
template: function(rf: RenderFlags, ctx: MyComponent) {
|
|
if (rf & RenderFlags.Create) {
|
|
text(0);
|
|
}
|
|
if (rf & RenderFlags.Update) {
|
|
textBinding(0, bind(ctx.myService.value));
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
const fixture = new ComponentFixture(MyComponent);
|
|
fixture.update();
|
|
expect(fixture.html).toEqual('MyService');
|
|
});
|
|
|
|
it('should throw if directive is not found anywhere', () => {
|
|
class Dir {
|
|
constructor(siblingDir: OtherDir) {}
|
|
|
|
static ngDirectiveDef = defineDirective({
|
|
selectors: [['', 'dir', '']],
|
|
type: Dir,
|
|
factory: () => new Dir(directiveInject(OtherDir))
|
|
});
|
|
}
|
|
|
|
class OtherDir {
|
|
static ngDirectiveDef = defineDirective(
|
|
{selectors: [['', 'other', '']], type: OtherDir, factory: () => new OtherDir()});
|
|
}
|
|
|
|
/** <div dir></div> */
|
|
const App = createComponent('app', function(rf: RenderFlags, ctx: any) {
|
|
if (rf & RenderFlags.Create) {
|
|
element(0, 'div', ['dir', '']);
|
|
}
|
|
}, 1, 0, [Dir, OtherDir]);
|
|
|
|
expect(() => new ComponentFixture(App)).toThrowError(/Injector: NOT_FOUND \[OtherDir\]/);
|
|
});
|
|
|
|
it('should throw if directive is not found in ancestor tree', () => {
|
|
class Dir {
|
|
constructor(siblingDir: OtherDir) {}
|
|
|
|
static ngDirectiveDef = defineDirective({
|
|
selectors: [['', 'dir', '']],
|
|
type: Dir,
|
|
factory: () => new Dir(directiveInject(OtherDir))
|
|
});
|
|
}
|
|
|
|
class OtherDir {
|
|
static ngDirectiveDef = defineDirective(
|
|
{selectors: [['', 'other', '']], type: OtherDir, factory: () => new OtherDir()});
|
|
}
|
|
|
|
/**
|
|
* <div other></div>
|
|
* <div dir></div>
|
|
*/
|
|
const App = createComponent('app', function(rf: RenderFlags, ctx: any) {
|
|
if (rf & RenderFlags.Create) {
|
|
element(0, 'div', ['other', '']);
|
|
element(1, 'div', ['dir', '']);
|
|
}
|
|
}, 2, 0, [Dir, OtherDir]);
|
|
|
|
expect(() => new ComponentFixture(App)).toThrowError(/Injector: NOT_FOUND \[OtherDir\]/);
|
|
});
|
|
|
|
|
|
it('should throw if directives try to inject each other', () => {
|
|
class DirA {
|
|
constructor(dir: DirB) {}
|
|
|
|
static ngDirectiveDef = defineDirective({
|
|
selectors: [['', 'dirA', '']],
|
|
type: DirA,
|
|
factory: () => new DirA(directiveInject(DirB))
|
|
});
|
|
}
|
|
|
|
class DirB {
|
|
constructor(dir: DirA) {}
|
|
|
|
static ngDirectiveDef = defineDirective({
|
|
selectors: [['', 'dirB', '']],
|
|
type: DirB,
|
|
factory: () => new DirB(directiveInject(DirA))
|
|
});
|
|
}
|
|
|
|
/** <div dirA dirB></div> */
|
|
const App = createComponent('app', function(rf: RenderFlags, ctx: any) {
|
|
if (rf & RenderFlags.Create) {
|
|
element(0, 'div', ['dirA', '', 'dirB', '']);
|
|
}
|
|
}, 1, 0, [DirA, DirB]);
|
|
|
|
expect(() => new ComponentFixture(App)).toThrowError(/Circular dep for/);
|
|
});
|
|
|
|
it('should throw if directive tries to inject itself', () => {
|
|
class Dir {
|
|
constructor(dir: Dir) {}
|
|
|
|
static ngDirectiveDef = defineDirective({
|
|
selectors: [['', 'dir', '']],
|
|
type: Dir,
|
|
factory: () => new Dir(directiveInject(Dir))
|
|
});
|
|
}
|
|
|
|
/** <div dir></div> */
|
|
const App = createComponent('app', function(rf: RenderFlags, ctx: any) {
|
|
if (rf & RenderFlags.Create) {
|
|
element(0, 'div', ['dir', '']);
|
|
}
|
|
}, 1, 0, [Dir]);
|
|
|
|
expect(() => new ComponentFixture(App)).toThrowError(/Circular dep for/);
|
|
});
|
|
|
|
describe('flags', () => {
|
|
|
|
class DirB {
|
|
// TODO(issue/24571): remove '!'.
|
|
value !: string;
|
|
|
|
static ngDirectiveDef = defineDirective({
|
|
type: DirB,
|
|
selectors: [['', 'dirB', '']],
|
|
factory: () => new DirB(),
|
|
inputs: {value: 'dirB'}
|
|
});
|
|
}
|
|
|
|
describe('Optional', () => {
|
|
let dirA: DirA|null = null;
|
|
|
|
class DirA {
|
|
constructor(@Optional() public dirB: DirB|null) {}
|
|
|
|
static ngDirectiveDef = defineDirective({
|
|
type: DirA,
|
|
selectors: [['', 'dirA', '']],
|
|
factory: () => dirA = new DirA(directiveInject(DirB, InjectFlags.Optional))
|
|
});
|
|
}
|
|
|
|
beforeEach(() => dirA = null);
|
|
|
|
it('should not throw if dependency is @Optional (limp mode)', () => {
|
|
|
|
/** <div dirA></div> */
|
|
const App = createComponent('app', function(rf: RenderFlags, ctx: any) {
|
|
if (rf & RenderFlags.Create) {
|
|
element(0, 'div', ['dirA', '']);
|
|
}
|
|
}, 1, 0, [DirA, DirB]);
|
|
|
|
expect(() => { new ComponentFixture(App); }).not.toThrow();
|
|
expect(dirA !.dirB).toEqual(null);
|
|
});
|
|
|
|
it('should not throw if dependency is @Optional (module injector)', () => {
|
|
class SomeModule {
|
|
static ngInjectorDef = defineInjector({factory: () => new SomeModule()});
|
|
}
|
|
|
|
/** <div dirA></div> */
|
|
const App = createComponent('app', function(rf: RenderFlags, ctx: any) {
|
|
if (rf & RenderFlags.Create) {
|
|
element(0, 'div', ['dirA', '']);
|
|
}
|
|
}, 1, 0, [DirA, DirB]);
|
|
|
|
expect(() => {
|
|
const injector = createInjector(SomeModule);
|
|
new ComponentFixture(App, {injector});
|
|
}).not.toThrow();
|
|
expect(dirA !.dirB).toEqual(null);
|
|
});
|
|
|
|
it('should return null if @Optional dependency has @Self flag', () => {
|
|
let dirC !: DirC;
|
|
|
|
class DirC {
|
|
constructor(@Optional() @Self() public dirB: DirB|null) {}
|
|
|
|
static ngDirectiveDef = defineDirective({
|
|
type: DirC,
|
|
selectors: [['', 'dirC', '']],
|
|
factory: () => dirC =
|
|
new DirC(directiveInject(DirB, InjectFlags.Optional|InjectFlags.Self))
|
|
});
|
|
}
|
|
|
|
/** <div dirC></div> */
|
|
const App = createComponent('app', function(rf: RenderFlags, ctx: any) {
|
|
if (rf & RenderFlags.Create) {
|
|
element(0, 'div', ['dirC', '']);
|
|
}
|
|
}, 1, 0, [DirC, DirB]);
|
|
|
|
expect(() => { new ComponentFixture(App); }).not.toThrow();
|
|
expect(dirC !.dirB).toEqual(null);
|
|
});
|
|
|
|
it('should not throw if dependency is @Optional but defined elsewhere', () => {
|
|
let dirA: DirA;
|
|
|
|
class DirA {
|
|
constructor(@Optional() public dirB: DirB|null) {}
|
|
|
|
static ngDirectiveDef = defineDirective({
|
|
type: DirA,
|
|
selectors: [['', 'dirA', '']],
|
|
factory: () => dirA = new DirA(directiveInject(DirB, InjectFlags.Optional))
|
|
});
|
|
}
|
|
|
|
/**
|
|
* <div dirB></div>
|
|
* <div dirA></div>
|
|
*/
|
|
const App = createComponent('app', function(rf: RenderFlags, ctx: any) {
|
|
if (rf & RenderFlags.Create) {
|
|
element(0, 'div', ['dirB', '']);
|
|
element(1, 'div', ['dirA', '']);
|
|
}
|
|
}, 2, 0, [DirA, DirB]);
|
|
|
|
expect(() => {
|
|
new ComponentFixture(App);
|
|
expect(dirA !.dirB).toEqual(null);
|
|
}).not.toThrow();
|
|
});
|
|
});
|
|
|
|
it('should skip the current node with @SkipSelf', () => {
|
|
let dirA: DirA;
|
|
|
|
class DirA {
|
|
constructor(@SkipSelf() public dirB: DirB) {}
|
|
|
|
static ngDirectiveDef = defineDirective({
|
|
type: DirA,
|
|
selectors: [['', 'dirA', '']],
|
|
factory: () => dirA = new DirA(directiveInject(DirB, InjectFlags.SkipSelf))
|
|
});
|
|
}
|
|
|
|
/** <div dirA dirB="self"></div> */
|
|
const Comp = createComponent('comp', function(rf: RenderFlags, ctx: any) {
|
|
if (rf & RenderFlags.Create) {
|
|
element(0, 'div', ['dirA', '', 'dirB', 'self']);
|
|
}
|
|
}, 1, 0, [DirA, DirB]);
|
|
|
|
/* <comp dirB="parent"></comp> */
|
|
const App = createComponent('app', function(rf: RenderFlags, ctx: any) {
|
|
if (rf & RenderFlags.Create) {
|
|
element(0, 'comp', ['dirB', 'parent']);
|
|
}
|
|
}, 1, 0, [Comp, DirB]);
|
|
|
|
new ComponentFixture(App);
|
|
expect(dirA !.dirB.value).toEqual('parent');
|
|
});
|
|
|
|
it('should check only the current node with @Self', () => {
|
|
let dirA: DirA;
|
|
|
|
class DirA {
|
|
constructor(@Self() public dirB: DirB) {}
|
|
|
|
static ngDirectiveDef = defineDirective({
|
|
type: DirA,
|
|
selectors: [['', 'dirA', '']],
|
|
factory: () => dirA = new DirA(directiveInject(DirB, InjectFlags.Self))
|
|
});
|
|
}
|
|
|
|
/**
|
|
* <div dirB>
|
|
* <div dirA></div>
|
|
* </div>
|
|
*/
|
|
const App = createComponent('app', function(rf: RenderFlags, ctx: any) {
|
|
if (rf & RenderFlags.Create) {
|
|
elementStart(0, 'div', ['dirB', '']);
|
|
element(1, 'div', ['dirA', '']);
|
|
elementEnd();
|
|
}
|
|
}, 2, 0, [DirA, DirB]);
|
|
|
|
expect(() => {
|
|
new ComponentFixture(App);
|
|
}).toThrowError(/NodeInjector: NOT_FOUND \[DirB\]/);
|
|
});
|
|
|
|
it('should check only the current node with @Self even with false positive', () => {
|
|
let dirA: DirA;
|
|
|
|
class DirA {
|
|
constructor(@Self() public dirB: DirB) {}
|
|
|
|
static ngDirectiveDef = defineDirective({
|
|
type: DirA,
|
|
selectors: [['', 'dirA', '']],
|
|
factory: () => dirA = new DirA(directiveInject(DirB, InjectFlags.Self))
|
|
});
|
|
}
|
|
|
|
const DirC = createDirective('dirC');
|
|
|
|
/**
|
|
* <div dirB>
|
|
* <div dirA dirC></div>
|
|
* </div>
|
|
*/
|
|
const App = createComponent('app', function(rf: RenderFlags, ctx: any) {
|
|
if (rf & RenderFlags.Create) {
|
|
elementStart(0, 'div', ['dirB', '']);
|
|
element(1, 'div', ['dirA', '', 'dirC', '']);
|
|
elementEnd();
|
|
}
|
|
}, 2, 0, [DirA, DirB, DirC]);
|
|
|
|
expect(() => {
|
|
(DirA as any)['__NG_ELEMENT_ID__'] = 1;
|
|
(DirC as any)['__NG_ELEMENT_ID__'] = 257;
|
|
new ComponentFixture(App);
|
|
}).toThrowError(/NodeInjector: NOT_FOUND \[DirB\]/);
|
|
});
|
|
|
|
describe('@Host', () => {
|
|
let dirA: DirA|null = null;
|
|
let dirString: DirString|null = null;
|
|
|
|
beforeEach(() => {
|
|
dirA = null;
|
|
dirString = null;
|
|
});
|
|
|
|
class DirA {
|
|
constructor(@Host() public dirB: DirB) {}
|
|
|
|
static ngDirectiveDef = defineDirective({
|
|
type: DirA,
|
|
selectors: [['', 'dirA', '']],
|
|
factory: () => dirA = new DirA(directiveInject(DirB, InjectFlags.Host))
|
|
});
|
|
}
|
|
|
|
class DirString {
|
|
constructor(@Host() public s: String) {}
|
|
|
|
static ngDirectiveDef = defineDirective({
|
|
type: DirString,
|
|
selectors: [['', 'dirString', '']],
|
|
factory: () => dirString = new DirString(directiveInject(String, InjectFlags.Host))
|
|
});
|
|
}
|
|
|
|
it('should find viewProviders on the host itself', () => {
|
|
/** <div dirString></div> */
|
|
const Comp = createComponent('comp', function(rf: RenderFlags, ctx: any) {
|
|
if (rf & RenderFlags.Create) {
|
|
element(0, 'div', ['dirString', '']);
|
|
}
|
|
}, 1, 0, [DirString], [], null, [], [{provide: String, useValue: 'Foo'}]);
|
|
|
|
/* <comp></comp> */
|
|
const App = createComponent('app', function(rf: RenderFlags, ctx: any) {
|
|
if (rf & RenderFlags.Create) {
|
|
element(0, 'comp');
|
|
}
|
|
}, 1, 0, [Comp]);
|
|
|
|
new ComponentFixture(App);
|
|
expect(dirString !.s).toEqual('Foo');
|
|
});
|
|
|
|
it('should find host component on the host itself', () => {
|
|
let dirComp: DirComp|null = null;
|
|
|
|
class DirComp {
|
|
constructor(@Host() public comp: any) {}
|
|
|
|
static ngDirectiveDef = defineDirective({
|
|
type: DirComp,
|
|
selectors: [['', 'dirCmp', '']],
|
|
factory: () => dirComp = new DirComp(directiveInject(Comp, InjectFlags.Host))
|
|
});
|
|
}
|
|
|
|
/** <div dirCmp></div> */
|
|
const Comp = createComponent('comp', function(rf: RenderFlags, ctx: any) {
|
|
if (rf & RenderFlags.Create) {
|
|
element(0, 'div', ['dirCmp', '']);
|
|
}
|
|
}, 1, 0, [DirComp]);
|
|
|
|
/* <comp></comp> */
|
|
const App = createComponent('app', function(rf: RenderFlags, ctx: any) {
|
|
if (rf & RenderFlags.Create) {
|
|
element(0, 'comp');
|
|
}
|
|
}, 1, 0, [Comp]);
|
|
|
|
new ComponentFixture(App);
|
|
expect(dirComp !.comp instanceof Comp).toBeTruthy();
|
|
});
|
|
|
|
it('should not find providers on the host itself', () => {
|
|
/** <div dirString></div> */
|
|
const Comp = createComponent('comp', function(rf: RenderFlags, ctx: any) {
|
|
if (rf & RenderFlags.Create) {
|
|
element(0, 'div', ['dirString', '']);
|
|
}
|
|
}, 1, 0, [DirString], [], null, [{provide: String, useValue: 'Foo'}]);
|
|
|
|
/* <comp></comp> */
|
|
const App = createComponent('app', function(rf: RenderFlags, ctx: any) {
|
|
if (rf & RenderFlags.Create) {
|
|
element(0, 'comp');
|
|
}
|
|
}, 1, 0, [Comp]);
|
|
|
|
expect(() => {
|
|
new ComponentFixture(App);
|
|
}).toThrowError(/NodeInjector: NOT_FOUND \[String\]/);
|
|
});
|
|
|
|
it('should not find other directives on the host itself', () => {
|
|
/** <div dirA></div> */
|
|
const Comp = createComponent('comp', function(rf: RenderFlags, ctx: any) {
|
|
if (rf & RenderFlags.Create) {
|
|
element(0, 'div', ['dirA', '']);
|
|
}
|
|
}, 1, 0, [DirA]);
|
|
|
|
/* <comp dirB></comp> */
|
|
const App = createComponent('app', function(rf: RenderFlags, ctx: any) {
|
|
if (rf & RenderFlags.Create) {
|
|
element(0, 'comp', ['dirB', '']);
|
|
}
|
|
}, 1, 0, [Comp, DirB]);
|
|
|
|
expect(() => {
|
|
new ComponentFixture(App);
|
|
}).toThrowError(/NodeInjector: NOT_FOUND \[DirB\]/);
|
|
});
|
|
|
|
it('should not find providers on the host itself if in inline view', () => {
|
|
let comp !: any;
|
|
|
|
/**
|
|
* % if (showing) {
|
|
* <div dirA></div>
|
|
* % }
|
|
*/
|
|
const Comp = createComponent('comp', function(rf: RenderFlags, ctx: any) {
|
|
if (rf & RenderFlags.Create) {
|
|
container(0);
|
|
}
|
|
if (rf & RenderFlags.Update) {
|
|
containerRefreshStart(0);
|
|
{
|
|
if (ctx.showing) {
|
|
let rf1 = embeddedViewStart(0, 1, 0);
|
|
if (rf1 & RenderFlags.Create) {
|
|
element(0, 'div', ['dirA', '']);
|
|
}
|
|
embeddedViewEnd();
|
|
}
|
|
}
|
|
containerRefreshEnd();
|
|
}
|
|
}, 1, 0, [DirA, DirB]);
|
|
|
|
/* <comp dirB></comp> */
|
|
const App = createComponent('app', function(rf: RenderFlags, ctx: any) {
|
|
if (rf & RenderFlags.Create) {
|
|
element(0, 'comp', ['dirB', '']);
|
|
}
|
|
if (rf & RenderFlags.Update) {
|
|
comp = getDirectiveOnNode(0);
|
|
}
|
|
}, 1, 0, [Comp, DirB]);
|
|
|
|
const fixture = new ComponentFixture(App);
|
|
expect(() => {
|
|
comp.showing = true;
|
|
fixture.update();
|
|
}).toThrowError(/NodeInjector: NOT_FOUND \[DirB\]/);
|
|
});
|
|
|
|
it('should find providers across embedded views if not passing component boundary', () => {
|
|
let dirB !: DirB;
|
|
|
|
function IfTemplate(rf: RenderFlags, ctx: any) {
|
|
if (rf & RenderFlags.Create) {
|
|
element(0, 'div', ['dirA', '']);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* <div dirB>
|
|
* <div *ngIf="showing" dirA></div>
|
|
* </div>
|
|
*/
|
|
const App = createComponent('app', function(rf: RenderFlags, ctx: any) {
|
|
if (rf & RenderFlags.Create) {
|
|
elementStart(0, 'div', ['dirB', '']);
|
|
{ template(1, IfTemplate, 1, 0, 'div', ['ngIf', '']); }
|
|
elementEnd();
|
|
}
|
|
if (rf & RenderFlags.Update) {
|
|
elementProperty(1, 'ngIf', bind(ctx.showing));
|
|
|
|
// testing only
|
|
dirB = getDirectiveOnNode(0);
|
|
}
|
|
}, 2, 1, [NgIf, DirA, DirB]);
|
|
|
|
const fixture = new ComponentFixture(App);
|
|
fixture.component.showing = true;
|
|
fixture.update();
|
|
|
|
expect(dirA !.dirB).toEqual(dirB);
|
|
});
|
|
|
|
it('should not find component above the host', () => {
|
|
let dirComp: DirComp|null = null;
|
|
|
|
class DirComp {
|
|
constructor(@Host() public comp: any) {}
|
|
|
|
static ngDirectiveDef = defineDirective({
|
|
type: DirComp,
|
|
selectors: [['', 'dirCmp', '']],
|
|
factory: () => dirComp = new DirComp(directiveInject(App, InjectFlags.Host))
|
|
});
|
|
}
|
|
|
|
/** <div dirCmp></div> */
|
|
const Comp = createComponent('comp', function(rf: RenderFlags, ctx: any) {
|
|
if (rf & RenderFlags.Create) {
|
|
element(0, 'div', ['dirCmp', '']);
|
|
}
|
|
}, 1, 0, [DirComp]);
|
|
|
|
/* <comp></comp> */
|
|
class App {
|
|
static ngComponentDef = defineComponent({
|
|
type: App,
|
|
selectors: [['app']],
|
|
consts: 1,
|
|
vars: 0,
|
|
factory: () => new App,
|
|
template: function(rf: RenderFlags, ctx: any) {
|
|
if (rf & RenderFlags.Create) {
|
|
element(0, 'comp');
|
|
}
|
|
},
|
|
directives: [Comp],
|
|
});
|
|
}
|
|
|
|
expect(() => {
|
|
new ComponentFixture(App);
|
|
}).toThrowError(/NodeInjector: NOT_FOUND \[App\]/);
|
|
});
|
|
|
|
describe('regression', () => {
|
|
// based on https://stackblitz.com/edit/angular-riss8k?file=src/app/app.component.ts
|
|
it('should allow directives with Host flag to inject view providers from containing component',
|
|
() => {
|
|
let controlContainers: ControlContainer[] = [];
|
|
let injectedControlContainer: ControlContainer|null = null;
|
|
|
|
class ControlContainer {}
|
|
|
|
/*
|
|
@Directive({
|
|
selector: '[group]',
|
|
providers: [{provide: ControlContainer, useExisting: GroupDirective}]
|
|
})
|
|
*/
|
|
class GroupDirective {
|
|
constructor() { controlContainers.push(this); }
|
|
|
|
static ngDirectiveDef = defineDirective({
|
|
type: GroupDirective,
|
|
selectors: [['', 'group', '']],
|
|
factory: () => new GroupDirective(),
|
|
features: [ProvidersFeature(
|
|
[{provide: ControlContainer, useExisting: GroupDirective}])],
|
|
});
|
|
}
|
|
|
|
// @Directive({selector: '[controlName]'})
|
|
class ControlNameDirective {
|
|
constructor(@Host() @SkipSelf() @Inject(ControlContainer) parent:
|
|
ControlContainer) {
|
|
injectedControlContainer = parent;
|
|
}
|
|
|
|
static ngDirectiveDef = defineDirective({
|
|
type: ControlNameDirective,
|
|
selectors: [['', 'controlName', '']],
|
|
factory: () => new ControlNameDirective(directiveInject(
|
|
ControlContainer, InjectFlags.Host|InjectFlags.SkipSelf))
|
|
});
|
|
}
|
|
|
|
/*
|
|
@Component({
|
|
selector: 'child',
|
|
template: `
|
|
<input controlName type="text">
|
|
`,
|
|
viewProviders: [{provide: ControlContainer, useExisting: GroupDirective}]
|
|
})
|
|
*/
|
|
class ChildComponent {
|
|
static ngComponentDef = defineComponent({
|
|
type: ChildComponent,
|
|
selectors: [['child']],
|
|
consts: 1,
|
|
vars: 0,
|
|
factory: () => new ChildComponent(),
|
|
template: function(rf: RenderFlags, ctx: ChildComponent) {
|
|
if (rf & RenderFlags.Create) {
|
|
element(0, 'input', ['controlName', '', 'type', 'text']);
|
|
}
|
|
},
|
|
directives: [ControlNameDirective],
|
|
features: [ProvidersFeature(
|
|
[], [{provide: ControlContainer, useExisting: GroupDirective}])],
|
|
});
|
|
}
|
|
/*
|
|
@Component({
|
|
selector: 'my-app',
|
|
template: `
|
|
<div group>
|
|
<child></child>
|
|
</div>
|
|
`
|
|
})
|
|
*/
|
|
class AppComponent {
|
|
static ngComponentDef = defineComponent({
|
|
type: AppComponent,
|
|
selectors: [['my-app']],
|
|
consts: 2,
|
|
vars: 0,
|
|
factory: () => new AppComponent(),
|
|
template: function(rf: RenderFlags, ctx: AppComponent) {
|
|
if (rf & RenderFlags.Create) {
|
|
elementStart(0, 'div', ['group', '']);
|
|
element(1, 'child');
|
|
elementEnd();
|
|
}
|
|
},
|
|
directives: [ChildComponent, GroupDirective]
|
|
});
|
|
}
|
|
|
|
const fixture = new ComponentFixture(AppComponent as ComponentType<AppComponent>);
|
|
expect(fixture.html)
|
|
.toEqual(
|
|
'<div group=""><child><input controlname="" type="text"></child></div>');
|
|
|
|
expect(controlContainers).toEqual([injectedControlContainer !]);
|
|
|
|
});
|
|
});
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('Special tokens', () => {
|
|
|
|
describe('Injector', () => {
|
|
|
|
it('should inject the injector', () => {
|
|
let injectorDir !: InjectorDir;
|
|
let otherInjectorDir !: OtherInjectorDir;
|
|
let divElement !: HTMLElement;
|
|
|
|
class InjectorDir {
|
|
constructor(public injector: Injector) {}
|
|
|
|
static ngDirectiveDef = defineDirective({
|
|
type: InjectorDir,
|
|
selectors: [['', 'injectorDir', '']],
|
|
factory: () => injectorDir = new InjectorDir(directiveInject(Injector as any))
|
|
});
|
|
}
|
|
|
|
class OtherInjectorDir {
|
|
constructor(public otherDir: InjectorDir, public injector: Injector) {}
|
|
|
|
static ngDirectiveDef = defineDirective({
|
|
type: OtherInjectorDir,
|
|
selectors: [['', 'otherInjectorDir', '']],
|
|
factory: () => otherInjectorDir = new OtherInjectorDir(
|
|
directiveInject(InjectorDir), directiveInject(Injector as any))
|
|
});
|
|
}
|
|
|
|
|
|
/** <div injectorDir otherInjectorDir></div> */
|
|
const App = createComponent('app', (rf: RenderFlags, ctx: any) => {
|
|
if (rf & RenderFlags.Create) {
|
|
element(0, 'div', ['injectorDir', '', 'otherInjectorDir', '']);
|
|
}
|
|
// testing only
|
|
divElement = load(0);
|
|
}, 1, 0, [InjectorDir, OtherInjectorDir]);
|
|
|
|
const fixture = new ComponentFixture(App);
|
|
expect(injectorDir.injector.get(ElementRef).nativeElement).toBe(divElement);
|
|
expect(otherInjectorDir.injector.get(ElementRef).nativeElement).toBe(divElement);
|
|
expect(otherInjectorDir.injector.get(InjectorDir)).toBe(injectorDir);
|
|
expect(injectorDir.injector).not.toBe(otherInjectorDir.injector);
|
|
});
|
|
|
|
it('should inject INJECTOR', () => {
|
|
let injectorDir !: INJECTORDir;
|
|
let divElement !: HTMLElement;
|
|
|
|
class INJECTORDir {
|
|
constructor(public injector: Injector) {}
|
|
|
|
static ngDirectiveDef = defineDirective({
|
|
type: INJECTORDir,
|
|
selectors: [['', 'injectorDir', '']],
|
|
factory: () => injectorDir = new INJECTORDir(directiveInject(INJECTOR as any))
|
|
});
|
|
}
|
|
|
|
|
|
/** <div injectorDir otherInjectorDir></div> */
|
|
const App = createComponent('app', (rf: RenderFlags, ctx: any) => {
|
|
if (rf & RenderFlags.Create) {
|
|
element(0, 'div', ['injectorDir', '']);
|
|
}
|
|
// testing only
|
|
divElement = load(0);
|
|
}, 1, 0, [INJECTORDir]);
|
|
|
|
const fixture = new ComponentFixture(App);
|
|
expect(injectorDir.injector.get(ElementRef).nativeElement).toBe(divElement);
|
|
expect(injectorDir.injector.get(Injector).get(ElementRef).nativeElement).toBe(divElement);
|
|
expect(injectorDir.injector.get(INJECTOR).get(ElementRef).nativeElement).toBe(divElement);
|
|
});
|
|
|
|
});
|
|
|
|
describe('ElementRef', () => {
|
|
|
|
it('should create directive with ElementRef dependencies', () => {
|
|
let dir !: Directive;
|
|
let dirSameInstance !: DirectiveSameInstance;
|
|
let div !: RElement;
|
|
|
|
class Directive {
|
|
value: string;
|
|
constructor(public elementRef: ElementRef) {
|
|
this.value = (elementRef.constructor as any).name;
|
|
}
|
|
static ngDirectiveDef = defineDirective({
|
|
type: Directive,
|
|
selectors: [['', 'dir', '']],
|
|
factory: () => dir = new Directive(directiveInject(ElementRef)),
|
|
exportAs: ['dir']
|
|
});
|
|
}
|
|
|
|
class DirectiveSameInstance {
|
|
isSameInstance: boolean;
|
|
constructor(public elementRef: ElementRef, directive: Directive) {
|
|
this.isSameInstance = elementRef === directive.elementRef;
|
|
}
|
|
static ngDirectiveDef = defineDirective({
|
|
type: DirectiveSameInstance,
|
|
selectors: [['', 'dirSame', '']],
|
|
factory: () => dirSameInstance = new DirectiveSameInstance(
|
|
directiveInject(ElementRef), directiveInject(Directive)),
|
|
exportAs: ['dirSame']
|
|
});
|
|
}
|
|
|
|
/** <div dir dirSame></div> */
|
|
const App = createComponent('app', function(rf: RenderFlags, ctx: any) {
|
|
if (rf & RenderFlags.Create) {
|
|
elementStart(0, 'div', ['dir', '', 'dirSame', '']);
|
|
elementEnd();
|
|
div = getNativeByIndex(0, getLView());
|
|
}
|
|
}, 1, 0, [Directive, DirectiveSameInstance]);
|
|
|
|
const fixture = new ComponentFixture(App);
|
|
expect(dir.value).toContain('ElementRef');
|
|
expect(dir.elementRef.nativeElement).toEqual(div);
|
|
expect(dirSameInstance.elementRef.nativeElement).toEqual(div);
|
|
|
|
// Each ElementRef instance should be unique
|
|
expect(dirSameInstance.isSameInstance).toBe(false);
|
|
});
|
|
|
|
it('should create ElementRef with comment if requesting directive is on <ng-template> node',
|
|
() => {
|
|
let dir !: Directive;
|
|
let lContainer !: LContainer;
|
|
|
|
class Directive {
|
|
value: string;
|
|
constructor(public elementRef: ElementRef) {
|
|
this.value = (elementRef.constructor as any).name;
|
|
}
|
|
static ngDirectiveDef = defineDirective({
|
|
type: Directive,
|
|
selectors: [['', 'dir', '']],
|
|
factory: () => dir = new Directive(directiveInject(ElementRef)),
|
|
exportAs: ['dir']
|
|
});
|
|
}
|
|
|
|
/** <ng-template dir></ng-template> */
|
|
const App = createComponent('app', function(rf: RenderFlags, ctx: any) {
|
|
if (rf & RenderFlags.Create) {
|
|
template(0, () => {}, 0, 0, 'ng-template', ['dir', '']);
|
|
lContainer = load(0) as any;
|
|
}
|
|
}, 1, 0, [Directive]);
|
|
|
|
const fixture = new ComponentFixture(App);
|
|
expect(dir.value).toContain('ElementRef');
|
|
expect(dir.elementRef.nativeElement).toEqual(lContainer[NATIVE]);
|
|
});
|
|
});
|
|
|
|
describe('TemplateRef', () => {
|
|
class Directive {
|
|
value: string;
|
|
constructor(public templateRef: TemplateRef<any>) {
|
|
this.value = (templateRef.constructor as any).name;
|
|
}
|
|
static ngDirectiveDef = defineDirective({
|
|
type: Directive,
|
|
selectors: [['', 'dir', '']],
|
|
factory: () => new Directive(directiveInject(TemplateRef as any)),
|
|
exportAs: ['dir']
|
|
});
|
|
}
|
|
|
|
it('should create directive with TemplateRef dependencies', () => {
|
|
class DirectiveSameInstance {
|
|
isSameInstance: boolean;
|
|
constructor(templateRef: TemplateRef<any>, directive: Directive) {
|
|
this.isSameInstance = templateRef === directive.templateRef;
|
|
}
|
|
static ngDirectiveDef = defineDirective({
|
|
type: DirectiveSameInstance,
|
|
selectors: [['', 'dirSame', '']],
|
|
factory: () => new DirectiveSameInstance(
|
|
directiveInject(TemplateRef as any), directiveInject(Directive)),
|
|
exportAs: ['dirSame']
|
|
});
|
|
}
|
|
|
|
/**
|
|
* <ng-template dir dirSame #dir="dir" #dirSame="dirSame">
|
|
* {{ dir.value }} - {{ dirSame.value }}
|
|
* </ng-template>
|
|
*/
|
|
const App = createComponent('app', function(rf: RenderFlags, ctx: any) {
|
|
if (rf & RenderFlags.Create) {
|
|
template(
|
|
0, function() {}, 0, 0, 'ng-template', ['dir', '', 'dirSame', ''],
|
|
['dir', 'dir', 'dirSame', 'dirSame']);
|
|
text(3);
|
|
}
|
|
if (rf & RenderFlags.Update) {
|
|
const tmp1 = reference(1) as any;
|
|
const tmp2 = reference(2) as any;
|
|
textBinding(3, interpolation2('', tmp1.value, '-', tmp2.isSameInstance, ''));
|
|
}
|
|
}, 4, 2, [Directive, DirectiveSameInstance]);
|
|
|
|
const fixture = new ComponentFixture(App);
|
|
// Each TemplateRef instance should be unique
|
|
expect(fixture.html).toContain('TemplateRef');
|
|
expect(fixture.html).toContain('false');
|
|
});
|
|
|
|
it('should throw if injected on an element', () => {
|
|
/** <div dir></div> */
|
|
const App = createComponent('app', (rf: RenderFlags, ctx: any) => {
|
|
if (rf & RenderFlags.Create) {
|
|
element(0, 'div', ['dir', '']);
|
|
}
|
|
}, 1, 0, [Directive]);
|
|
|
|
expect(() => new ComponentFixture(App)).toThrowError(/No provider for TemplateRef/);
|
|
});
|
|
|
|
it('should throw if injected on an ng-container', () => {
|
|
/** <ng-container dir></ng-container> */
|
|
const App = createComponent('app', (rf: RenderFlags, ctx: any) => {
|
|
if (rf & RenderFlags.Create) {
|
|
elementContainerStart(0, ['dir', '']);
|
|
elementContainerEnd();
|
|
}
|
|
}, 1, 0, [Directive]);
|
|
|
|
expect(() => new ComponentFixture(App)).toThrowError(/No provider for TemplateRef/);
|
|
});
|
|
|
|
it('should NOT throw if optional and injected on an element', () => {
|
|
let dir !: OptionalDirective;
|
|
class OptionalDirective {
|
|
constructor(@Optional() public templateRef: TemplateRef<any>) {}
|
|
|
|
static ngDirectiveDef = defineDirective({
|
|
type: OptionalDirective,
|
|
selectors: [['', 'dir', '']],
|
|
factory: () => dir = new OptionalDirective(
|
|
directiveInject(TemplateRef as any, InjectFlags.Optional)),
|
|
exportAs: ['dir']
|
|
});
|
|
}
|
|
|
|
/** <div dir></div> */
|
|
const App = createComponent('app', (rf: RenderFlags, ctx: any) => {
|
|
if (rf & RenderFlags.Create) {
|
|
element(0, 'div', ['dir', '']);
|
|
}
|
|
}, 1, 0, [OptionalDirective]);
|
|
|
|
expect(() => new ComponentFixture(App)).not.toThrow();
|
|
expect(dir.templateRef).toBeNull();
|
|
});
|
|
|
|
});
|
|
|
|
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({
|
|
type: Directive,
|
|
selectors: [['', 'dir', '']],
|
|
factory: () => new Directive(directiveInject(ViewContainerRef as any)),
|
|
exportAs: ['dir']
|
|
});
|
|
}
|
|
|
|
class DirectiveSameInstance {
|
|
isSameInstance: boolean;
|
|
constructor(viewContainerRef: ViewContainerRef, directive: Directive) {
|
|
this.isSameInstance = viewContainerRef === directive.viewContainerRef;
|
|
}
|
|
static ngDirectiveDef = defineDirective({
|
|
type: DirectiveSameInstance,
|
|
selectors: [['', 'dirSame', '']],
|
|
factory: () => new DirectiveSameInstance(
|
|
directiveInject(ViewContainerRef as any), directiveInject(Directive)),
|
|
exportAs: ['dirSame']
|
|
});
|
|
}
|
|
|
|
/**
|
|
* <div dir dirSame #dir="dir" #dirSame="dirSame">
|
|
* {{ dir.value }} - {{ dirSame.value }}
|
|
* </div>
|
|
*/
|
|
const App = createComponent('app', function(rf: RenderFlags, ctx: any) {
|
|
if (rf & RenderFlags.Create) {
|
|
elementStart(
|
|
0, 'div', ['dir', '', 'dirSame', ''], ['dir', 'dir', 'dirSame', 'dirSame']);
|
|
{ text(3); }
|
|
elementEnd();
|
|
}
|
|
if (rf & RenderFlags.Update) {
|
|
const tmp1 = reference(1) as any;
|
|
const tmp2 = reference(2) as any;
|
|
textBinding(3, interpolation2('', tmp1.value, '-', tmp2.isSameInstance, ''));
|
|
}
|
|
}, 4, 2, [Directive, DirectiveSameInstance]);
|
|
|
|
const fixture = new ComponentFixture(App);
|
|
// Each ViewContainerRef instance should be unique
|
|
expect(fixture.html).toContain('ViewContainerRef');
|
|
expect(fixture.html).toContain('false');
|
|
});
|
|
});
|
|
|
|
describe('ChangeDetectorRef', () => {
|
|
let dir: Directive;
|
|
let dirSameInstance: DirectiveSameInstance;
|
|
let comp: MyComp;
|
|
|
|
class MyComp {
|
|
constructor(public cdr: ChangeDetectorRef) {}
|
|
|
|
static ngComponentDef = defineComponent({
|
|
type: MyComp,
|
|
selectors: [['my-comp']],
|
|
factory: () => comp = new MyComp(directiveInject(ChangeDetectorRef as any)),
|
|
consts: 1,
|
|
vars: 0,
|
|
template: function(rf: RenderFlags, ctx: MyComp) {
|
|
if (rf & RenderFlags.Create) {
|
|
projectionDef();
|
|
projection(0);
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
class Directive {
|
|
value: string;
|
|
|
|
constructor(public cdr: ChangeDetectorRef) { this.value = (cdr.constructor as any).name; }
|
|
|
|
static ngDirectiveDef = defineDirective({
|
|
type: Directive,
|
|
selectors: [['', 'dir', '']],
|
|
factory: () => dir = new Directive(directiveInject(ChangeDetectorRef as any)),
|
|
exportAs: ['dir']
|
|
});
|
|
}
|
|
|
|
class DirectiveSameInstance {
|
|
constructor(public cdr: ChangeDetectorRef) {}
|
|
|
|
static ngDirectiveDef = defineDirective({
|
|
type: DirectiveSameInstance,
|
|
selectors: [['', 'dirSame', '']],
|
|
factory: () => dirSameInstance =
|
|
new DirectiveSameInstance(directiveInject(ChangeDetectorRef as any))
|
|
});
|
|
}
|
|
|
|
const directives = [MyComp, Directive, DirectiveSameInstance, NgIf];
|
|
|
|
it('should inject current component ChangeDetectorRef into directives on the same node as components',
|
|
() => {
|
|
/** <my-comp dir dirSameInstance #dir="dir"></my-comp> {{ dir.value }} */
|
|
const MyApp = createComponent('my-app', function(rf: RenderFlags, ctx: any) {
|
|
if (rf & RenderFlags.Create) {
|
|
element(0, 'my-comp', ['dir', '', 'dirSame', ''], ['dir', 'dir']);
|
|
text(2);
|
|
}
|
|
if (rf & RenderFlags.Update) {
|
|
const tmp = reference(1) as any;
|
|
textBinding(2, bind(tmp.value));
|
|
}
|
|
}, 3, 1, directives);
|
|
|
|
const app = renderComponent(MyApp);
|
|
// ChangeDetectorRef is the token, ViewRef has historically been the constructor
|
|
expect(toHtml(app)).toEqual('<my-comp dir="" dirsame=""></my-comp>ViewRef');
|
|
expect((comp !.cdr as ViewRef<MyComp>).context).toBe(comp);
|
|
|
|
// Each ChangeDetectorRef instance should be unique
|
|
expect(dir !.cdr).not.toBe(comp !.cdr);
|
|
expect(dir !.cdr).not.toBe(dirSameInstance !.cdr);
|
|
});
|
|
|
|
it('should inject host component ChangeDetectorRef into directives on normal elements',
|
|
() => {
|
|
|
|
class MyApp {
|
|
constructor(public cdr: ChangeDetectorRef) {}
|
|
|
|
static ngComponentDef = defineComponent({
|
|
type: MyApp,
|
|
selectors: [['my-app']],
|
|
consts: 3,
|
|
vars: 1,
|
|
factory: () => new MyApp(directiveInject(ChangeDetectorRef as any)),
|
|
/** <div dir dirSameInstance #dir="dir"> {{ dir.value }} </div> */
|
|
template: function(rf: RenderFlags, ctx: any) {
|
|
if (rf & RenderFlags.Create) {
|
|
elementStart(0, 'div', ['dir', '', 'dirSame', ''], ['dir', 'dir']);
|
|
{ text(2); }
|
|
elementEnd();
|
|
}
|
|
if (rf & RenderFlags.Update) {
|
|
const tmp = reference(1) as any;
|
|
textBinding(2, bind(tmp.value));
|
|
}
|
|
},
|
|
directives: directives
|
|
});
|
|
}
|
|
|
|
const app = renderComponent(MyApp);
|
|
expect(toHtml(app)).toEqual('<div dir="" dirsame="">ViewRef</div>');
|
|
expect((app !.cdr as ViewRef<MyApp>).context).toBe(app);
|
|
|
|
// Each ChangeDetectorRef instance should be unique
|
|
expect(dir !.cdr).not.toBe(app.cdr);
|
|
expect(dir !.cdr).not.toBe(dirSameInstance !.cdr);
|
|
});
|
|
|
|
it('should inject host component ChangeDetectorRef into directives in a component\'s ContentChildren',
|
|
() => {
|
|
class MyApp {
|
|
constructor(public cdr: ChangeDetectorRef) {}
|
|
|
|
static ngComponentDef = defineComponent({
|
|
type: MyApp,
|
|
selectors: [['my-app']],
|
|
consts: 4,
|
|
vars: 1,
|
|
factory: () => new MyApp(directiveInject(ChangeDetectorRef as any)),
|
|
/**
|
|
* <my-comp>
|
|
* <div dir dirSameInstance #dir="dir"></div>
|
|
* </my-comp>
|
|
* {{ dir.value }}
|
|
*/
|
|
template: function(rf: RenderFlags, ctx: any) {
|
|
if (rf & RenderFlags.Create) {
|
|
elementStart(0, 'my-comp');
|
|
{ element(1, 'div', ['dir', '', 'dirSame', ''], ['dir', 'dir']); }
|
|
elementEnd();
|
|
text(3);
|
|
}
|
|
if (rf & RenderFlags.Update) {
|
|
const tmp = reference(2) as any;
|
|
textBinding(3, bind(tmp.value));
|
|
}
|
|
},
|
|
directives: directives
|
|
});
|
|
}
|
|
|
|
const app = renderComponent(MyApp);
|
|
expect(toHtml(app)).toEqual('<my-comp><div dir="" dirsame=""></div></my-comp>ViewRef');
|
|
expect((app !.cdr as ViewRef<MyApp>).context).toBe(app);
|
|
|
|
// Each ChangeDetectorRef instance should be unique
|
|
expect(dir !.cdr).not.toBe(app !.cdr);
|
|
expect(dir !.cdr).not.toBe(dirSameInstance !.cdr);
|
|
});
|
|
|
|
it('should inject host component ChangeDetectorRef into directives in embedded views', () => {
|
|
|
|
class MyApp {
|
|
showing = true;
|
|
|
|
constructor(public cdr: ChangeDetectorRef) {}
|
|
|
|
static ngComponentDef = defineComponent({
|
|
type: MyApp,
|
|
selectors: [['my-app']],
|
|
factory: () => new MyApp(directiveInject(ChangeDetectorRef as any)),
|
|
consts: 1,
|
|
vars: 0,
|
|
/**
|
|
* % if (showing) {
|
|
* <div dir dirSameInstance #dir="dir"> {{ dir.value }} </div>
|
|
* % }
|
|
*/
|
|
template: function(rf: RenderFlags, ctx: MyApp) {
|
|
if (rf & RenderFlags.Create) {
|
|
container(0);
|
|
}
|
|
if (rf & RenderFlags.Update) {
|
|
containerRefreshStart(0);
|
|
{
|
|
if (ctx.showing) {
|
|
let rf1 = embeddedViewStart(0, 3, 1);
|
|
if (rf1 & RenderFlags.Create) {
|
|
elementStart(0, 'div', ['dir', '', 'dirSame', ''], ['dir', 'dir']);
|
|
{ text(2); }
|
|
elementEnd();
|
|
}
|
|
if (rf1 & RenderFlags.Update) {
|
|
const tmp = reference(1) as any;
|
|
textBinding(2, bind(tmp.value));
|
|
}
|
|
}
|
|
embeddedViewEnd();
|
|
}
|
|
containerRefreshEnd();
|
|
}
|
|
},
|
|
directives: directives
|
|
});
|
|
}
|
|
|
|
const app = renderComponent(MyApp);
|
|
expect(toHtml(app)).toEqual('<div dir="" dirsame="">ViewRef</div>');
|
|
expect((app !.cdr as ViewRef<MyApp>).context).toBe(app);
|
|
|
|
// Each ChangeDetectorRef instance should be unique
|
|
expect(dir !.cdr).not.toBe(app.cdr);
|
|
expect(dir !.cdr).not.toBe(dirSameInstance !.cdr);
|
|
});
|
|
|
|
it('should inject host component ChangeDetectorRef into directives on containers', () => {
|
|
function C1(rf1: RenderFlags, ctx1: any) {
|
|
if (rf1 & RenderFlags.Create) {
|
|
elementStart(0, 'div', ['dir', '', 'dirSame', ''], ['dir', 'dir']);
|
|
{ text(2); }
|
|
elementEnd();
|
|
}
|
|
if (rf1 & RenderFlags.Update) {
|
|
const tmp = reference(1) as any;
|
|
textBinding(2, bind(tmp.value));
|
|
}
|
|
}
|
|
|
|
class MyApp {
|
|
showing = true;
|
|
|
|
constructor(public cdr: ChangeDetectorRef) {}
|
|
|
|
static ngComponentDef = defineComponent({
|
|
type: MyApp,
|
|
selectors: [['my-app']],
|
|
factory: () => new MyApp(directiveInject(ChangeDetectorRef as any)),
|
|
consts: 1,
|
|
vars: 0,
|
|
/** <div *ngIf="showing" dir dirSameInstance #dir="dir"> {{ dir.value }} </div> */
|
|
template: function(rf: RenderFlags, ctx: MyApp) {
|
|
if (rf & RenderFlags.Create) {
|
|
template(0, C1, 3, 1, 'div', ['ngIf', 'showing']);
|
|
}
|
|
},
|
|
directives: directives
|
|
});
|
|
}
|
|
|
|
const app = renderComponent(MyApp);
|
|
expect(toHtml(app)).toEqual('<div dir="" dirsame="">ViewRef</div>');
|
|
expect((app !.cdr as ViewRef<MyApp>).context).toBe(app);
|
|
|
|
// Each ChangeDetectorRef instance should be unique
|
|
expect(dir !.cdr).not.toBe(app.cdr);
|
|
expect(dir !.cdr).not.toBe(dirSameInstance !.cdr);
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('string tokens', () => {
|
|
it('should be able to provide a string token', () => {
|
|
let injectorDir !: InjectorDir;
|
|
let divElement !: HTMLElement;
|
|
|
|
class InjectorDir {
|
|
constructor(public value: string) {}
|
|
|
|
static ngDirectiveDef = defineDirective({
|
|
type: InjectorDir,
|
|
selectors: [['', 'injectorDir', '']],
|
|
factory: () => injectorDir = new InjectorDir(directiveInject('test' as any)),
|
|
features: [ProvidersFeature([{provide: 'test', useValue: 'provided'}])],
|
|
});
|
|
}
|
|
|
|
/** <div injectorDir otherInjectorDir></div> */
|
|
const App = createComponent('app', (rf: RenderFlags, ctx: any) => {
|
|
if (rf & RenderFlags.Create) {
|
|
element(0, 'div', ['injectorDir', '']);
|
|
}
|
|
// testing only
|
|
divElement = load(0);
|
|
}, 1, 0, [InjectorDir]);
|
|
|
|
const fixture = new ComponentFixture(App);
|
|
expect(injectorDir.value).toBe('provided');
|
|
});
|
|
});
|
|
|
|
describe('Renderer2', () => {
|
|
class MyComp {
|
|
constructor(public renderer: Renderer2) {}
|
|
|
|
static ngComponentDef = defineComponent({
|
|
type: MyComp,
|
|
selectors: [['my-comp']],
|
|
factory: () => new MyComp(directiveInject(Renderer2 as any)),
|
|
consts: 1,
|
|
vars: 0,
|
|
template: function(rf: RenderFlags, ctx: MyComp) {
|
|
if (rf & RenderFlags.Create) {
|
|
text(0, 'Foo');
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
it('should inject the Renderer2 used by the application', () => {
|
|
const rendererFactory = getRendererFactory2(document);
|
|
const fixture = new ComponentFixture(MyComp, {rendererFactory: rendererFactory});
|
|
expect(isProceduralRenderer(fixture.component.renderer)).toBeTruthy();
|
|
});
|
|
|
|
it('should throw when injecting Renderer2 but the application is using Renderer3',
|
|
() => { expect(() => new ComponentFixture(MyComp)).toThrow(); });
|
|
});
|
|
|
|
describe('@Attribute', () => {
|
|
let myDirectiveInstance !: MyDirective | null;
|
|
|
|
class MyDirective {
|
|
exists = 'wrong' as string | null;
|
|
myDirective = 'wrong' as string | null;
|
|
constructor(
|
|
@Attribute('exist') existAttrValue: string|null,
|
|
@Attribute('myDirective') myDirectiveAttrValue: string|null) {
|
|
this.exists = existAttrValue;
|
|
this.myDirective = myDirectiveAttrValue;
|
|
}
|
|
|
|
static ngDirectiveDef = defineDirective({
|
|
type: MyDirective,
|
|
selectors: [['', 'myDirective', '']],
|
|
factory: () => myDirectiveInstance =
|
|
new MyDirective(injectAttribute('exist'), injectAttribute('myDirective'))
|
|
});
|
|
}
|
|
|
|
beforeEach(() => myDirectiveInstance = null);
|
|
|
|
it('should inject attribute', () => {
|
|
let exist = 'wrong' as string | null;
|
|
let nonExist = 'wrong' as string | null;
|
|
|
|
const MyApp = createComponent('my-app', function(rf: RenderFlags, ctx: any) {
|
|
if (rf & RenderFlags.Create) {
|
|
elementStart(0, 'div', ['exist', 'existValue', 'other', 'ignore']);
|
|
exist = injectAttribute('exist');
|
|
nonExist = injectAttribute('nonExist');
|
|
}
|
|
}, 1);
|
|
|
|
new ComponentFixture(MyApp);
|
|
expect(exist).toEqual('existValue');
|
|
expect(nonExist).toBeNull();
|
|
});
|
|
|
|
// https://stackblitz.com/edit/angular-scawyi?file=src%2Fapp%2Fapp.component.ts
|
|
it('should inject attributes on <ng-template>', () => {
|
|
let myDirectiveInstance: MyDirective;
|
|
|
|
/* <ng-template myDirective="initial" exist="existValue" other="ignore"></ng-template>*/
|
|
const MyApp = createComponent('my-app', function(rf: RenderFlags, ctx: any) {
|
|
if (rf & RenderFlags.Create) {
|
|
template(
|
|
0, null, 0, 0, 'ng-template',
|
|
['myDirective', 'initial', 'exist', 'existValue', 'other', 'ignore']);
|
|
}
|
|
if (rf & RenderFlags.Update) {
|
|
myDirectiveInstance = getDirectiveOnNode(0);
|
|
}
|
|
}, 1, 0, [MyDirective]);
|
|
|
|
new ComponentFixture(MyApp);
|
|
expect(myDirectiveInstance !.exists).toEqual('existValue');
|
|
expect(myDirectiveInstance !.myDirective).toEqual('initial');
|
|
});
|
|
|
|
// https://stackblitz.com/edit/angular-scawyi?file=src%2Fapp%2Fapp.component.ts
|
|
it('should inject attributes on <ng-container>', () => {
|
|
let myDirectiveInstance: MyDirective;
|
|
|
|
/* <ng-container myDirective="initial" exist="existValue" other="ignore"></ng-container>*/
|
|
const MyApp = createComponent('my-app', function(rf: RenderFlags, ctx: any) {
|
|
if (rf & RenderFlags.Create) {
|
|
elementContainerStart(
|
|
0, ['myDirective', 'initial', 'exist', 'existValue', 'other', 'ignore']);
|
|
elementContainerEnd();
|
|
}
|
|
if (rf & RenderFlags.Update) {
|
|
myDirectiveInstance = getDirectiveOnNode(0);
|
|
}
|
|
}, 1, 0, [MyDirective]);
|
|
|
|
new ComponentFixture(MyApp);
|
|
expect(myDirectiveInstance !.exists).toEqual('existValue');
|
|
expect(myDirectiveInstance !.myDirective).toEqual('initial');
|
|
});
|
|
|
|
// https://stackblitz.com/edit/angular-8ytqkp?file=src%2Fapp%2Fapp.component.ts
|
|
it('should not inject attributes representing bindings and outputs', () => {
|
|
let exist = 'wrong' as string | null;
|
|
let nonExist = 'wrong' as string | null;
|
|
|
|
const MyApp = createComponent('my-app', function(rf: RenderFlags, ctx: any) {
|
|
if (rf & RenderFlags.Create) {
|
|
elementStart(0, 'div', ['exist', 'existValue', AttributeMarker.SelectOnly, 'nonExist']);
|
|
exist = injectAttribute('exist');
|
|
nonExist = injectAttribute('nonExist');
|
|
}
|
|
}, 1);
|
|
|
|
new ComponentFixture(MyApp);
|
|
expect(exist).toEqual('existValue');
|
|
expect(nonExist).toBeNull();
|
|
});
|
|
|
|
it('should not accidentally inject attributes representing bindings and outputs', () => {
|
|
let exist = 'wrong' as string | null;
|
|
let nonExist = 'wrong' as string | null;
|
|
|
|
const MyApp = createComponent('my-app', function(rf: RenderFlags, ctx: any) {
|
|
if (rf & RenderFlags.Create) {
|
|
elementStart(0, 'div', [
|
|
'exist', 'existValue', AttributeMarker.SelectOnly, 'binding1', 'nonExist', 'binding2'
|
|
]);
|
|
exist = injectAttribute('exist');
|
|
nonExist = injectAttribute('nonExist');
|
|
}
|
|
}, 1);
|
|
|
|
new ComponentFixture(MyApp);
|
|
expect(exist).toEqual('existValue');
|
|
expect(nonExist).toBeNull();
|
|
});
|
|
});
|
|
|
|
describe('inject', () => {
|
|
describe('bloom filter', () => {
|
|
let mockTView: any;
|
|
beforeEach(() => {
|
|
mockTView = {data: [0, 0, 0, 0, 0, 0, 0, 0, null], firstTemplatePass: true};
|
|
});
|
|
|
|
function bloomState() { return mockTView.data.slice(0, TNODE).reverse(); }
|
|
|
|
class Dir0 {
|
|
/** @internal */ static __NG_ELEMENT_ID__ = 0;
|
|
}
|
|
class Dir1 {
|
|
/** @internal */ static __NG_ELEMENT_ID__ = 1;
|
|
}
|
|
class Dir33 {
|
|
/** @internal */ static __NG_ELEMENT_ID__ = 33;
|
|
}
|
|
class Dir66 {
|
|
/** @internal */ static __NG_ELEMENT_ID__ = 66;
|
|
}
|
|
class Dir99 {
|
|
/** @internal */ static __NG_ELEMENT_ID__ = 99;
|
|
}
|
|
class Dir132 {
|
|
/** @internal */ static __NG_ELEMENT_ID__ = 132;
|
|
}
|
|
class Dir165 {
|
|
/** @internal */ static __NG_ELEMENT_ID__ = 165;
|
|
}
|
|
class Dir198 {
|
|
/** @internal */ static __NG_ELEMENT_ID__ = 198;
|
|
}
|
|
class Dir231 {
|
|
/** @internal */ static __NG_ELEMENT_ID__ = 231;
|
|
}
|
|
|
|
it('should add values', () => {
|
|
bloomAdd(0, mockTView, Dir0);
|
|
expect(bloomState()).toEqual([0, 0, 0, 0, 0, 0, 0, 1]);
|
|
bloomAdd(0, mockTView, Dir33);
|
|
expect(bloomState()).toEqual([0, 0, 0, 0, 0, 0, 2, 1]);
|
|
bloomAdd(0, mockTView, Dir66);
|
|
expect(bloomState()).toEqual([0, 0, 0, 0, 0, 4, 2, 1]);
|
|
bloomAdd(0, mockTView, Dir99);
|
|
expect(bloomState()).toEqual([0, 0, 0, 0, 8, 4, 2, 1]);
|
|
bloomAdd(0, mockTView, Dir132);
|
|
expect(bloomState()).toEqual([0, 0, 0, 16, 8, 4, 2, 1]);
|
|
bloomAdd(0, mockTView, Dir165);
|
|
expect(bloomState()).toEqual([0, 0, 32, 16, 8, 4, 2, 1]);
|
|
bloomAdd(0, mockTView, Dir198);
|
|
expect(bloomState()).toEqual([0, 64, 32, 16, 8, 4, 2, 1]);
|
|
bloomAdd(0, mockTView, Dir231);
|
|
expect(bloomState()).toEqual([128, 64, 32, 16, 8, 4, 2, 1]);
|
|
});
|
|
|
|
it('should query values', () => {
|
|
bloomAdd(0, mockTView, Dir0);
|
|
bloomAdd(0, mockTView, Dir33);
|
|
bloomAdd(0, mockTView, Dir66);
|
|
bloomAdd(0, mockTView, Dir99);
|
|
bloomAdd(0, mockTView, Dir132);
|
|
bloomAdd(0, mockTView, Dir165);
|
|
bloomAdd(0, mockTView, Dir198);
|
|
bloomAdd(0, mockTView, Dir231);
|
|
|
|
expect(bloomHasToken(bloomHash(Dir0) as number, 0, mockTView.data)).toEqual(true);
|
|
expect(bloomHasToken(bloomHash(Dir1) as number, 0, mockTView.data)).toEqual(false);
|
|
expect(bloomHasToken(bloomHash(Dir33) as number, 0, mockTView.data)).toEqual(true);
|
|
expect(bloomHasToken(bloomHash(Dir66) as number, 0, mockTView.data)).toEqual(true);
|
|
expect(bloomHasToken(bloomHash(Dir99) as number, 0, mockTView.data)).toEqual(true);
|
|
expect(bloomHasToken(bloomHash(Dir132) as number, 0, mockTView.data)).toEqual(true);
|
|
expect(bloomHasToken(bloomHash(Dir165) as number, 0, mockTView.data)).toEqual(true);
|
|
expect(bloomHasToken(bloomHash(Dir198) as number, 0, mockTView.data)).toEqual(true);
|
|
expect(bloomHasToken(bloomHash(Dir231) as number, 0, mockTView.data)).toEqual(true);
|
|
});
|
|
});
|
|
|
|
it('should inject from parent view', () => {
|
|
const ParentDirective = createDirective('parentDir');
|
|
|
|
class ChildDirective {
|
|
value: string;
|
|
constructor(public parent: any) { this.value = (parent.constructor as any).name; }
|
|
static ngDirectiveDef = defineDirective({
|
|
type: ChildDirective,
|
|
selectors: [['', 'childDir', '']],
|
|
factory: () => new ChildDirective(directiveInject(ParentDirective)),
|
|
exportAs: ['childDir']
|
|
});
|
|
}
|
|
|
|
class Child2Directive {
|
|
value: boolean;
|
|
constructor(parent: any, child: ChildDirective) { this.value = parent === child.parent; }
|
|
static ngDirectiveDef = defineDirective({
|
|
selectors: [['', 'child2Dir', '']],
|
|
type: Child2Directive,
|
|
factory: () => new Child2Directive(
|
|
directiveInject(ParentDirective), directiveInject(ChildDirective)),
|
|
exportAs: ['child2Dir']
|
|
});
|
|
}
|
|
|
|
/**
|
|
* <div parentDir>
|
|
* % if (...) {
|
|
* <span childDir child2Dir #child1="childDir" #child2="child2Dir">
|
|
* {{ child1.value }} - {{ child2.value }}
|
|
* </span>
|
|
* % }
|
|
* </div>
|
|
*/
|
|
const App = createComponent('app', function(rf: RenderFlags, ctx: any) {
|
|
if (rf & RenderFlags.Create) {
|
|
elementStart(0, 'div', ['parentDir', '']);
|
|
{ container(1); }
|
|
elementEnd();
|
|
}
|
|
if (rf & RenderFlags.Update) {
|
|
containerRefreshStart(1);
|
|
{
|
|
let rf1 = embeddedViewStart(0, 4, 2);
|
|
if (rf1 & RenderFlags.Create) {
|
|
elementStart(
|
|
0, 'span', ['childDir', '', 'child2Dir', ''],
|
|
['child1', 'childDir', 'child2', 'child2Dir']);
|
|
{ text(3); }
|
|
elementEnd();
|
|
}
|
|
if (rf & RenderFlags.Update) {
|
|
const tmp1 = reference(1) as any;
|
|
const tmp2 = reference(2) as any;
|
|
textBinding(3, interpolation2('', tmp1.value, '-', tmp2.value, ''));
|
|
}
|
|
embeddedViewEnd();
|
|
}
|
|
containerRefreshEnd();
|
|
}
|
|
}, 2, 0, [ChildDirective, Child2Directive, ParentDirective]);
|
|
|
|
const fixture = new ComponentFixture(App);
|
|
expect(fixture.html)
|
|
.toEqual('<div parentdir=""><span child2dir="" childdir="">Directive-true</span></div>');
|
|
});
|
|
|
|
it('should inject from module Injector', () => {
|
|
|
|
});
|
|
});
|
|
|
|
describe('getOrCreateNodeInjector', () => {
|
|
it('should handle initial undefined state', () => {
|
|
const contentView = createLView(
|
|
null, createTView(-1, null, 1, 0, null, null, null), null, LViewFlags.CheckAlways,
|
|
{} as any, {} as any);
|
|
const oldView = enterView(contentView, null);
|
|
try {
|
|
const parentTNode = createNodeAtIndex(0, TNodeType.Element, null, 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.
|
|
(parentTNode as{parent: any}).parent = undefined;
|
|
|
|
const injector = getOrCreateNodeInjectorForNode(parentTNode, contentView);
|
|
expect(injector).not.toEqual(-1);
|
|
} finally {
|
|
leaveView(oldView);
|
|
}
|
|
});
|
|
});
|
|
|
|
});
|