/** * @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 {SelectorFlags} from '@angular/core/src/render3/interfaces/projection'; import {AttributeMarker, detectChanges} from '../../src/render3/index'; import {bind, container, containerRefreshEnd, containerRefreshStart, elementEnd, elementProperty, elementStart, embeddedViewEnd, embeddedViewStart, loadDirective, projection, projectionDef, text} from '../../src/render3/instructions'; import {RenderFlags} from '../../src/render3/interfaces/definition'; import {ComponentFixture, createComponent, renderComponent, toHtml} from './render_util'; describe('content projection', () => { it('should project content', () => { /** *
*/ const Child = createComponent('child', function(rf: RenderFlags, ctx: any) { if (rf & RenderFlags.Create) { projectionDef(0); elementStart(1, 'div'); { projection(2, 0); } elementEnd(); } }); /** * content */ const Parent = createComponent('parent', function(rf: RenderFlags, ctx: any) { if (rf & RenderFlags.Create) { elementStart(0, 'child'); { text(1, 'content'); } elementEnd(); } }, [Child]); const parent = renderComponent(Parent); expect(toHtml(parent)).toEqual('
content
'); }); it('should project content when root.', () => { const Child = createComponent('child', function(rf: RenderFlags, ctx: any) { if (rf & RenderFlags.Create) { projectionDef(0); projection(1, 0); } }); const Parent = createComponent('parent', function(rf: RenderFlags, ctx: any) { if (rf & RenderFlags.Create) { elementStart(0, 'child'); { text(1, 'content'); } elementEnd(); } }, [Child]); const parent = renderComponent(Parent); expect(toHtml(parent)).toEqual('content'); }); it('should re-project content when root.', () => { const GrandChild = createComponent('grand-child', function(rf: RenderFlags, ctx: any) { if (rf & RenderFlags.Create) { projectionDef(0); elementStart(1, 'div'); { projection(2, 0); } elementEnd(); } }); const Child = createComponent('child', function(rf: RenderFlags, ctx: any) { if (rf & RenderFlags.Create) { projectionDef(0); elementStart(1, 'grand-child'); { projection(2, 0); } elementEnd(); } }, [GrandChild]); const Parent = createComponent('parent', function(rf: RenderFlags, ctx: any) { if (rf & RenderFlags.Create) { elementStart(0, 'child'); { elementStart(1, 'b'); text(2, 'Hello'); elementEnd(); text(3, 'World!'); } elementEnd(); } }, [Child]); const parent = renderComponent(Parent); expect(toHtml(parent)) .toEqual('
HelloWorld!
'); }); it('should project components', () => { /**
*/ const Child = createComponent('child', (rf: RenderFlags, ctx: any) => { if (rf & RenderFlags.Create) { projectionDef(0); elementStart(1, 'div'); { projection(2, 0); } elementEnd(); } }); const ProjectedComp = createComponent('projected-comp', (rf: RenderFlags, ctx: any) => { if (rf & RenderFlags.Create) { text(0, 'content'); } }); /** * * * */ const Parent = createComponent('parent', (rf: RenderFlags, ctx: any) => { if (rf & RenderFlags.Create) { elementStart(0, 'child'); { elementStart(1, 'projected-comp'); elementEnd(); } elementEnd(); } }, [Child, ProjectedComp]); const parent = renderComponent(Parent); expect(toHtml(parent)) .toEqual('
content
'); }); it('should project content with container.', () => { const Child = createComponent('child', function(rf: RenderFlags, ctx: any) { if (rf & RenderFlags.Create) { projectionDef(0); elementStart(1, 'div'); { projection(2, 0); } elementEnd(); } }); const Parent = createComponent('parent', function(rf: RenderFlags, ctx: {value: any}) { if (rf & RenderFlags.Create) { elementStart(0, 'child'); { text(1, '('); container(2); text(3, ')'); } elementEnd(); } if (rf & RenderFlags.Update) { containerRefreshStart(2); { if (ctx.value) { let rf0 = embeddedViewStart(0); if (rf0 & RenderFlags.Create) { text(0, 'content'); } embeddedViewEnd(); } } containerRefreshEnd(); } }, [Child]); const parent = renderComponent(Parent); expect(toHtml(parent)).toEqual('
()
'); parent.value = true; detectChanges(parent); expect(toHtml(parent)).toEqual('
(content)
'); parent.value = false; detectChanges(parent); expect(toHtml(parent)).toEqual('
()
'); }); it('should project content with container into root', () => { const Child = createComponent('child', function(rf: RenderFlags, ctx: any) { if (rf & RenderFlags.Create) { projectionDef(0); projection(1, 0); } }); const Parent = createComponent('parent', function(rf: RenderFlags, ctx: {value: any}) { if (rf & RenderFlags.Create) { elementStart(0, 'child'); { container(1); } elementEnd(); } if (rf & RenderFlags.Update) { containerRefreshStart(1); { if (ctx.value) { let rf0 = embeddedViewStart(0); if (rf0 & RenderFlags.Create) { text(0, 'content'); } embeddedViewEnd(); } } containerRefreshEnd(); } }, [Child]); const parent = renderComponent(Parent); expect(toHtml(parent)).toEqual(''); parent.value = true; detectChanges(parent); expect(toHtml(parent)).toEqual('content'); parent.value = false; detectChanges(parent); expect(toHtml(parent)).toEqual(''); }); it('should project content with container and if-else.', () => { const Child = createComponent('child', function(rf: RenderFlags, ctx: any) { if (rf & RenderFlags.Create) { projectionDef(0); elementStart(1, 'div'); { projection(2, 0); } elementEnd(); } }); const Parent = createComponent('parent', function(rf: RenderFlags, ctx: {value: any}) { if (rf & RenderFlags.Create) { elementStart(0, 'child'); { text(1, '('); container(2); text(3, ')'); } elementEnd(); } if (rf & RenderFlags.Update) { containerRefreshStart(2); { if (ctx.value) { let rf0 = embeddedViewStart(0); if (rf0 & RenderFlags.Create) { text(0, 'content'); } embeddedViewEnd(); } else { if (embeddedViewStart(1)) { text(0, 'else'); } embeddedViewEnd(); } } containerRefreshEnd(); } }, [Child]); const parent = renderComponent(Parent); expect(toHtml(parent)).toEqual('
(else)
'); parent.value = true; detectChanges(parent); expect(toHtml(parent)).toEqual('
(content)
'); parent.value = false; detectChanges(parent); expect(toHtml(parent)).toEqual('
(else)
'); }); it('should support projection in embedded views', () => { let childCmptInstance: any; /** *
* % if (!skipContent) { * * * * % } *
*/ const Child = createComponent('child', function(rf: RenderFlags, ctx: any) { if (rf & RenderFlags.Create) { projectionDef(0); elementStart(1, 'div'); { container(2); } elementEnd(); } if (rf & RenderFlags.Update) { containerRefreshStart(2); { if (!ctx.skipContent) { let rf0 = embeddedViewStart(0); if (rf0 & RenderFlags.Create) { elementStart(0, 'span'); projection(1, 0); elementEnd(); } embeddedViewEnd(); } } containerRefreshEnd(); } }); /** * content */ const Parent = createComponent('parent', function(rf: RenderFlags, ctx: any) { if (rf & RenderFlags.Create) { elementStart(0, 'child'); { childCmptInstance = loadDirective(0); text(1, 'content'); } elementEnd(); } }, [Child]); const parent = renderComponent(Parent); expect(toHtml(parent)).toEqual('
content
'); childCmptInstance.skipContent = true; detectChanges(parent); expect(toHtml(parent)).toEqual('
'); }); it('should support projection in embedded views when ng-content is a root node of an embedded view', () => { let childCmptInstance: any; /** *
* % if (!skipContent) { * * % } *
*/ const Child = createComponent('child', function(rf: RenderFlags, ctx: any) { if (rf & RenderFlags.Create) { projectionDef(0); elementStart(1, 'div'); { container(2); } elementEnd(); } if (rf & RenderFlags.Update) { containerRefreshStart(2); { if (!ctx.skipContent) { let rf0 = embeddedViewStart(0); if (rf0 & RenderFlags.Create) { projection(0, 0); } embeddedViewEnd(); } } containerRefreshEnd(); } }); /** * content */ const Parent = createComponent('parent', function(rf: RenderFlags, ctx: any) { if (rf & RenderFlags.Create) { elementStart(0, 'child'); { childCmptInstance = loadDirective(0); text(1, 'content'); } elementEnd(); } }, [Child]); const parent = renderComponent(Parent); expect(toHtml(parent)).toEqual('
content
'); childCmptInstance.skipContent = true; detectChanges(parent); expect(toHtml(parent)).toEqual('
'); }); it('should support projection in embedded views when ng-content is a root node of an embedded view, with other nodes after', () => { let childCmptInstance: any; /** *
* % if (!skipContent) { * before--after * % } *
*/ const Child = createComponent('child', function(rf: RenderFlags, ctx: any) { if (rf & RenderFlags.Create) { projectionDef(0); elementStart(1, 'div'); { container(2); } elementEnd(); } if (rf & RenderFlags.Update) { containerRefreshStart(2); { if (!ctx.skipContent) { let rf0 = embeddedViewStart(0); if (rf0 & RenderFlags.Create) { text(0, 'before-'); projection(1, 0); text(2, '-after'); } embeddedViewEnd(); } } containerRefreshEnd(); } }); /** * content */ const Parent = createComponent('parent', function(rf: RenderFlags, ctx: any) { if (rf & RenderFlags.Create) { elementStart(0, 'child'); { childCmptInstance = loadDirective(0); text(1, 'content'); } elementEnd(); } }, [Child]); const parent = renderComponent(Parent); expect(toHtml(parent)).toEqual('
before-content-after
'); childCmptInstance.skipContent = true; detectChanges(parent); expect(toHtml(parent)).toEqual('
'); }); it('should project nodes into the last ng-content', () => { /** *
* */ const Child = createComponent('child', function(rf: RenderFlags, ctx: any) { if (rf & RenderFlags.Create) { projectionDef(0); elementStart(1, 'div'); { projection(2, 0); } elementEnd(); elementStart(3, 'span'); { projection(4, 0); } elementEnd(); } }); /** * content */ const Parent = createComponent('parent', function(rf: RenderFlags, ctx: any) { if (rf & RenderFlags.Create) { elementStart(0, 'child'); { text(1, 'content'); } elementEnd(); } }, [Child]); const parent = renderComponent(Parent); expect(toHtml(parent)).toEqual('
content
'); }); /** * Warning: this test is _not_ in-line with what Angular does atm. * Moreover the current implementation logic will result in DOM nodes * being re-assigned from one parent to another. Proposal: have compiler * to remove all but the latest occurrence of so we generate * only one P(n, m, 0) instruction. It would make it consistent with the * current Angular behaviour: * http://plnkr.co/edit/OAYkNawTDPkYBFTqovTP?p=preview */ it('should project nodes into the last available ng-content', () => { let childCmptInstance: any; /** * *
* % if (show) { * * % } *
*/ const Child = createComponent('child', function(rf: RenderFlags, ctx: any) { if (rf & RenderFlags.Create) { projectionDef(0); projection(1, 0); elementStart(2, 'div'); { container(3); } elementEnd(); } if (rf & RenderFlags.Update) { containerRefreshStart(3); { if (ctx.show) { let rf0 = embeddedViewStart(0); if (rf0 & RenderFlags.Create) { projection(0, 0); } embeddedViewEnd(); } } containerRefreshEnd(); } }); /** * content */ const Parent = createComponent('parent', function(rf: RenderFlags, ctx: any) { if (rf & RenderFlags.Create) { elementStart(0, 'child'); { childCmptInstance = loadDirective(0); text(1, 'content'); } elementEnd(); } }, [Child]); const parent = renderComponent(Parent); expect(toHtml(parent)).toEqual('content
'); childCmptInstance.show = true; detectChanges(parent); expect(toHtml(parent)).toEqual('
content
'); }); describe('with selectors', () => { it('should project nodes using attribute selectors', () => { /** *
*
*/ const Child = createComponent('child', function(rf: RenderFlags, ctx: any) { if (rf & RenderFlags.Create) { projectionDef( 0, [[['span', 'title', 'toFirst']], [['span', 'title', 'toSecond']]], ['span[title=toFirst]', 'span[title=toSecond]']); elementStart(1, 'div', ['id', 'first']); { projection(2, 0, 1); } elementEnd(); elementStart(3, 'div', ['id', 'second']); { projection(4, 0, 2); } elementEnd(); } }); /** * * 1 * 2 * */ const Parent = createComponent('parent', function(rf: RenderFlags, ctx: any) { if (rf & RenderFlags.Create) { elementStart(0, 'child'); { elementStart(1, 'span', ['title', 'toFirst']); { text(2, '1'); } elementEnd(); elementStart(3, 'span', ['title', 'toSecond']); { text(4, '2'); } elementEnd(); } elementEnd(); } }, [Child]); const parent = renderComponent(Parent); expect(toHtml(parent)) .toEqual( '
1
2
'); }); // https://stackblitz.com/edit/angular-psokum?file=src%2Fapp%2Fapp.module.ts it('should project nodes where attribute selector matches a binding', () => { /** * */ const Child = createComponent('child', function(rf: RenderFlags, ctx: any) { if (rf & RenderFlags.Create) { projectionDef(0, [[['', 'title', '']]], ['[title]']); { projection(1, 0, 1); } } }); /** * * Has title * */ const Parent = createComponent('parent', function(rf: RenderFlags, ctx: any) { if (rf & RenderFlags.Create) { elementStart(0, 'child'); { elementStart(1, 'span', [AttributeMarker.SelectOnly, 'title']); { text(2, 'Has title'); } elementEnd(); } elementEnd(); } if (rf & RenderFlags.Update) { elementProperty(1, 'title', bind('Some title')); } }, [Child]); const fixture = new ComponentFixture(Parent); expect(fixture.html).toEqual('Has title'); }); it('should project nodes using class selectors', () => { /** *
*
*/ const Child = createComponent('child', function(rf: RenderFlags, ctx: any) { if (rf & RenderFlags.Create) { projectionDef( 0, [ [['span', SelectorFlags.CLASS, 'toFirst']], [['span', SelectorFlags.CLASS, 'toSecond']] ], ['span.toFirst', 'span.toSecond']); elementStart(1, 'div', ['id', 'first']); { projection(2, 0, 1); } elementEnd(); elementStart(3, 'div', ['id', 'second']); { projection(4, 0, 2); } elementEnd(); } }); /** * * 1 * 2 * */ const Parent = createComponent('parent', function(rf: RenderFlags, ctx: any) { if (rf & RenderFlags.Create) { elementStart(0, 'child'); { elementStart(1, 'span', ['class', 'toFirst']); { text(2, '1'); } elementEnd(); elementStart(3, 'span', ['class', 'toSecond']); { text(4, '2'); } elementEnd(); } elementEnd(); } }, [Child]); const parent = renderComponent(Parent); expect(toHtml(parent)) .toEqual( '
1
2
'); }); it('should project nodes using class selectors when element has multiple classes', () => { /** *
*
*/ const Child = createComponent('child', function(rf: RenderFlags, ctx: any) { if (rf & RenderFlags.Create) { projectionDef( 0, [ [['span', SelectorFlags.CLASS, 'toFirst']], [['span', SelectorFlags.CLASS, 'toSecond']] ], ['span.toFirst', 'span.toSecond']); elementStart(1, 'div', ['id', 'first']); { projection(2, 0, 1); } elementEnd(); elementStart(3, 'div', ['id', 'second']); { projection(4, 0, 2); } elementEnd(); } }); /** * * 1 * 2 * */ const Parent = createComponent('parent', function(rf: RenderFlags, ctx: any) { if (rf & RenderFlags.Create) { elementStart(0, 'child'); { elementStart(1, 'span', ['class', 'other toFirst']); { text(2, '1'); } elementEnd(); elementStart(3, 'span', ['class', 'toSecond noise']); { text(4, '2'); } elementEnd(); } elementEnd(); } }, [Child]); const parent = renderComponent(Parent); expect(toHtml(parent)) .toEqual( '
1
2
'); }); it('should project nodes into the first matching selector', () => { /** *
*
*/ const Child = createComponent('child', function(rf: RenderFlags, ctx: any) { if (rf & RenderFlags.Create) { projectionDef( 0, [[['span']], [['span', SelectorFlags.CLASS, 'toSecond']]], ['span', 'span.toSecond']); elementStart(1, 'div', ['id', 'first']); { projection(2, 0, 1); } elementEnd(); elementStart(3, 'div', ['id', 'second']); { projection(4, 0, 2); } elementEnd(); } }); /** * * 1 * 2 * */ const Parent = createComponent('parent', function(rf: RenderFlags, ctx: any) { if (rf & RenderFlags.Create) { elementStart(0, 'child'); { elementStart(1, 'span', ['class', 'toFirst']); { text(2, '1'); } elementEnd(); elementStart(3, 'span', ['class', 'toSecond']); { text(4, '2'); } elementEnd(); } elementEnd(); } }, [Child]); const parent = renderComponent(Parent); expect(toHtml(parent)) .toEqual( '
12
'); }); it('should allow mixing ng-content with and without selectors', () => { /** *
*
*/ const Child = createComponent('child', function(rf: RenderFlags, ctx: any) { if (rf & RenderFlags.Create) { projectionDef(0, [[['span', SelectorFlags.CLASS, 'toFirst']]], ['span.toFirst']); elementStart(1, 'div', ['id', 'first']); { projection(2, 0, 1); } elementEnd(); elementStart(3, 'div', ['id', 'second']); { projection(4, 0); } elementEnd(); } }); /** * * 1 * 2 * */ const Parent = createComponent('parent', function(rf: RenderFlags, ctx: any) { if (rf & RenderFlags.Create) { elementStart(0, 'child'); { elementStart(1, 'span', ['class', 'toFirst']); { text(2, '1'); } elementEnd(); elementStart(3, 'span'); { text(4, 'remaining'); } elementEnd(); text(5, 'more remaining'); } elementEnd(); } }, [Child]); const parent = renderComponent(Parent); expect(toHtml(parent)) .toEqual( '
1
remainingmore remaining
'); }); it('should allow mixing ng-content with and without selectors - ng-content first', () => { /** *
*
*/ const Child = createComponent('child', function(rf: RenderFlags, ctx: any) { if (rf & RenderFlags.Create) { projectionDef(0, [[['span', SelectorFlags.CLASS, 'toSecond']]], ['span.toSecond']); elementStart(1, 'div', ['id', 'first']); { projection(2, 0); } elementEnd(); elementStart(3, 'div', ['id', 'second']); { projection(4, 0, 1); } elementEnd(); } }); /** * * 1 * 2 * remaining * */ const Parent = createComponent('parent', function(rf: RenderFlags, ctx: any) { if (rf & RenderFlags.Create) { elementStart(0, 'child'); { elementStart(1, 'span'); { text(2, '1'); } elementEnd(); elementStart(3, 'span', ['class', 'toSecond']); { text(4, '2'); } elementEnd(); text(5, 'remaining'); } elementEnd(); } }, [Child]); const parent = renderComponent(Parent); expect(toHtml(parent)) .toEqual( '
1remaining
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', () => { /** * *
* */ const GrandChild = createComponent('grand-child', function(rf: RenderFlags, ctx: any) { if (rf & RenderFlags.Create) { projectionDef(0, [[['span']]], ['span']); projection(1, 0, 1); elementStart(2, 'hr'); elementEnd(); projection(3, 0, 0); } }); /** * * * in child template * */ const Child = createComponent('child', function(rf: RenderFlags, ctx: any) { if (rf & RenderFlags.Create) { projectionDef(0); elementStart(1, 'grand-child'); { projection(2, 0); elementStart(3, 'span'); { text(4, 'in child template'); } elementEnd(); } elementEnd(); } }, [GrandChild]); /** * *
* parent content *
*
*/ const Parent = createComponent('parent', function(rf: RenderFlags, ctx: any) { if (rf & RenderFlags.Create) { elementStart(0, 'child'); { elementStart(1, 'span'); { text(2, 'parent content'); } elementEnd(); } elementEnd(); } }, [Child]); const parent = renderComponent(Parent); expect(toHtml(parent)) .toEqual( 'in child template
parent content
'); }); it('should match selectors on ng-content nodes with attributes', () => { /** * *
* */ const Card = createComponent('card', function(rf: RenderFlags, ctx: any) { if (rf & RenderFlags.Create) { projectionDef( 0, [[['', 'card-title', '']], [['', 'card-content', '']]], ['[card-title]', '[card-content]']); projection(1, 0, 1); elementStart(2, 'hr'); elementEnd(); projection(3, 0, 2); } }); /** * *

Title

* *
*/ const CardWithTitle = createComponent('card-with-title', function(rf: RenderFlags, ctx: any) { if (rf & RenderFlags.Create) { projectionDef(0); elementStart(1, 'card'); { elementStart(2, 'h1', ['card-title', '']); { text(3, 'Title'); } elementEnd(); projection(4, 0, 0, ['card-content', '']); } elementEnd(); } }, [Card]); /** * * content * */ const App = createComponent('app', function(rf: RenderFlags, ctx: any) { if (rf & RenderFlags.Create) { elementStart(0, 'card-with-title'); { text(1, 'content'); } elementEnd(); } }, [CardWithTitle]); const app = renderComponent(App); expect(toHtml(app)) .toEqual( '

Title


content
'); }); it('should support ngProjectAs on elements (including )', () => { /** * *
* */ const Card = createComponent('card', function(rf: RenderFlags, ctx: any) { if (rf & RenderFlags.Create) { projectionDef( 0, [[['', 'card-title', '']], [['', 'card-content', '']]], ['[card-title]', '[card-content]']); projection(1, 0, 1); elementStart(2, 'hr'); elementEnd(); projection(3, 0, 2); } }); /** * *

* */ const CardWithTitle = createComponent('card-with-title', function(rf: RenderFlags, ctx: any) { if (rf & RenderFlags.Create) { projectionDef(0); elementStart(1, 'card'); { elementStart(2, 'h1', ['ngProjectAs', '[card-title]']); { text(3, 'Title'); } elementEnd(); projection(4, 0, 0, ['ngProjectAs', '[card-content]']); } elementEnd(); } }, [Card]); /** * * content * */ const App = createComponent('app', function(rf: RenderFlags, ctx: any) { if (rf & RenderFlags.Create) { elementStart(0, 'card-with-title'); { text(1, 'content'); } elementEnd(); } }, [CardWithTitle]); const app = renderComponent(App); expect(toHtml(app)) .toEqual('

Title


content
'); }); it('should not match selectors against node having ngProjectAs attribute', function() { /** * */ const Child = createComponent('child', function(rf: RenderFlags, ctx: any) { if (rf & RenderFlags.Create) { projectionDef(0, [[['div']]], ['div']); projection(1, 0, 1); } }); /** * *
should not project
*
should project
*
*/ const Parent = createComponent('parent', function(rf: RenderFlags, ctx: any) { if (rf & RenderFlags.Create) { elementStart(0, 'child'); { elementStart(1, 'div', ['ngProjectAs', 'span']); { text(2, 'should not project'); } elementEnd(); elementStart(3, 'div'); { text(4, 'should project'); } elementEnd(); } elementEnd(); } }, [Child]); const parent = renderComponent(Parent); expect(toHtml(parent)).toEqual('
should project
'); }); it('should match selectors against projected containers', () => { /** * * * */ const Child = createComponent('child', function(rf: RenderFlags, ctx: any) { if (rf & RenderFlags.Create) { projectionDef(0, [[['div']]], ['div']); elementStart(1, 'span'); { projection(2, 0, 1); } elementEnd(); } }); /** * *
content
*
*/ const Parent = createComponent('parent', function(rf: RenderFlags, ctx: {value: any}) { if (rf & RenderFlags.Create) { elementStart(0, 'child'); { container(1, undefined, 'div'); } elementEnd(); } if (rf & RenderFlags.Update) { containerRefreshStart(1); { if (true) { let rf0 = embeddedViewStart(0); if (rf0 & RenderFlags.Create) { elementStart(0, 'div'); { text(1, 'content'); } elementEnd(); } embeddedViewEnd(); } } containerRefreshEnd(); } }, [Child]); const parent = renderComponent(Parent); expect(toHtml(parent)).toEqual('
content
'); }); }); });