diff --git a/packages/compiler-cli/test/compliance/r3_compiler_compliance_spec.ts b/packages/compiler-cli/test/compliance/r3_compiler_compliance_spec.ts index 8aaf4273a0..4afe687e2f 100644 --- a/packages/compiler-cli/test/compliance/r3_compiler_compliance_spec.ts +++ b/packages/compiler-cli/test/compliance/r3_compiler_compliance_spec.ts @@ -1013,7 +1013,7 @@ describe('compiler compliance', () => { }); }); - it('should support content projection', () => { + it('should support content projection in root template', () => { const files = { app: { 'spec.ts': ` @@ -1061,10 +1061,10 @@ describe('compiler compliance', () => { });`; const ComplexComponentDefinition = ` - const $c1$ = [[["span", "title", "tofirst"]], [["span", "title", "tosecond"]]]; - const $c2$ = ["span[title=toFirst]", "span[title=toSecond]"]; const $c3$ = ["id","first"]; const $c4$ = ["id","second"]; + const $c1$ = [[["span", "title", "tofirst"]], [["span", "title", "tosecond"]]]; + const $c2$ = ["span[title=toFirst]", "span[title=toSecond]"]; … ComplexComponent.ngComponentDef = $r3$.ɵdefineComponent({ type: ComplexComponent, @@ -1095,6 +1095,76 @@ describe('compiler compliance', () => { result.source, ComplexComponentDefinition, 'Incorrect ComplexComponent definition'); }); + it('should support content projection in nested templates', () => { + const files = { + app: { + 'spec.ts': ` + import {Component, NgModule} from '@angular/core'; + + @Component({ + template: \` +
+ +
+
+ No ng-content, no instructions generated. +
+ + '*' selector: + + \`, + }) + class Cmp {} + + @NgModule({ declarations: [Cmp] }) + class Module {} + ` + } + }; + const output = ` + const $_c0$ = [1, "ngIf"]; + const $_c1$ = ["id", "second"]; + const $_c2$ = [[["span", "title", "tofirst"]]]; + const $_c3$ = ["span[title=toFirst]"]; + function Cmp_div_Template_0(rf, ctx) { if (rf & 1) { + $r3$.ɵprojectionDef($_c2$, $_c3$); + $r3$.ɵelementStart(0, "div", $_c1$); + $r3$.ɵprojection(1, 1); + $r3$.ɵelementEnd(); + } } + const $_c4$ = ["id", "third"]; + function Cmp_div_Template_1(rf, ctx) { + if (rf & 1) { + $r3$.ɵelementStart(0, "div", $_c4$); + $r3$.ɵtext(1, " No ng-content, no instructions generated. "); + $r3$.ɵelementEnd(); + } + } + function Template_2(rf, ctx) { + if (rf & 1) { + $r3$.ɵprojectionDef(); + $r3$.ɵtext(0, " '*' selector: "); + $r3$.ɵprojection(1); + } + } + … + template: function Cmp_Template(rf, ctx) { + if (rf & 1) { + $r3$.ɵtemplate(0, Cmp_div_Template_0, 2, 0, null, $_c0$); + $r3$.ɵtemplate(1, Cmp_div_Template_1, 2, 0, null, $_c0$); + $r3$.ɵtemplate(2, Template_2, 2, 0); + } + if (rf & 2) { + $r3$.ɵelementProperty(0, "ngIf", $r3$.ɵbind(ctx.visible)); + $r3$.ɵelementProperty(1, "ngIf", $r3$.ɵbind(ctx.visible)); + } + } + `; + + const {source} = compile(files, angularFiles); + expectEmit(source, output, 'Invalid content projection instructions generated'); + }); + describe('queries', () => { const directive = { 'some.directive.ts': ` diff --git a/packages/compiler/src/render3/r3_ast.ts b/packages/compiler/src/render3/r3_ast.ts index 9501af0048..e524e157b1 100644 --- a/packages/compiler/src/render3/r3_ast.ts +++ b/packages/compiler/src/render3/r3_ast.ts @@ -83,7 +83,7 @@ export class Template implements Node { export class Content implements Node { constructor( - public selectorIndex: number, public attributes: TextAttribute[], + public selector: string, public attributes: TextAttribute[], public sourceSpan: ParseSourceSpan, public i18n?: I18nAST) {} visit(visitor: Visitor): Result { return visitor.visitContent(this); } } diff --git a/packages/compiler/src/render3/r3_template_transform.ts b/packages/compiler/src/render3/r3_template_transform.ts index 9bd4d4caa1..284b103b1f 100644 --- a/packages/compiler/src/render3/r3_template_transform.ts +++ b/packages/compiler/src/render3/r3_template_transform.ts @@ -45,16 +45,10 @@ const IDENT_PROPERTY_IDX = 9; const IDENT_EVENT_IDX = 10; const TEMPLATE_ATTR_PREFIX = '*'; -// Default selector used by `` if none specified -const DEFAULT_CONTENT_SELECTOR = '*'; // Result of the html AST to Ivy AST transformation export type Render3ParseResult = { nodes: t.Node[]; errors: ParseError[]; - // Any non default (empty or '*') selector found in the template - ngContentSelectors: string[]; - // Wether the template contains any `` - hasNgContent: boolean; }; export function htmlAstToRender3Ast( @@ -74,17 +68,11 @@ export function htmlAstToRender3Ast( return { nodes: ivyNodes, errors: allErrors, - ngContentSelectors: transformer.ngContentSelectors, - hasNgContent: transformer.hasNgContent, }; } class HtmlAstToIvyAst implements html.Visitor { errors: ParseError[] = []; - // Selectors for the `ng-content` tags. Only non `*` selectors are recorded here - ngContentSelectors: string[] = []; - // Any `` in the template ? - hasNgContent = false; constructor(private bindingParser: BindingParser) {} @@ -168,20 +156,12 @@ class HtmlAstToIvyAst implements html.Visitor { let parsedElement: t.Node|undefined; if (preparsedElement.type === PreparsedElementType.NG_CONTENT) { // `` - this.hasNgContent = true; - if (element.children && !element.children.every(isEmptyTextNode)) { this.reportError(` element cannot have content.`, element.sourceSpan); } - const selector = preparsedElement.selectAttr; - - let attributes: t.TextAttribute[] = - element.attrs.map(attribute => this.visitAttribute(attribute)); - - const selectorIndex = - selector === DEFAULT_CONTENT_SELECTOR ? 0 : this.ngContentSelectors.push(selector); - parsedElement = new t.Content(selectorIndex, attributes, element.sourceSpan, element.i18n); + const attrs: t.TextAttribute[] = element.attrs.map(attr => this.visitAttribute(attr)); + parsedElement = new t.Content(selector, attrs, element.sourceSpan, element.i18n); } else if (isTemplateElement) { // `` const attrs = this.extractAttributes(element.name, parsedProperties, i18nAttrsMeta); diff --git a/packages/compiler/src/render3/view/api.ts b/packages/compiler/src/render3/view/api.ts index 8fb10cc046..ae0929bcaf 100644 --- a/packages/compiler/src/render3/view/api.ts +++ b/packages/compiler/src/render3/view/api.ts @@ -123,16 +123,6 @@ export interface R3ComponentMetadata extends R3DirectiveMetadata { * Parsed nodes of the template. */ nodes: t.Node[]; - - /** - * Whether the template includes tags. - */ - hasNgContent: boolean; - - /** - * Selectors found in the tags in the template. - */ - ngContentSelectors: string[]; }; /** diff --git a/packages/compiler/src/render3/view/compiler.ts b/packages/compiler/src/render3/view/compiler.ts index 30a122e232..908a16569f 100644 --- a/packages/compiler/src/render3/view/compiler.ts +++ b/packages/compiler/src/render3/view/compiler.ts @@ -258,8 +258,7 @@ export function compileComponentFromMetadata( meta.viewQueries, directiveMatcher, directivesUsed, meta.pipes, pipesUsed, R3.namespaceHTML, meta.relativeContextFilePath, meta.i18nUseExternalIds); - const templateFunctionExpression = templateBuilder.buildTemplateFunction( - template.nodes, [], template.hasNgContent, template.ngContentSelectors); + const templateFunctionExpression = templateBuilder.buildTemplateFunction(template.nodes, []); // e.g. `consts: 2` definitionMap.set('consts', o.literal(templateBuilder.getConstCount())); @@ -371,11 +370,7 @@ export function compileComponentFromRender2( const meta: R3ComponentMetadata = { ...directiveMetadataFromGlobalMetadata(component, outputCtx, reflector), selector: component.selector, - template: { - nodes: render3Ast.nodes, - hasNgContent: render3Ast.hasNgContent, - ngContentSelectors: render3Ast.ngContentSelectors, - }, + template: {nodes: render3Ast.nodes}, directives: [], pipes: typeMapToExpressionMap(pipeTypeByName, outputCtx), viewQueries: queriesFromGlobalMetadata(component.viewQueries, outputCtx), diff --git a/packages/compiler/src/render3/view/template.ts b/packages/compiler/src/render3/view/template.ts index 29ca6fbe0b..ff3eabe2cc 100644 --- a/packages/compiler/src/render3/view/template.ts +++ b/packages/compiler/src/render3/view/template.ts @@ -58,6 +58,8 @@ export function renderFlagCheckIfStmt( return o.ifStmt(o.variable(RENDER_FLAGS).bitwiseAnd(o.literal(flags), null, false), statements); } +// Default selector used by `` if none specified +const DEFAULT_CONTENT_SELECTOR = '*'; export class TemplateDefinitionBuilder implements t.Visitor, LocalResolver { private _dataIndex = 0; private _bindingContext = 0; @@ -102,6 +104,12 @@ export class TemplateDefinitionBuilder implements t.Visitor, LocalResolver private fileBasedI18nSuffix: string; + // Whether the template includes tags. + private _hasNgContent: boolean = false; + + // Selectors found in the tags in the template. + private _ngContentSelectors: string[] = []; + constructor( private constantPool: ConstantPool, parentBindingScope: BindingScope, private level = 0, private contextName: string|null, private i18nContext: I18nContext|null, @@ -154,9 +162,7 @@ export class TemplateDefinitionBuilder implements t.Visitor, LocalResolver }); } - buildTemplateFunction( - nodes: t.Node[], variables: t.Variable[], hasNgContent: boolean = false, - ngContentSelectors: string[] = [], i18n?: i18n.AST): o.FunctionExpr { + buildTemplateFunction(nodes: t.Node[], variables: t.Variable[], i18n?: i18n.AST): o.FunctionExpr { if (this._namespace !== R3.namespaceHTML) { this.creationInstruction(null, this._namespace); } @@ -164,22 +170,6 @@ export class TemplateDefinitionBuilder implements t.Visitor, LocalResolver // Create variable bindings variables.forEach(v => this.registerContextVariables(v)); - // Output a `ProjectionDef` instruction when some `` are present - if (hasNgContent) { - const parameters: o.Expression[] = []; - - // Only selectors with a non-default value are generated - if (ngContentSelectors.length > 1) { - const r3Selectors = ngContentSelectors.map(s => core.parseSelectorToR3Selector(s)); - // `projectionDef` needs both the parsed and raw value of the selectors - const parsed = this.constantPool.getConstLiteral(asLiteral(r3Selectors), true); - const unParsed = this.constantPool.getConstLiteral(asLiteral(ngContentSelectors), true); - parameters.push(parsed, unParsed); - } - - this.creationInstruction(null, R3.projectionDef, parameters); - } - // Initiate i18n context in case: // - this template has parent i18n context // - or the template has i18n meta associated with it, @@ -198,6 +188,26 @@ export class TemplateDefinitionBuilder implements t.Visitor, LocalResolver // resolving bindings. We also count bindings in this pass as we walk bound expressions. t.visitAll(this, nodes); + // Output a `ProjectionDef` instruction when some `` are present + if (this._hasNgContent) { + const parameters: o.Expression[] = []; + + // Only selectors with a non-default value are generated + if (this._ngContentSelectors.length) { + const r3Selectors = this._ngContentSelectors.map(s => core.parseSelectorToR3Selector(s)); + // `projectionDef` needs both the parsed and raw value of the selectors + const parsed = this.constantPool.getConstLiteral(asLiteral(r3Selectors), true); + const unParsed = + this.constantPool.getConstLiteral(asLiteral(this._ngContentSelectors), true); + parameters.push(parsed, unParsed); + } + + // Since we accumulate ngContent selectors while processing template elements, + // we *prepend* `projectionDef` to creation instructions block, to put it before + // any `projection` instructions + this.creationInstruction(null, R3.projectionDef, parameters, /* prepend */ true); + } + // Add total binding count to pure function count so pure function instructions are // generated with the correct slot offset when update instructions are processed. this._pureFunctionSlots += this._bindingSlots; @@ -399,8 +409,11 @@ export class TemplateDefinitionBuilder implements t.Visitor, LocalResolver } visitContent(ngContent: t.Content) { + this._hasNgContent = true; const slot = this.allocateDataSlot(); - const selectorIndex = ngContent.selectorIndex; + let selectorIndex = ngContent.selector === DEFAULT_CONTENT_SELECTOR ? + 0 : + this._ngContentSelectors.push(ngContent.selector); const parameters: o.Expression[] = [o.literal(slot)]; const attributeAsList: string[] = []; @@ -724,7 +737,7 @@ export class TemplateDefinitionBuilder implements t.Visitor, LocalResolver // template definition. e.g.
{{ foo }}
this._nestedTemplateFns.push(() => { const templateFunctionExpr = templateVisitor.buildTemplateFunction( - template.children, template.variables, false, [], template.i18n); + template.children, template.variables, template.i18n); this.constantPool.statements.push(templateFunctionExpr.toDeclStmt(templateName, null)); }); @@ -834,8 +847,8 @@ export class TemplateDefinitionBuilder implements t.Visitor, LocalResolver // bindings. e.g. {{ foo }}
private instructionFn( fns: (() => o.Statement)[], span: ParseSourceSpan|null, reference: o.ExternalReference, - paramsOrFn: o.Expression[]|(() => o.Expression[])): void { - fns.push(() => { + paramsOrFn: o.Expression[]|(() => o.Expression[]), prepend: boolean = false): void { + fns[prepend ? 'unshift' : 'push'](() => { const params = Array.isArray(paramsOrFn) ? paramsOrFn : paramsOrFn(); return instruction(span, reference, params).toStmt(); }); @@ -856,8 +869,8 @@ export class TemplateDefinitionBuilder implements t.Visitor, LocalResolver private creationInstruction( span: ParseSourceSpan|null, reference: o.ExternalReference, - paramsOrFn?: o.Expression[]|(() => o.Expression[])) { - this.instructionFn(this._creationCodeFns, span, reference, paramsOrFn || []); + paramsOrFn?: o.Expression[]|(() => o.Expression[]), prepend?: boolean) { + this.instructionFn(this._creationCodeFns, span, reference, paramsOrFn || [], prepend); } private updateInstruction( @@ -1398,14 +1411,14 @@ function interpolate(args: o.Expression[]): o.Expression { export function parseTemplate( template: string, templateUrl: string, options: {preserveWhitespaces?: boolean, interpolationConfig?: InterpolationConfig} = {}): - {errors?: ParseError[], nodes: t.Node[], hasNgContent: boolean, ngContentSelectors: string[]} { + {errors?: ParseError[], nodes: t.Node[]} { const {interpolationConfig, preserveWhitespaces} = options; const bindingParser = makeBindingParser(interpolationConfig); const htmlParser = new HtmlParser(); const parseResult = htmlParser.parse(template, templateUrl, true, interpolationConfig); if (parseResult.errors && parseResult.errors.length > 0) { - return {errors: parseResult.errors, nodes: [], hasNgContent: false, ngContentSelectors: []}; + return {errors: parseResult.errors, nodes: []}; } let rootNodes: html.Node[] = parseResult.rootNodes; @@ -1428,13 +1441,12 @@ export function parseTemplate( new I18nMetaVisitor(interpolationConfig, /* keepI18nAttrs */ false), rootNodes); } - const {nodes, hasNgContent, ngContentSelectors, errors} = - htmlAstToRender3Ast(rootNodes, bindingParser); + const {nodes, errors} = htmlAstToRender3Ast(rootNodes, bindingParser); if (errors && errors.length > 0) { - return {errors, nodes: [], hasNgContent: false, ngContentSelectors: []}; + return {errors, nodes: []}; } - return {nodes, hasNgContent, ngContentSelectors}; + return {nodes}; } /** diff --git a/packages/compiler/test/render3/r3_template_transform_spec.ts b/packages/compiler/test/render3/r3_template_transform_spec.ts index 477fac1be0..5e8a541b2a 100644 --- a/packages/compiler/test/render3/r3_template_transform_spec.ts +++ b/packages/compiler/test/render3/r3_template_transform_spec.ts @@ -39,7 +39,7 @@ class R3AstHumanizer implements t.Visitor { } visitContent(content: t.Content) { - this.result.push(['Content', content.selectorIndex]); + this.result.push(['Content', content.selector]); t.visitAll(this, content.attributes); } @@ -110,17 +110,15 @@ describe('R3 template transform', () => { it('should parse ngContent', () => { const res = parse(''); - expect(res.hasNgContent).toEqual(true); - expect(res.ngContentSelectors).toEqual(['a']); expectFromR3Nodes(res.nodes).toEqual([ - ['Content', 1], + ['Content', 'a'], ['TextAttribute', 'select', 'a'], ]); }); it('should parse ngContent when it contains WS only', () => { expectFromHtml(' \n ').toEqual([ - ['Content', 1], + ['Content', 'a'], ['TextAttribute', 'select', 'a'], ]); }); @@ -128,7 +126,7 @@ describe('R3 template transform', () => { it('should parse ngContent regardless the namespace', () => { expectFromHtml('').toEqual([ ['Element', ':svg:svg'], - ['Content', 1], + ['Content', 'a'], ['TextAttribute', 'select', 'a'], ]); }); @@ -377,30 +375,16 @@ describe('R3 template transform', () => { describe('ng-content', () => { it('should parse ngContent without selector', () => { const res = parse(''); - expect(res.hasNgContent).toEqual(true); - expect(res.ngContentSelectors).toEqual([]); expectFromR3Nodes(res.nodes).toEqual([ - ['Content', 0], - ]); - }); - - it('should parse ngContent with a * selector', () => { - const res = parse(''); - const selectors = ['']; - expect(res.hasNgContent).toEqual(true); - expect(res.ngContentSelectors).toEqual([]); - expectFromR3Nodes(res.nodes).toEqual([ - ['Content', 0], + ['Content', '*'], ]); }); it('should parse ngContent with a specific selector', () => { const res = parse(''); const selectors = ['', 'tag[attribute]']; - expect(res.hasNgContent).toEqual(true); - expect(res.ngContentSelectors).toEqual(['tag[attribute]']); expectFromR3Nodes(res.nodes).toEqual([ - ['Content', 1], + ['Content', selectors[1]], ['TextAttribute', 'select', selectors[1]], ]); }); @@ -408,24 +392,20 @@ describe('R3 template transform', () => { it('should parse ngContent with a selector', () => { const res = parse( ''); - const selectors = ['', 'a', 'b']; - expect(res.hasNgContent).toEqual(true); - expect(res.ngContentSelectors).toEqual(['a', 'b']); + const selectors = ['*', 'a', 'b']; expectFromR3Nodes(res.nodes).toEqual([ - ['Content', 1], + ['Content', selectors[1]], ['TextAttribute', 'select', selectors[1]], - ['Content', 0], - ['Content', 2], + ['Content', selectors[0]], + ['Content', selectors[2]], ['TextAttribute', 'select', selectors[2]], ]); }); it('should parse ngProjectAs as an attribute', () => { const res = parse(''); - expect(res.hasNgContent).toEqual(true); - expect(res.ngContentSelectors).toEqual([]); expectFromR3Nodes(res.nodes).toEqual([ - ['Content', 0], + ['Content', '*'], ['TextAttribute', 'ngProjectAs', 'a'], ]); });