feat(compiler): introduce <ng-template>, deprecate <template> and template attribute

The rationale of this change is to improve the inter-operability with web
components that might make use of the `<template>` tag.

DEPRECATION

The template tags and template attribute are deprecated:

    <template ngFor [ngFor]=items let-item><li>...</li></template>
    <li template="ngFor: let item of items">...</li>

should be rewritten as:

    <ng-template ngFor [ngFor]=items let-item><li>...</li></ng-template>

Note that they still be supported in 4.x with a deprecartion warning in
development mode.

MIGRATION

- `template` tags (or elements with a `template` attribute) should be rewritten
as a `ng-template` tag,
- `ng-content` selectors should be updated to referto a `ng-template` where they
use to refer to a template: `<ng-content selector="template[attr]">` should be
rewritten as `<ng-content selector="ng-template[attr]">`
- if you consume a component relying on your templates being actual `template`
elements (that is they include a `<ng-content selector="template[attr]">`). You
should  still migrate to `ng-template` and make use of `ngProjectAs` to override
the way `ng-content` sees the template:
`<ng-template projectAs="template[attr]">`
- while `template` elements are deprecated in 4.x they continue to work.
This commit is contained in:
Victor Berchet
2017-01-09 13:16:46 -08:00
committed by Igor Minar
parent 3f519207a4
commit bf8eb41248
31 changed files with 312 additions and 184 deletions

View File

@ -286,7 +286,7 @@ export function main() {
vc: ViewContainerRef;
}
@Component({template: '<template #t>Dynamic content</template>'})
@Component({template: '<ng-template #t>Dynamic content</ng-template>'})
class EmbeddedViewComp {
@ViewChild(TemplateRef)
tplRef: TemplateRef<Object>;

View File

@ -485,8 +485,8 @@ function createTests({viewEngine}: {viewEngine: boolean}) {
fakeAsync(() => { expect(_bindAndCheckSimpleValue('"$"')).toEqual(['someProp=$']); }));
it('should read locals', fakeAsync(() => {
const ctx =
createCompFixture('<template testLocals let-local="someLocal">{{local}}</template>');
const ctx = createCompFixture(
'<ng-template testLocals let-local="someLocal">{{local}}</ng-template>');
ctx.detectChanges(false);
expect(renderLog.log).toEqual(['{{someLocalValue}}']);
@ -1242,7 +1242,7 @@ function createTests({viewEngine}: {viewEngine: boolean}) {
it('should recurse into nested view containers even if there are no bindings in the component view',
() => {
@Component({template: '<template #vc>{{name}}</template>'})
@Component({template: '<ng-template #vc>{{name}}</ng-template>'})
class Comp {
name = 'Tom';
@ViewChild('vc', {read: ViewContainerRef}) vc: ViewContainerRef;

View File

@ -130,7 +130,7 @@ export function main() {
it('should support using structural directives with ngTemplateOutlet', () => {
@Component({
template:
'<child [templateCtx]="templateCtx"><template let-shown="shown" #tpl><span *ngIf="shown">hello</span></template></child>'
'<child [templateCtx]="templateCtx"><ng-template let-shown="shown" #tpl><span *ngIf="shown">hello</span></ng-template></child>'
})
class Parent {
templateCtx = {shown: false};

View File

@ -363,10 +363,10 @@ function declareTests({useJit, viewEngine}: {useJit: boolean, viewEngine: boolea
const fixture = TestBed.createComponent(MyComp);
});
it('should support template directives via `<template>` elements.', () => {
it('should support template directives via `<ng-template>` elements.', () => {
TestBed.configureTestingModule({declarations: [MyComp, SomeViewport]});
const template =
'<template some-viewport let-greeting="someTmpl"><span>{{greeting}}</span></template>';
'<ng-template some-viewport let-greeting="someTmpl"><span>{{greeting}}</span></ng-template>';
TestBed.overrideComponent(MyComp, {set: {template}});
const fixture = TestBed.createComponent(MyComp);
@ -382,7 +382,7 @@ function declareTests({useJit, viewEngine}: {useJit: boolean, viewEngine: boolea
it('should not share empty context for template directives - issue #10045', () => {
TestBed.configureTestingModule({declarations: [MyComp, PollutedContext, NoContext]});
const template =
'<template pollutedContext let-foo="bar">{{foo}}</template><template noContext let-foo="bar">{{foo}}</template>';
'<ng-template pollutedContext let-foo="bar">{{foo}}</ng-template><ng-template noContext let-foo="bar">{{foo}}</ng-template>';
TestBed.overrideComponent(MyComp, {set: {template}});
const fixture = TestBed.createComponent(MyComp);
@ -393,7 +393,7 @@ function declareTests({useJit, viewEngine}: {useJit: boolean, viewEngine: boolea
it('should not detach views in ViewContainers when the parent view is destroyed.', () => {
TestBed.configureTestingModule({declarations: [MyComp, SomeViewport]});
const template =
'<div *ngIf="ctxBoolProp"><template some-viewport let-greeting="someTmpl"><span>{{greeting}}</span></template></div>';
'<div *ngIf="ctxBoolProp"><ng-template some-viewport let-greeting="someTmpl"><span>{{greeting}}</span></ng-template></div>';
TestBed.overrideComponent(MyComp, {set: {template}});
const fixture = TestBed.createComponent(MyComp);
@ -412,11 +412,11 @@ function declareTests({useJit, viewEngine}: {useJit: boolean, viewEngine: boolea
expect(fixture.debugElement.children.length).toBe(0);
});
it('should use a comment while stamping out `<template>` elements.', () => {
TestBed.configureTestingModule({declarations: [MyComp]});
const template = '<template></template>';
TestBed.overrideComponent(MyComp, {set: {template}});
const fixture = TestBed.createComponent(MyComp);
it('should use a comment while stamping out `<ng-template>` elements.', () => {
const fixture =
TestBed.configureTestingModule({declarations: [MyComp]})
.overrideComponent(MyComp, {set: {template: '<ng-template></ng-template>'}})
.createComponent(MyComp);
const childNodesOfWrapper = getDOM().childNodes(fixture.nativeElement);
expect(childNodesOfWrapper.length).toBe(1);
@ -448,7 +448,7 @@ function declareTests({useJit, viewEngine}: {useJit: boolean, viewEngine: boolea
schemas: [NO_ERRORS_SCHEMA],
});
const template =
'<some-directive><toolbar><template toolbarpart let-toolbarProp="toolbarProp">{{ctxProp}},{{toolbarProp}},<cmp-with-host></cmp-with-host></template></toolbar></some-directive>';
'<some-directive><toolbar><ng-template toolbarpart let-toolbarProp="toolbarProp">{{ctxProp}},{{toolbarProp}},<cmp-with-host></cmp-with-host></ng-template></toolbar></some-directive>';
TestBed.overrideComponent(MyComp, {set: {template}});
const fixture = TestBed.createComponent(MyComp);
@ -484,7 +484,7 @@ function declareTests({useJit, viewEngine}: {useJit: boolean, viewEngine: boolea
() => {
TestBed.configureTestingModule({declarations: [MyComp, ChildComp]});
const template =
'<template [ngIf]="true">{{alice.ctxProp}}</template>|{{alice.ctxProp}}|<child-cmp ref-alice></child-cmp>';
'<ng-template [ngIf]="true">{{alice.ctxProp}}</ng-template>|{{alice.ctxProp}}|<child-cmp ref-alice></child-cmp>';
TestBed.overrideComponent(MyComp, {set: {template}});
const fixture = TestBed.createComponent(MyComp);
@ -530,10 +530,10 @@ function declareTests({useJit, viewEngine}: {useJit: boolean, viewEngine: boolea
});
it('should assign the TemplateRef to a user-defined variable', () => {
TestBed.configureTestingModule({declarations: [MyComp]});
const template = '<template ref-alice></template>';
TestBed.overrideComponent(MyComp, {set: {template}});
const fixture = TestBed.createComponent(MyComp);
const fixture =
TestBed.configureTestingModule({declarations: [MyComp]})
.overrideComponent(MyComp, {set: {template: '<template ref-alice></template>'}})
.createComponent(MyComp);
const value = fixture.debugElement.childNodes[0].references['alice'];
expect(value.createEmbeddedView).toBeTruthy();
@ -552,14 +552,16 @@ function declareTests({useJit, viewEngine}: {useJit: boolean, viewEngine: boolea
describe('variables', () => {
it('should allow to use variables in a for loop', () => {
TestBed.configureTestingModule({declarations: [MyComp, ChildCompNoTemplate]});
const template =
'<template ngFor [ngForOf]="[1]" let-i><child-cmp-no-template #cmp></child-cmp-no-template>{{i}}-{{cmp.ctxProp}}</template>';
TestBed.overrideComponent(MyComp, {set: {template}});
const fixture = TestBed.createComponent(MyComp);
'<ng-template ngFor [ngForOf]="[1]" let-i><child-cmp-no-template #cmp></child-cmp-no-template>{{i}}-{{cmp.ctxProp}}</ng-template>';
const fixture =
TestBed.configureTestingModule({declarations: [MyComp, ChildCompNoTemplate]})
.overrideComponent(MyComp, {set: {template}})
.createComponent(MyComp);
fixture.detectChanges();
// Get the element at index 2, since index 0 is the <template>.
// Get the element at index 2, since index 0 is the <ng-template>.
expect(getDOM().childNodes(fixture.nativeElement)[2]).toHaveText('1-hello');
});
});
@ -774,11 +776,17 @@ function declareTests({useJit, viewEngine}: {useJit: boolean, viewEngine: boolea
}));
it('should support events via EventEmitter on template elements', async(() => {
TestBed.configureTestingModule(
{declarations: [MyComp, DirectiveEmittingEvent, DirectiveListeningEvent]});
const template = '<template emitter listener (event)="ctxProp=$event"></template>';
TestBed.overrideComponent(MyComp, {set: {template}});
const fixture = TestBed.createComponent(MyComp);
const fixture =
TestBed
.configureTestingModule(
{declarations: [MyComp, DirectiveEmittingEvent, DirectiveListeningEvent]})
.overrideComponent(MyComp, {
set: {
template:
'<ng-template emitter listener (event)="ctxProp=$event"></ng-template>'
}
})
.createComponent(MyComp);
const tc = fixture.debugElement.childNodes[0];
@ -1487,10 +1495,11 @@ function declareTests({useJit, viewEngine}: {useJit: boolean, viewEngine: boolea
});
it('should reflect property values on template comments', () => {
TestBed.configureTestingModule({declarations: [MyComp]});
const template = '<template [ngIf]="ctxBoolProp"></template>';
TestBed.overrideComponent(MyComp, {set: {template}});
const fixture = TestBed.createComponent(MyComp);
const fixture =
TestBed.configureTestingModule({declarations: [MyComp]})
.overrideComponent(
MyComp, {set: {template: '<ng-template [ngIf]="ctxBoolProp"></ng-template>'}})
.createComponent(MyComp);
fixture.componentInstance.ctxBoolProp = true;
fixture.detectChanges();

View File

@ -132,7 +132,7 @@ function createTests({viewEngine}: {viewEngine: boolean}) {
TestBed.overrideComponent(MainComp, {
set: {
template: '<multiple-content-tags>' +
'<template manual class="left"><div>A1</div></template>' +
'<ng-template manual class="left"><div>A1</div></ng-template>' +
'<div>B</div>' +
'</multiple-content-tags>'
}
@ -175,7 +175,7 @@ function createTests({viewEngine}: {viewEngine: boolean}) {
TestBed.overrideComponent(MainComp, {
set: {
template: '<outer>' +
'<template manual class="left"><div>A</div></template>' +
'<ng-template manual class="left"><div>A</div></ng-template>' +
'<div>B</div>' +
'<div>C</div>' +
'</outer>'
@ -260,7 +260,7 @@ function createTests({viewEngine}: {viewEngine: boolean}) {
TestBed.overrideComponent(MainComp, {
set: {
template: '<empty>' +
' <template manual><div>A</div></template>' +
' <ng-template manual><div>A</div></ng-template>' +
'</empty>' +
'START(<div project></div>)END'
}
@ -282,7 +282,7 @@ function createTests({viewEngine}: {viewEngine: boolean}) {
{declarations: [Empty, ProjectDirective, ManualViewportDirective]});
TestBed.overrideComponent(MainComp, {
set: {
template: '<simple><template manual><div>A</div></template></simple>' +
template: '<simple><ng-template manual><div>A</div></ng-template></simple>' +
'START(<div project></div>)END'
}
});
@ -488,7 +488,7 @@ function createTests({viewEngine}: {viewEngine: boolean}) {
set: {
template: '<conditional-content>' +
'<div class="left">A</div>' +
'<template manual class="left">B</template>' +
'<ng-template manual class="left">B</ng-template>' +
'<div class="left">C</div>' +
'<div>D</div>' +
'</conditional-content>'
@ -628,7 +628,7 @@ class ConditionalContentComponent {
@Component({
selector: 'conditional-text',
template:
'MAIN(<template manual>FIRST(<template manual>SECOND(<ng-content></ng-content>)</template>)</template>)',
'MAIN(<ng-template manual>FIRST(<ng-template manual>SECOND(<ng-content></ng-content>)</ng-template>)</ng-template>)',
})
class ConditionalTextComponent {
}

View File

@ -225,7 +225,7 @@ function createTests({viewEngine}: {viewEngine: boolean}) {
describe('query for TemplateRef', () => {
it('should find TemplateRefs in the light and shadow dom', () => {
const template = '<needs-tpl><template><div>light</div></template></needs-tpl>';
const template = '<needs-tpl><ng-template><div>light</div></ng-template></needs-tpl>';
const view = createTestCmpAndDetectChanges(MyComp0, template);
const needsTpl: NeedsTpl = view.debugElement.children[0].injector.get(NeedsTpl);
@ -237,7 +237,7 @@ function createTests({viewEngine}: {viewEngine: boolean}) {
it('should find named TemplateRefs', () => {
const template =
'<needs-named-tpl><template #tpl><div>light</div></template></needs-named-tpl>';
'<needs-named-tpl><ng-template #tpl><div>light</div></ng-template></needs-named-tpl>';
const view = createTestCmpAndDetectChanges(MyComp0, template);
const needsTpl: NeedsNamedTpl = view.debugElement.children[0].injector.get(NeedsNamedTpl);
expect(needsTpl.vc.createEmbeddedView(needsTpl.contentTpl).rootNodes[0])
@ -308,7 +308,7 @@ function createTests({viewEngine}: {viewEngine: boolean}) {
it('should support reading a ViewContainer', () => {
const template =
'<needs-viewcontainer-read><template>hello</template></needs-viewcontainer-read>';
'<needs-viewcontainer-read><ng-template>hello</ng-template></needs-viewcontainer-read>';
const view = createTestCmpAndDetectChanges(MyComp0, template);
const comp: NeedsViewContainerWithRead =
@ -403,7 +403,7 @@ function createTests({viewEngine}: {viewEngine: boolean}) {
it('should contain all the elements in the light dom with the given var binding', () => {
const template = '<needs-query-by-ref-binding #q>' +
'<div template="ngFor: let item of list">' +
'<div *ngFor="let item of list">' +
'<div #textLabel>{{item}}</div>' +
'</div>' +
'</needs-query-by-ref-binding>';
@ -536,7 +536,7 @@ function createTests({viewEngine}: {viewEngine: boolean}) {
describe('query over moved templates', () => {
it('should include manually projected templates in queries', () => {
const template =
'<manual-projecting #q><template><div text="1"></div></template></manual-projecting>';
'<manual-projecting #q><ng-template><div text="1"></div></ng-template></manual-projecting>';
const view = createTestCmpAndDetectChanges(MyComp0, template);
const q = view.debugElement.children[0].references['q'];
expect(q.query.length).toBe(0);
@ -730,14 +730,15 @@ class NeedsViewQueryOrderWithParent {
list: string[] = ['2', '3'];
}
@Component({selector: 'needs-tpl', template: '<template><div>shadow</div></template>'})
@Component({selector: 'needs-tpl', template: '<ng-template><div>shadow</div></ng-template>'})
class NeedsTpl {
@ViewChildren(TemplateRef) viewQuery: QueryList<TemplateRef<Object>>;
@ContentChildren(TemplateRef) query: QueryList<TemplateRef<Object>>;
constructor(public vc: ViewContainerRef) {}
}
@Component({selector: 'needs-named-tpl', template: '<template #tpl><div>shadow</div></template>'})
@Component(
{selector: 'needs-named-tpl', template: '<ng-template #tpl><div>shadow</div></ng-template>'})
class NeedsNamedTpl {
@ViewChild('tpl') viewTpl: TemplateRef<Object>;
@ContentChild('tpl') contentTpl: TemplateRef<Object>;
@ -767,7 +768,7 @@ class NeedsContentChildTemplateRef {
@Component({
selector: 'needs-content-child-template-ref-app',
template: '<needs-content-child-template-ref>' +
'<template>OUTER<template>INNER</template></template>' +
'<ng-template>OUTER<ng-template>INNER</ng-template></ng-template>' +
'</needs-content-child-template-ref>'
})
class NeedsContentChildTemplateRefApp {

View File

@ -656,7 +656,8 @@ function createTests({viewEngine}: {viewEngine: boolean}) {
it('should inject TemplateRef', () => {
TestBed.configureTestingModule({declarations: [NeedsViewContainerRef, NeedsTemplateRef]});
const el = createComponent('<template needsViewContainerRef needsTemplateRef></template>');
const el =
createComponent('<ng-template needsViewContainerRef needsTemplateRef></ng-template>');
expect(el.childNodes[0].injector.get(NeedsTemplateRef).templateRef.elementRef)
.toEqual(el.childNodes[0].injector.get(NeedsViewContainerRef).viewContainer.element);
});