/**
* @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 {CommonModule} from '@angular/common';
import {ChangeDetectorRef, Component, Directive, TemplateRef, ViewChild, ViewContainerRef} from '@angular/core';
import {Input} from '@angular/core/src/metadata';
import {TestBed} from '@angular/core/testing';
import {By} from '@angular/platform-browser';
import {expect} from '@angular/platform-browser/testing/src/matchers';
describe('projection', () => {
function getElementHtml(element: HTMLElement) {
return element.innerHTML.replace(//g, '')
.replace(/\sng-reflect-\S*="[^"]*"/g, '');
}
it('should project content', () => {
@Component({selector: 'child', template: `
`})
class Child {
}
@Component({selector: 'parent', template: 'content '})
class Parent {
}
TestBed.configureTestingModule({declarations: [Parent, Child]});
const fixture = TestBed.createComponent(Parent);
fixture.detectChanges();
expect(fixture.nativeElement.innerHTML).toBe(`content
`);
});
it('should project content when is at a template root', () => {
@Component({
selector: 'child',
template: ' ',
})
class Child {
}
@Component({
selector: 'parent',
template: 'content ',
})
class Parent {
}
TestBed.configureTestingModule({declarations: [Parent, Child]});
const fixture = TestBed.createComponent(Parent);
fixture.detectChanges();
expect(fixture.nativeElement.innerHTML).toBe(`content `);
});
it('should project content with siblings', () => {
@Component({selector: 'child', template: ' '})
class Child {
}
@Component({selector: 'parent', template: `beforecontent
after `})
class Parent {
}
TestBed.configureTestingModule({declarations: [Parent, Child]});
const fixture = TestBed.createComponent(Parent);
fixture.detectChanges();
expect(fixture.nativeElement.innerHTML).toBe(`beforecontent
after `);
});
it('should be able to re-project content', () => {
@Component({selector: 'grand-child', template: `
`})
class GrandChild {
}
@Component(
{selector: 'child', template: ` `})
class Child {
}
@Component({
selector: 'parent',
template: `Hello World! `,
})
class Parent {
}
TestBed.configureTestingModule({declarations: [Parent, Child, GrandChild]});
const fixture = TestBed.createComponent(Parent);
fixture.detectChanges();
expect(fixture.nativeElement.innerHTML)
.toBe('Hello World!
');
});
it('should project components', () => {
@Component({
selector: 'child',
template: `
`,
})
class Child {
}
@Component({
selector: 'projected-comp',
template: 'content',
})
class ProjectedComp {
}
@Component({selector: 'parent', template: ` `})
class Parent {
}
TestBed.configureTestingModule({declarations: [Parent, Child, ProjectedComp]});
const fixture = TestBed.createComponent(Parent);
fixture.detectChanges();
expect(fixture.nativeElement.innerHTML)
.toBe(' ');
});
it('should project components that have their own projection', () => {
@Component({selector: 'child', template: `
`})
class Child {
}
@Component({selector: 'projected-comp', template: `
`})
class ProjectedComp {
}
@Component({
selector: 'parent',
template: `
Some content
Other content
`,
})
class Parent {
}
TestBed.configureTestingModule({declarations: [Parent, Child, ProjectedComp]});
const fixture = TestBed.createComponent(Parent);
fixture.detectChanges();
expect(fixture.nativeElement.innerHTML)
.toBe(
`
Some content
Other content `);
});
it('should project with multiple instances of a component with projection', () => {
@Component({selector: 'child', template: `
`})
class Child {
}
@Component({selector: 'projected-comp', template: `Before After`})
class ProjectedComp {
}
@Component({
selector: 'parent',
template: `
A
123
B
456
`,
})
class Parent {
}
TestBed.configureTestingModule({declarations: [Parent, Child, ProjectedComp]});
const fixture = TestBed.createComponent(Parent);
fixture.detectChanges();
expect(fixture.nativeElement.innerHTML)
.toBe(
'' +
'
BeforeA
123
After ' +
'
BeforeB
456
After ' +
'
');
});
it('should re-project with multiple instances of a component with projection', () => {
@Component({selector: 'child', template: `
`})
class Child {
}
@Component({selector: 'projected-comp', template: `Before After`})
class ProjectedComp {
}
@Component({
selector: 'parent',
template: `
A
123
B
456
`,
})
class Parent {
}
@Component({
selector: 'app',
template: `
**ABC**
**DEF**
`,
})
class App {
}
TestBed.configureTestingModule({declarations: [App, Parent, Child, ProjectedComp]});
const fixture = TestBed.createComponent(App);
fixture.detectChanges();
expect(fixture.nativeElement.innerHTML)
.toBe(
'' +
'
BeforeA
**ABC**123
After ' +
'
BeforeB
456
After ' +
'
' +
'' +
'
BeforeA
**DEF**123
After ' +
'
BeforeB
456
After ' +
'
');
});
it('should project into dynamic views (with createEmbeddedView)', () => {
@Component({
selector: 'child',
template: `Before- -After`
})
class Child {
showing = false;
}
@Component({selector: 'parent', template: `A
Some text `})
class Parent {
}
TestBed.configureTestingModule({declarations: [Parent, Child], imports: [CommonModule]});
const fixture = TestBed.createComponent(Parent);
const childDebugEl = fixture.debugElement.query(By.directive(Child));
const childInstance = childDebugEl.injector.get(Child);
const childElement = childDebugEl.nativeElement as HTMLElement;
childInstance.showing = true;
fixture.detectChanges();
expect(getElementHtml(childElement)).toBe(`Before-A
Some text-After`);
childInstance.showing = false;
fixture.detectChanges();
expect(getElementHtml(childElement)).toBe(`Before--After`);
childInstance.showing = true;
fixture.detectChanges();
expect(getElementHtml(childElement)).toBe(`Before-A
Some text-After`);
});
it('should project into dynamic views with specific selectors', () => {
@Component({
selector: 'child',
template: `
Before-
-After`
})
class Child {
showing = false;
}
@Component({
selector: 'parent',
template: `
A
B
`
})
class Parent {
}
TestBed.configureTestingModule({declarations: [Parent, Child], imports: [CommonModule]});
const fixture = TestBed.createComponent(Parent);
const childDebugEl = fixture.debugElement.query(By.directive(Child));
const childInstance = childDebugEl.injector.get(Child);
childInstance.showing = true;
fixture.detectChanges();
expect(getElementHtml(fixture.nativeElement))
.toBe('B Before- A
-After ');
childInstance.showing = false;
fixture.detectChanges();
expect(getElementHtml(fixture.nativeElement))
.toBe('B Before- -After ');
childInstance.showing = true;
fixture.detectChanges();
expect(getElementHtml(fixture.nativeElement))
.toBe('B Before- A
-After ');
});
it('should project if is in a template that has different declaration/insertion points',
() => {
@Component(
{selector: 'comp', template: ` `})
class Comp {
@ViewChild(TemplateRef, {static: true}) template !: TemplateRef;
}
@Directive({selector: '[trigger]'})
class Trigger {
@Input() trigger !: Comp;
constructor(public vcr: ViewContainerRef) {}
open() { this.vcr.createEmbeddedView(this.trigger.template); }
}
@Component({
selector: 'parent',
template: `
Some content
`
})
class Parent {
}
TestBed.configureTestingModule({declarations: [Parent, Trigger, Comp]});
const fixture = TestBed.createComponent(Parent);
const trigger = fixture.debugElement.query(By.directive(Trigger)).injector.get(Trigger);
fixture.detectChanges();
expect(getElementHtml(fixture.nativeElement)).toBe(` `);
trigger.open();
expect(getElementHtml(fixture.nativeElement))
.toBe(` Some content `);
});
it('should project nodes into the last ng-content', () => {
@Component({
selector: 'child',
template: `
`
})
class Child {
}
@Component({selector: 'parent', template: `content `})
class Parent {
}
TestBed.configureTestingModule({declarations: [Parent, Child], imports: [CommonModule]});
const fixture = TestBed.createComponent(Parent);
fixture.detectChanges();
expect(getElementHtml(fixture.nativeElement))
.toBe('
content ');
});
// https://stackblitz.com/edit/angular-ceqmnw?file=src%2Fapp%2Fapp.component.ts
it('should project nodes into the last ng-content unrolled by ngFor', () => {
@Component({
selector: 'child',
template:
`({{i}}):
`
})
class Child {
}
@Component({selector: 'parent', template: `content `})
class Parent {
}
TestBed.configureTestingModule({declarations: [Parent, Child], imports: [CommonModule]});
const fixture = TestBed.createComponent(Parent);
fixture.detectChanges();
expect(getElementHtml(fixture.nativeElement))
.toBe('(0):
(1):content
');
});
it('should handle projected containers inside other containers', () => {
@Component({selector: 'nested-comp', template: `Child content
`})
class NestedComp {
}
@Component({
selector: 'root-comp',
template: ` `,
})
class RootComp {
}
@Component({
selector: 'my-app',
template: `
`
})
class MyApp {
items = [1, 2];
}
TestBed.configureTestingModule(
{declarations: [MyApp, RootComp, NestedComp], imports: [CommonModule]});
const fixture = TestBed.createComponent(MyApp);
fixture.detectChanges();
// expecting # of divs to be (items.length - 1), since last element is filtered out by *ngIf,
// this applies to all other assertions below
expect(fixture.nativeElement.querySelectorAll('div').length).toBe(1);
fixture.componentInstance.items = [3, 4, 5];
fixture.detectChanges();
expect(fixture.nativeElement.querySelectorAll('div').length).toBe(2);
fixture.componentInstance.items = [6, 7, 8, 9];
fixture.detectChanges();
expect(fixture.nativeElement.querySelectorAll('div').length).toBe(3);
});
it('should handle projection into element containers at the view root', () => {
@Component({
selector: 'root-comp',
template: `
`,
})
class RootComp {
@Input() show: boolean = true;
}
@Component({
selector: 'my-app',
template: `
`
})
class MyApp {
show = true;
}
TestBed.configureTestingModule({declarations: [MyApp, RootComp]});
const fixture = TestBed.createComponent(MyApp);
fixture.detectChanges();
expect(fixture.nativeElement.querySelectorAll('div').length).toBe(1);
fixture.componentInstance.show = false;
fixture.detectChanges();
expect(fixture.nativeElement.querySelectorAll('div').length).toBe(0);
});
it('should handle projection of views with element containers at the root', () => {
@Component({
selector: 'root-comp',
template: ` `,
})
class RootComp {
@Input() show: boolean = true;
}
@Component({
selector: 'my-app',
template: `
`
})
class MyApp {
show = true;
}
TestBed.configureTestingModule({declarations: [MyApp, RootComp]});
const fixture = TestBed.createComponent(MyApp);
fixture.detectChanges();
expect(fixture.nativeElement.querySelectorAll('div').length).toBe(1);
fixture.componentInstance.show = false;
fixture.detectChanges();
expect(fixture.nativeElement.querySelectorAll('div').length).toBe(0);
});
it('should project ng-container at the content root', () => {
@Component({selector: 'child', template: ` `})
class Child {
}
@Component({
selector: 'parent',
template: `
content
`
})
class Parent {
}
TestBed.configureTestingModule({declarations: [Parent, Child]});
const fixture = TestBed.createComponent(Parent);
fixture.detectChanges();
expect(getElementHtml(fixture.nativeElement)).toBe('content ');
});
it('should re-project ng-container at the content root', () => {
@Component({selector: 'grand-child', template: ` `})
class GrandChild {
}
@Component({
selector: 'child',
template: `
`
})
class Child {
}
@Component({
selector: 'parent',
template: `
content
`
})
class Parent {
}
TestBed.configureTestingModule({declarations: [Parent, Child, GrandChild]});
const fixture = TestBed.createComponent(Parent);
fixture.detectChanges();
expect(getElementHtml(fixture.nativeElement))
.toBe('content ');
});
it('should handle re-projection at the root of an embedded view', () => {
@Component({
selector: 'child-comp',
template: ` `,
})
class ChildComp {
@Input() show: boolean = true;
}
@Component({
selector: 'parent-comp',
template: ` `
})
class ParentComp {
@Input() show: boolean = true;
}
@Component(
{selector: 'my-app', template: `
`})
class MyApp {
show = true;
}
TestBed.configureTestingModule({declarations: [MyApp, ParentComp, ChildComp]});
const fixture = TestBed.createComponent(MyApp);
fixture.detectChanges();
expect(fixture.nativeElement.querySelectorAll('div').length).toBe(1);
fixture.componentInstance.show = false;
fixture.detectChanges();
expect(fixture.nativeElement.querySelectorAll('div').length).toBe(0);
});
describe('with selectors', () => {
it('should project nodes using attribute selectors', () => {
@Component({
selector: 'child',
template: `
`,
})
class Child {
}
@Component({
selector: 'parent',
template: `1 2 `
})
class Parent {
}
TestBed.configureTestingModule({declarations: [Child, Parent]});
const fixture = TestBed.createComponent(Parent);
fixture.detectChanges();
expect(getElementHtml(fixture.nativeElement))
.toEqual(
'1
2
');
});
it('should project nodes using class selectors', () => {
@Component({
selector: 'child',
template: `
`,
})
class Child {
}
@Component({
selector: 'parent',
template: `1 2 `
})
class Parent {
}
TestBed.configureTestingModule({declarations: [Child, Parent]});
const fixture = TestBed.createComponent(Parent);
fixture.detectChanges();
expect(getElementHtml(fixture.nativeElement))
.toEqual(
'1
2
');
});
it('should project nodes using class selectors when element has multiple classes', () => {
@Component({
selector: 'child',
template: `
`
})
class Child {
}
@Component({
selector: 'parent',
template:
`1 2 `
})
class Parent {
}
TestBed.configureTestingModule({declarations: [Child, Parent]});
const fixture = TestBed.createComponent(Parent);
fixture.detectChanges();
expect(getElementHtml(fixture.nativeElement))
.toEqual(
'1
2
');
});
it('should project nodes into the first matching selector', () => {
@Component({
selector: 'child',
template: `
`
})
class Child {
}
@Component({
selector: 'parent',
template: `1 2 `
})
class Parent {
}
TestBed.configureTestingModule({declarations: [Child, Parent]});
const fixture = TestBed.createComponent(Parent);
fixture.detectChanges();
expect(getElementHtml(fixture.nativeElement))
.toEqual(
'1 2
');
});
it('should allow mixing ng-content with and without selectors', () => {
@Component({
selector: 'child',
template: `
`
})
class Child {
}
@Component({
selector: 'parent',
template:
`1 remaining more remaining `
})
class Parent {
}
TestBed.configureTestingModule({declarations: [Child, Parent]});
const fixture = TestBed.createComponent(Parent);
fixture.detectChanges();
expect(getElementHtml(fixture.nativeElement))
.toEqual(
'1
remaining more remaining
');
});
it('should allow mixing ng-content with and without selectors - ng-content first', () => {
@Component({
selector: 'child',
template: `
`
})
class Child {
}
@Component({
selector: 'parent',
template: `1 2 remaining `
})
class Parent {
}
TestBed.configureTestingModule({declarations: [Child, Parent]});
const fixture = TestBed.createComponent(Parent);
fixture.detectChanges();
expect(getElementHtml(fixture.nativeElement))
.toEqual(
'1 remaining
2
');
});
/**
* Descending into projected content for selector-matching purposes is not supported
* today: http://plnkr.co/edit/MYQcNfHSTKp9KvbzJWVQ?p=preview
*/
it('should not descend into re-projected content', () => {
@Component({
selector: 'grand-child',
template: ` `
})
class GrandChild {
}
@Component({
selector: 'child',
template: `
in child template
`
})
class Child {
}
@Component({selector: 'parent', template: `parent content `})
class Parent {
}
TestBed.configureTestingModule({declarations: [GrandChild, Child, Parent]});
const fixture = TestBed.createComponent(Parent);
fixture.detectChanges();
expect(getElementHtml(fixture.nativeElement))
.toEqual(
'in child template parent content ');
});
it('should not descend into re-projected content', () => {
@Component({
selector: 'card',
template:
` `
})
class Card {
}
@Component({
selector: 'card-with-title',
template: `
Title
`
})
class CardWithTitle {
}
@Component({selector: 'parent', template: `content `})
class Parent {
}
TestBed.configureTestingModule({declarations: [Card, CardWithTitle, Parent]});
const fixture = TestBed.createComponent(Parent);
fixture.detectChanges();
expect(getElementHtml(fixture.nativeElement))
.toEqual(
'Title content ');
});
it('should not match selectors against node having ngProjectAs attribute', () => {
@Component({selector: 'child', template: ` `})
class Child {
}
@Component({
selector: 'parent',
template:
`should not project
should project
`
})
class Parent {
}
TestBed.configureTestingModule({declarations: [Child, Parent]});
const fixture = TestBed.createComponent(Parent);
fixture.detectChanges();
expect(getElementHtml(fixture.nativeElement))
.toEqual('should project
');
});
// https://stackblitz.com/edit/angular-psokum?file=src%2Fapp%2Fapp.module.ts
it('should project nodes where attribute selector matches a binding', () => {
@Component({
selector: 'child',
template: ` `,
})
class Child {
}
@Component({
selector: 'parent',
template: `Has title `
})
class Parent {
}
TestBed.configureTestingModule({declarations: [Child, Parent]});
const fixture = TestBed.createComponent(Parent);
fixture.detectChanges();
expect(getElementHtml(fixture.nativeElement))
.toEqual('Has title ');
});
it('should match selectors against projected containers', () => {
@Component(
{selector: 'child', template: ` `})
class Child {
}
@Component({template: `content
`})
class Parent {
value = false;
}
TestBed.configureTestingModule({declarations: [Child, Parent]});
const fixture = TestBed.createComponent(Parent);
fixture.componentInstance.value = true;
fixture.detectChanges();
expect(getElementHtml(fixture.nativeElement))
.toEqual('content
');
});
});
it('should handle projected containers inside other containers', () => {
@Component({
selector: 'child-comp', //
template: ' '
})
class ChildComp {
}
@Component({
selector: 'root-comp', //
template: ' '
})
class RootComp {
}
@Component({
selector: 'my-app',
template: `
{{ item }}|
`
})
class MyApp {
items: number[] = [1, 2, 3];
}
TestBed.configureTestingModule({declarations: [ChildComp, RootComp, MyApp]});
const fixture = TestBed.createComponent(MyApp);
fixture.detectChanges();
// expecting # of elements to be (items.length - 1), since last element is filtered out by
// *ngIf, this applies to all other assertions below
expect(fixture.nativeElement).toHaveText('1|2|');
fixture.componentInstance.items = [4, 5];
fixture.detectChanges();
expect(fixture.nativeElement).toHaveText('4|');
fixture.componentInstance.items = [6, 7, 8, 9];
fixture.detectChanges();
expect(fixture.nativeElement).toHaveText('6|7|8|');
});
it('should project content if the change detector has been detached', () => {
@Component({selector: 'my-comp', template: ' '})
class MyComp {
constructor(changeDetectorRef: ChangeDetectorRef) { changeDetectorRef.detach(); }
}
@Component({
selector: 'my-app',
template: `
hello
`
})
class MyApp {
}
TestBed.configureTestingModule({declarations: [MyComp, MyApp]});
const fixture = TestBed.createComponent(MyApp);
fixture.detectChanges();
expect(fixture.nativeElement).toHaveText('hello');
});
it('should support ngProjectAs on elements (including )', () => {
@Component({
selector: 'card',
template: `
---
`
})
class Card {
}
@Component({
selector: 'card-with-title',
template: `
Title
`
})
class CardWithTitle {
}
@Component({
selector: 'app',
template: `
content
`
})
class App {
}
TestBed.configureTestingModule({declarations: [Card, CardWithTitle, App]});
const fixture = TestBed.createComponent(App);
fixture.detectChanges();
// Compare the text output, because Ivy and ViewEngine produce slightly different HTML.
expect(fixture.nativeElement.textContent).toContain('Title --- content');
});
it('should not match multiple selectors in ngProjectAs', () => {
@Component({
selector: 'card',
template: `
content
`
})
class Card {
}
@Component({
template: `
Title
`
})
class App {
}
TestBed.configureTestingModule({declarations: [Card, App]});
const fixture = TestBed.createComponent(App);
fixture.detectChanges();
// Compare the text output, because Ivy and ViewEngine produce slightly different HTML.
expect(fixture.nativeElement.textContent).not.toContain('Title content');
});
describe('on inline templates (e.g. *ngIf)', () => {
it('should work when matching the element name', () => {
let divDirectives = 0;
@Component({selector: 'selector-proj', template: ' '})
class SelectedNgContentComp {
}
@Directive({selector: 'div'})
class DivDirective {
constructor() { divDirectives++; }
}
@Component({
selector: 'main-selector',
template: 'Hello world!
'
})
class SelectorMainComp {
}
TestBed.configureTestingModule(
{declarations: [DivDirective, SelectedNgContentComp, SelectorMainComp]});
const fixture = TestBed.createComponent(SelectorMainComp);
fixture.detectChanges();
expect(fixture.nativeElement).toHaveText('Hello world!');
expect(divDirectives).toEqual(1);
});
it('should work when matching attributes', () => {
let xDirectives = 0;
@Component({selector: 'selector-proj', template: ' '})
class SelectedNgContentComp {
}
@Directive({selector: '[x]'})
class XDirective {
constructor() { xDirectives++; }
}
@Component({
selector: 'main-selector',
template: 'Hello world!
'
})
class SelectorMainComp {
}
TestBed.configureTestingModule(
{declarations: [XDirective, SelectedNgContentComp, SelectorMainComp]});
const fixture = TestBed.createComponent(SelectorMainComp);
fixture.detectChanges();
expect(fixture.nativeElement).toHaveText('Hello world!');
expect(xDirectives).toEqual(1);
});
it('should work when matching classes', () => {
let xDirectives = 0;
@Component({selector: 'selector-proj', template: ' '})
class SelectedNgContentComp {
}
@Directive({selector: '.x'})
class XDirective {
constructor() { xDirectives++; }
}
@Component({
selector: 'main-selector',
template: 'Hello world!
'
})
class SelectorMainComp {
}
TestBed.configureTestingModule(
{declarations: [XDirective, SelectedNgContentComp, SelectorMainComp]});
const fixture = TestBed.createComponent(SelectorMainComp);
fixture.detectChanges();
expect(fixture.nativeElement).toHaveText('Hello world!');
expect(xDirectives).toEqual(1);
});
it('should ignore synthesized attributes (e.g. ngTrackBy)', () => {
@Component(
{selector: 'selector-proj', template: ' '})
class SelectedNgContentComp {
}
@Component({
selector: 'main-selector',
template:
'inline({{item.name}}
)' +
'ng-template({{item.name}}
)'
})
class SelectorMainComp {
items = [
{id: 1, name: 'one'},
{id: 2, name: 'two'},
{id: 3, name: 'three'},
];
getItemId(item: {id: number}) { return item.id; }
}
TestBed.configureTestingModule({declarations: [SelectedNgContentComp, SelectorMainComp]});
const fixture = TestBed.createComponent(SelectorMainComp);
fixture.detectChanges();
expect(fixture.nativeElement).toHaveText('inline()ng-template(onetwothree)');
});
describe('on containers', () => {
it('should work when matching attributes', () => {
let xDirectives = 0;
@Component({selector: 'selector-proj', template: ' '})
class SelectedNgContentComp {
}
@Directive({selector: '[x]'})
class XDirective {
constructor() { xDirectives++; }
}
@Component({
selector: 'main-selector',
template:
'Hello world! '
})
class SelectorMainComp {
}
TestBed.configureTestingModule(
{declarations: [XDirective, SelectedNgContentComp, SelectorMainComp]});
const fixture = TestBed.createComponent(SelectorMainComp);
fixture.detectChanges();
expect(fixture.nativeElement).toHaveText('Hello world!');
expect(xDirectives).toEqual(1);
});
it('should work when matching classes', () => {
let xDirectives = 0;
@Component({selector: 'selector-proj', template: ' '})
class SelectedNgContentComp {
}
@Directive({selector: '.x'})
class XDirective {
constructor() { xDirectives++; }
}
@Component({
selector: 'main-selector',
template:
'Hello world! '
})
class SelectorMainComp {
}
TestBed.configureTestingModule(
{declarations: [XDirective, SelectedNgContentComp, SelectorMainComp]});
const fixture = TestBed.createComponent(SelectorMainComp);
fixture.detectChanges();
expect(fixture.nativeElement).toHaveText('Hello world!');
expect(xDirectives).toEqual(1);
});
});
});
});