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

@ -563,16 +563,23 @@ Binding to attribute 'onEvent' is disallowed for security reasons ("<my-componen
const dirA =
CompileDirectiveMetadata
.create({
selector: 'template',
selector: 'template,ng-template',
outputs: ['e'],
type: createTypeMeta({reference: {filePath: someModuleUrl, name: 'DirA'}})
})
.toSummary();
expect(humanizeTplAst(parse('<template (e)="f"></template>', [dirA]))).toEqual([
[EmbeddedTemplateAst],
[BoundEventAst, 'e', null, 'f'],
[DirectiveAst, dirA],
]);
expect(humanizeTplAst(parse('<ng-template (e)="f"></ng-template>', [dirA]))).toEqual([
[EmbeddedTemplateAst],
[BoundEventAst, 'e', null, 'f'],
[DirectiveAst, dirA],
]);
});
});
@ -1167,7 +1174,8 @@ Reference "#a" is defined several times ("<div #a></div><div [ERROR ->]#a></div>
() => {
expect(() => parse('<div #a><template #a><span>OK</span></template></div>', []))
.not.toThrowError();
expect(() => parse('<div #a><ng-template #a><span>OK</span></ng-template></div>', []))
.not.toThrowError();
});
it('should assign references with empty value to components', () => {
@ -1204,25 +1212,35 @@ Reference "#a" is defined several times ("<div #a></div><div [ERROR ->]#a></div>
});
describe('explicit templates', () => {
it('should create embedded templates for <template> elements', () => {
it('should create embedded templates for <ng-template> elements', () => {
expect(humanizeTplAst(parse('<template></template>', [
]))).toEqual([[EmbeddedTemplateAst]]);
expect(humanizeTplAst(parse('<TEMPLATE></TEMPLATE>', [
]))).toEqual([[EmbeddedTemplateAst]]);
expect(humanizeTplAst(parse('<ng-template></ng-template>', [
]))).toEqual([[EmbeddedTemplateAst]]);
});
it('should create embedded templates for <template> elements regardless the namespace',
it('should create embedded templates for <ng-template> elements regardless the namespace',
() => {
expect(humanizeTplAst(parse('<svg><template></template></svg>', []))).toEqual([
[ElementAst, ':svg:svg'],
[EmbeddedTemplateAst],
]);
expect(humanizeTplAst(parse('<svg><ng-template></ng-template></svg>', []))).toEqual([
[ElementAst, ':svg:svg'],
[EmbeddedTemplateAst],
]);
});
it('should support references via #...', () => {
expect(humanizeTplAst(parse('<template #a>', []))).toEqual([
[EmbeddedTemplateAst],
[ReferenceAst, 'a', createIdentifierToken(Identifiers.TemplateRef)]
[ReferenceAst, 'a', createIdentifierToken(Identifiers.TemplateRef)],
]);
expect(humanizeTplAst(parse('<ng-template #a>', []))).toEqual([
[EmbeddedTemplateAst],
[ReferenceAst, 'a', createIdentifierToken(Identifiers.TemplateRef)],
]);
});
@ -1231,11 +1249,21 @@ Reference "#a" is defined several times ("<div #a></div><div [ERROR ->]#a></div>
[EmbeddedTemplateAst],
[ReferenceAst, 'a', createIdentifierToken(Identifiers.TemplateRef)]
]);
expect(humanizeTplAst(parse('<ng-template ref-a>', []))).toEqual([
[EmbeddedTemplateAst],
[ReferenceAst, 'a', createIdentifierToken(Identifiers.TemplateRef)]
]);
});
it('should parse variables via let-...', () => {
expect(humanizeTplAst(parse('<template let-a="b">', [
]))).toEqual([[EmbeddedTemplateAst], [VariableAst, 'a', 'b']]);
expect(humanizeTplAst(parse('<template let-a="b">', []))).toEqual([
[EmbeddedTemplateAst],
[VariableAst, 'a', 'b'],
]);
expect(humanizeTplAst(parse('<ng-template let-a="b">', []))).toEqual([
[EmbeddedTemplateAst],
[VariableAst, 'a', 'b'],
]);
});
it('should not locate directives in variables', () => {
@ -1247,7 +1275,12 @@ Reference "#a" is defined several times ("<div #a></div><div [ERROR ->]#a></div>
})
.toSummary();
expect(humanizeTplAst(parse('<template let-a="b"></template>', [dirA]))).toEqual([
[EmbeddedTemplateAst], [VariableAst, 'a', 'b']
[EmbeddedTemplateAst],
[VariableAst, 'a', 'b'],
]);
expect(humanizeTplAst(parse('<ng-template let-a="b"></ng-template>', [dirA]))).toEqual([
[EmbeddedTemplateAst],
[VariableAst, 'a', 'b'],
]);
});
@ -1255,8 +1288,10 @@ Reference "#a" is defined several times ("<div #a></div><div [ERROR ->]#a></div>
describe('inline templates', () => {
it('should wrap the element into an EmbeddedTemplateAST', () => {
expect(humanizeTplAst(parse('<div template>', [
]))).toEqual([[EmbeddedTemplateAst], [ElementAst, 'div']]);
expect(humanizeTplAst(parse('<div template>', []))).toEqual([
[EmbeddedTemplateAst],
[ElementAst, 'div'],
]);
});
it('should wrap the element with data-template attribute into an EmbeddedTemplateAST ',
@ -1403,7 +1438,10 @@ Reference "#a" is defined several times ("<div #a></div><div [ERROR ->]#a></div>
describe('project text nodes', () => {
it('should project text nodes with wildcard selector', () => {
expect(humanizeContentProjection(parse('<div>hello</div>', [createComp('div', ['*'])])))
.toEqual([['div', null], ['#text(hello)', 0]]);
.toEqual([
['div', null],
['#text(hello)', 0],
]);
});
});
@ -1415,24 +1453,37 @@ Reference "#a" is defined several times ("<div #a></div><div [ERROR ->]#a></div>
});
it('should project elements with css selector', () => {
expect(humanizeContentProjection(parse('<div><a x></a><b></b></div>', [
createComp('div', ['a[x]'])
]))).toEqual([['div', null], ['a', 0], ['b', null]]);
expect(humanizeContentProjection(
parse('<div><a x></a><b></b></div>', [createComp('div', ['a[x]'])])))
.toEqual([
['div', null],
['a', 0],
['b', null],
]);
});
});
describe('embedded templates', () => {
it('should project embedded templates with wildcard selector', () => {
expect(humanizeContentProjection(parse('<div><template></template></div>', [
createComp('div', ['*'])
]))).toEqual([['div', null], ['template', 0]]);
expect(humanizeContentProjection(parse(
'<div><template></template><ng-template></ng-template></div>',
[createComp('div', ['*'])])))
.toEqual([
['div', null],
['template', 0],
['template', 0],
]);
});
it('should project embedded templates with css selector', () => {
expect(humanizeContentProjection(parse(
'<div><template x></template><template></template></div>',
[createComp('div', ['template[x]'])])))
.toEqual([['div', null], ['template', 0], ['template', null]]);
'<div><ng-template x></ng-template><ng-template></ng-template></div>',
[createComp('div', ['ng-template[x]'])])))
.toEqual([
['div', null],
['template', 0],
['template', null],
]);
});
});
@ -1501,18 +1552,27 @@ Reference "#a" is defined several times ("<div #a></div><div [ERROR ->]#a></div>
.toEqual([['div', null], ['ng-content', 1]]);
});
it('should override <template>', () => {
expect(humanizeContentProjection(parse(
'<div><template ngProjectAs="b"></template></div>',
[createComp('div', ['template', 'b'])])))
.toEqual([['div', null], ['template', 1]]);
it('should override <ng-template>', () => {
expect(
humanizeContentProjection(parse(
'<div><template ngProjectAs="b"></template><ng-template ngProjectAs="b"></ng-template></div>',
[createComp('div', ['template', 'b'])])))
.toEqual([
['div', null],
['template', 1],
['template', 1],
]);
});
it('should override inline templates', () => {
expect(humanizeContentProjection(parse(
'<div><a *ngIf="cond" ngProjectAs="b"></a></div>',
[createComp('div', ['a', 'b']), ngIf])))
.toEqual([['div', null], ['template', 1], ['a', null]]);
.toEqual([
['div', null],
['template', 1],
['a', null],
]);
});
});
@ -1539,13 +1599,17 @@ Reference "#a" is defined several times ("<div #a></div><div [ERROR ->]#a></div>
`<ng-content> element cannot have content. ("[ERROR ->]<ng-content>content</ng-content>"): TestComp@0:0`);
});
it('should treat *attr on a template element as valid',
() => { expect(() => parse('<template *ngIf>', [])).not.toThrowError(); });
it('should treat *attr on a template element as valid', () => {
expect(() => parse('<template *ngIf>', [])).not.toThrowError();
expect(() => parse('<ng-template *ngIf>', [])).not.toThrowError();
});
it('should treat template attribute on a template element as valid',
() => { expect(() => parse('<template template="ngIf">', [])).not.toThrowError(); });
it('should treat template attribute on a template element as valid', () => {
expect(() => parse('<template template="ngIf">', [])).not.toThrowError();
expect(() => parse('<ng-template template="ngIf">', [])).not.toThrowError();
});
it('should report when mutliple *attrs are used on the same element', () => {
it('should report when multiple *attrs are used on the same element', () => {
expect(() => parse('<div *ngIf *ngFor>', [])).toThrowError(`Template parse errors:
Can't have multiple template bindings on one element. Use only one attribute named 'template' or prefixed with * ("<div *ngIf [ERROR ->]*ngFor>"): TestComp@0:11`);
});
@ -1630,11 +1694,18 @@ Parser Error: Unexpected token 'b' at column 3 in [a b] in TestComp@0:5 ("<div [
template: new CompileTemplateMetadata({ngContentSelectors: []})
})
.toSummary();
expect(() => parse('<template [a]="b" (e)="f"></template>', [dirA]))
.toThrowError(`Template parse errors:
Event binding e not emitted by any directive on an embedded template. Make sure that the event name is spelled correctly and all directives are listed in the "@NgModule.declarations". ("<template [a]="b" [ERROR ->](e)="f"></template>"): TestComp@0:18
Components on an embedded template: DirA ("[ERROR ->]<template [a]="b" (e)="f"></template>"): TestComp@0:0
Property binding a not used by any directive on an embedded template. Make sure that the property name is spelled correctly and all directives are listed in the "@NgModule.declarations". ("[ERROR ->]<template [a]="b" (e)="f"></template>"): TestComp@0:0`);
expect(() => parse('<ng-template [a]="b" (e)="f"></ng-template>', [dirA]))
.toThrowError(`Template parse errors:
Event binding e not emitted by any directive on an embedded template. Make sure that the event name is spelled correctly and all directives are listed in the "@NgModule.declarations". ("<ng-template [a]="b" [ERROR ->](e)="f"></ng-template>"): TestComp@0:21
Components on an embedded template: DirA ("[ERROR ->]<ng-template [a]="b" (e)="f"></ng-template>"): TestComp@0:0
Property binding a not used by any directive on an embedded template. Make sure that the property name is spelled correctly and all directives are listed in the "@NgModule.declarations". ("[ERROR ->]<ng-template [a]="b" (e)="f"></ng-template>"): TestComp@0:0`);
});
it('should not allow components or element bindings on inline embedded templates', () => {
@ -1745,7 +1816,8 @@ Property binding a not used by any directive on an embedded template. Make sure
it('should support embedded template', () => {
expect(humanizeTplAstSourceSpans(parse('<template></template>', [
]))).toEqual([[EmbeddedTemplateAst, '<template>']]);
expect(humanizeTplAstSourceSpans(parse('<ng-template></ng-template>', [
]))).toEqual([[EmbeddedTemplateAst, '<ng-template>']]);
});
it('should support element and attributes', () => {
@ -1762,8 +1834,14 @@ Property binding a not used by any directive on an embedded template. Make sure
it('should support variables', () => {
expect(humanizeTplAstSourceSpans(parse('<template let-a="b"></template>', []))).toEqual([
[EmbeddedTemplateAst, '<template let-a="b">'], [VariableAst, 'a', 'b', 'let-a="b"']
[EmbeddedTemplateAst, '<template let-a="b">'],
[VariableAst, 'a', 'b', 'let-a="b"'],
]);
expect(humanizeTplAstSourceSpans(parse('<ng-template let-a="b"></ng-template>', [])))
.toEqual([
[EmbeddedTemplateAst, '<ng-template let-a="b">'],
[VariableAst, 'a', 'b', 'let-a="b"'],
]);
});
it('should support events', () => {
@ -1917,8 +1995,8 @@ The pipe 'test' could not be found ("{{[ERROR ->]a | test}}"): TestComp@0:2`);
it('should expand plural messages', () => {
const shortForm = '{ count, plural, =0 {small} many {big} }';
const expandedForm = '<ng-container [ngPlural]="count">' +
'<template ngPluralCase="=0">small</template>' +
'<template ngPluralCase="many">big</template>' +
'<ng-template ngPluralCase="=0">small</ng-template>' +
'<ng-template ngPluralCase="many">big</ng-template>' +
'</ng-container>';
expect(humanizeTplAst(parse(shortForm, []))).toEqual(humanizeTplAst(parse(expandedForm, [
@ -1928,8 +2006,8 @@ The pipe 'test' could not be found ("{{[ERROR ->]a | test}}"): TestComp@0:2`);
it('should expand select messages', () => {
const shortForm = '{ sex, select, female {foo} other {bar} }';
const expandedForm = '<ng-container [ngSwitch]="sex">' +
'<template ngSwitchCase="female">foo</template>' +
'<template ngSwitchDefault>bar</template>' +
'<ng-template ngSwitchCase="female">foo</ng-template>' +
'<ng-template ngSwitchDefault>bar</ng-template>' +
'</ng-container>';
expect(humanizeTplAst(parse(shortForm, []))).toEqual(humanizeTplAst(parse(expandedForm, [
@ -2057,10 +2135,6 @@ class TemplateHumanizer implements TemplateAstVisitor {
}
}
function sourceInfo(ast: TemplateAst): string {
return `${ast.sourceSpan}: ${ast.sourceSpan.start}`;
}
function humanizeContentProjection(templateAsts: TemplateAst[]): any[] {
const humanizer = new TemplateContentProjectionHumanizer();
templateVisitAll(humanizer, templateAsts);