@@ -18,7 +18,7 @@ import {BindingParser} from '../../../template_parser/binding_parser';
1818import * as ir from '../ir' ;
1919
2020import { ComponentCompilationJob , HostBindingCompilationJob , type CompilationJob , type ViewCompilationUnit } from './compilation' ;
21- import { BINARY_OPERATORS , namespaceForKey } from './conversion' ;
21+ import { BINARY_OPERATORS , namespaceForKey , prefixWithNamespace } from './conversion' ;
2222
2323const compatibilityMode = ir . CompatibilityMode . TemplateDefinitionBuilder ;
2424
@@ -149,10 +149,6 @@ function ingestElement(unit: ViewCompilationUnit, element: t.Element): void {
149149 throw Error ( `Unhandled i18n metadata type for element: ${ element . i18n . constructor . name } ` ) ;
150150 }
151151
152- const staticAttributes : Record < string , string > = { } ;
153- for ( const attr of element . attributes ) {
154- staticAttributes [ attr . name ] = attr . value ;
155- }
156152 const id = unit . job . allocateXrefId ( ) ;
157153
158154 const [ namespaceKey , elementName ] = splitNsName ( element . name ) ;
@@ -196,9 +192,13 @@ function ingestTemplate(unit: ViewCompilationUnit, tmpl: t.Template): void {
196192 }
197193
198194 const i18nPlaceholder = tmpl . i18n instanceof i18n . TagPlaceholder ? tmpl . i18n : undefined ;
195+ const namespace = namespaceForKey ( namespacePrefix ) ;
196+ const functionNameSuffix = tagNameWithoutNamespace === null ?
197+ '' :
198+ prefixWithNamespace ( tagNameWithoutNamespace , namespace ) ;
199199 const tplOp = ir . createTemplateOp (
200- childView . xref , tagNameWithoutNamespace , namespaceForKey ( namespacePrefix ) , false ,
201- i18nPlaceholder , tmpl . startSourceSpan ) ;
200+ childView . xref , tagNameWithoutNamespace , functionNameSuffix , namespace , i18nPlaceholder ,
201+ tmpl . startSourceSpan ) ;
202202 unit . create . push ( tplOp ) ;
203203
204204 ingestBindings ( unit , tplOp , tmpl ) ;
@@ -281,20 +281,29 @@ function ingestIfBlock(unit: ViewCompilationUnit, ifBlock: t.IfBlock): void {
281281 let firstXref : ir . XrefId | null = null ;
282282 let firstSlotHandle : ir . SlotHandle | null = null ;
283283 let conditions : Array < ir . ConditionalCaseExpr > = [ ] ;
284- for ( const ifCase of ifBlock . branches ) {
284+ for ( let i = 0 ; i < ifBlock . branches . length ; i ++ ) {
285+ const ifCase = ifBlock . branches [ i ] ;
285286 const cView = unit . job . allocateView ( unit . xref ) ;
287+ let tagName : string | null = null ;
288+
289+ // Only the first branch can be used for projection, because the conditional
290+ // uses the container of the first branch as the insertion point for all branches.
291+ if ( i === 0 ) {
292+ tagName = ingestControlFlowInsertionPoint ( unit , cView . xref , ifCase ) ;
293+ }
286294 if ( ifCase . expressionAlias !== null ) {
287295 cView . contextVariables . set ( ifCase . expressionAlias . name , ir . CTX_REF ) ;
288296 }
289297 const tmplOp = ir . createTemplateOp (
290- cView . xref , 'Conditional' , ir . Namespace . HTML , true ,
298+ cView . xref , tagName , 'Conditional' , ir . Namespace . HTML ,
291299 undefined /* TODO: figure out how i18n works with new control flow */ , ifCase . sourceSpan ) ;
292300 unit . create . push ( tmplOp ) ;
293301
294302 if ( firstXref === null ) {
295303 firstXref = cView . xref ;
296304 firstSlotHandle = tmplOp . slot ;
297305 }
306+
298307 const caseExpr = ifCase . expression ? convertAst ( ifCase . expression , unit . job , null ) : null ;
299308 const conditionalCaseExpr =
300309 new ir . ConditionalCaseExpr ( caseExpr , tmplOp . xref , tmplOp . slot , ifCase . expressionAlias ) ;
@@ -316,7 +325,7 @@ function ingestSwitchBlock(unit: ViewCompilationUnit, switchBlock: t.SwitchBlock
316325 for ( const switchCase of switchBlock . cases ) {
317326 const cView = unit . job . allocateView ( unit . xref ) ;
318327 const tmplOp = ir . createTemplateOp (
319- cView . xref , 'Case' , ir . Namespace . HTML , true ,
328+ cView . xref , null , 'Case' , ir . Namespace . HTML ,
320329 undefined /* TODO: figure out how i18n works with new control flow */ ,
321330 switchCase . sourceSpan ) ;
322331 unit . create . push ( tmplOp ) ;
@@ -346,7 +355,7 @@ function ingestDeferView(
346355 const secondaryView = unit . job . allocateView ( unit . xref ) ;
347356 ingestNodes ( secondaryView , children ) ;
348357 const templateOp = ir . createTemplateOp (
349- secondaryView . xref , `Defer${ suffix } ` , ir . Namespace . HTML , true , undefined , sourceSpan ! ) ;
358+ secondaryView . xref , null , `Defer${ suffix } ` , ir . Namespace . HTML , undefined , sourceSpan ! ) ;
350359 unit . create . push ( templateOp ) ;
351360 return templateOp ;
352361}
@@ -503,8 +512,9 @@ function ingestForBlock(unit: ViewCompilationUnit, forBlock: t.ForLoopBlock): vo
503512 $implicit : forBlock . item . name ,
504513 } ;
505514
515+ const tagName = ingestControlFlowInsertionPoint ( unit , repeaterView . xref , forBlock ) ;
506516 const repeaterCreate = ir . createRepeaterCreateOp (
507- repeaterView . xref , emptyView ?. xref ?? null , track , varNames , forBlock . sourceSpan ) ;
517+ repeaterView . xref , emptyView ?. xref ?? null , tagName , track , varNames , forBlock . sourceSpan ) ;
508518 unit . create . push ( repeaterCreate ) ;
509519
510520 const expression = convertAst (
@@ -841,3 +851,64 @@ function convertSourceSpan(
841851 const fullStart = baseSourceSpan . fullStart . moveBy ( span . start ) ;
842852 return new ParseSourceSpan ( start , end , fullStart ) ;
843853}
854+
855+ /**
856+ * With the directive-based control flow users were able to conditionally project content using
857+ * the `*` syntax. E.g. `<div *ngIf="expr" projectMe></div>` will be projected into
858+ * `<ng-content select="[projectMe]"/>`, because the attributes and tag name from the `div` are
859+ * copied to the template via the template creation instruction. With `@if` and `@for` that is
860+ * not the case, because the conditional is placed *around* elements, rather than *on* them.
861+ * The result is that content projection won't work in the same way if a user converts from
862+ * `*ngIf` to `@if`.
863+ *
864+ * This function aims to cover the most common case by doing the same copying when a control flow
865+ * node has *one and only one* root element or template node.
866+ *
867+ * This approach comes with some caveats:
868+ * 1. As soon as any other node is added to the root, the copying behavior won't work anymore.
869+ * A diagnostic will be added to flag cases like this and to explain how to work around it.
870+ * 2. If `preserveWhitespaces` is enabled, it's very likely that indentation will break this
871+ * workaround, because it'll include an additional text node as the first child. We can work
872+ * around it here, but in a discussion it was decided not to, because the user explicitly opted
873+ * into preserving the whitespace and we would have to drop it from the generated code.
874+ * The diagnostic mentioned point #1 will flag such cases to users.
875+ *
876+ * @returns Tag name to be used for the control flow template.
877+ */
878+ function ingestControlFlowInsertionPoint (
879+ unit : ViewCompilationUnit , xref : ir . XrefId , node : t . IfBlockBranch | t . ForLoopBlock ) : string | null {
880+ let root : t . Element | t . Template | null = null ;
881+
882+ for ( const child of node . children ) {
883+ // Skip over comment nodes.
884+ if ( child instanceof t . Comment ) {
885+ continue ;
886+ }
887+
888+ // We can only infer the tag name/attributes if there's a single root node.
889+ if ( root !== null ) {
890+ return null ;
891+ }
892+
893+ // Root nodes can only elements or templates with a tag name (e.g. `<div *foo></div>`).
894+ if ( child instanceof t . Element || ( child instanceof t . Template && child . tagName !== null ) ) {
895+ root = child ;
896+ }
897+ }
898+
899+ // If we've found a single root node, its tag name and *static* attributes can be copied
900+ // to the surrounding template to be used for content projection. Note that it's important
901+ // that we don't copy any bound attributes since they don't participate in content projection
902+ // and they can be used in directive matching (in the case of `Template.templateAttrs`).
903+ if ( root !== null ) {
904+ for ( const attr of root . attributes ) {
905+ ingestBinding (
906+ unit , xref , attr . name , o . literal ( attr . value ) , e . BindingType . Attribute , null ,
907+ SecurityContext . NONE , attr . sourceSpan , BindingFlags . TextValue ) ;
908+ }
909+
910+ return root instanceof t . Element ? root . name : root . tagName ;
911+ }
912+
913+ return null ;
914+ }
0 commit comments