55 * Use of this source code is governed by an MIT-style license that can be
66 * found in the LICENSE file at https://angular.io/license
77 */
8+ import { findMatchingDehydratedView } from '../../hydration/views' ;
89import { newArray } from '../../util/array_utils' ;
10+ import { assertLContainer , assertTNode } from '../assert' ;
911import { ComponentTemplate } from '../interfaces/definition' ;
1012import { TAttributes , TElementNode , TNode , TNodeFlags , TNodeType } from '../interfaces/node' ;
1113import { ProjectionSlots } from '../interfaces/projection' ;
12- import { DECLARATION_COMPONENT_VIEW , HEADER_OFFSET , HYDRATION , T_HOST } from '../interfaces/view' ;
14+ import { DECLARATION_COMPONENT_VIEW , HEADER_OFFSET , HYDRATION , LView , T_HOST , TView } from '../interfaces/view' ;
1315import { applyProjection } from '../node_manipulation' ;
1416import { getProjectAsAttrValue , isNodeMatchingSelectorList , isSelectorInSelectorList } from '../node_selector_matcher' ;
1517import { getLView , getTView , isInSkipHydrationBlock , setCurrentTNodeAsNotParent } from '../state' ;
18+ import { getTNode } from '../util/view_utils' ;
19+ import { addLViewToLContainer , createAndRenderEmbeddedLView , shouldAddViewToDom } from '../view_manipulation' ;
1620
1721import { getOrCreateTNode } from './shared' ;
22+ import { ɵɵtemplate } from './template' ;
1823
1924
2025
@@ -110,10 +115,15 @@ export function ɵɵprojectionDef(projectionSlots?: ProjectionSlots): void {
110115 * Inserts previously re-distributed projected nodes. This instruction must be preceded by a call
111116 * to the projectionDef instruction.
112117 *
113- * @param nodeIndex
114- * @param selectorIndex:
115- * - 0 when the selector is `*` (or unspecified as this is the default value),
116- * - 1 based index of the selector from the {@link projectionDef}
118+ * @param nodeIndex Index of the projection node.
119+ * @param selectorIndex Index of the slot selector.
120+ * - 0 when the selector is `*` (or unspecified as this is the default value),
121+ * - 1 based index of the selector from the {@link projectionDef}
122+ * @param attrs Static attributes set on the `ng-content` node.
123+ * @param fallbackTemplateFn Template function with fallback content.
124+ * Will be rendered if the slot is empty at runtime.
125+ * @param fallbackDecls Number of declarations in the fallback template.
126+ * @param fallbackVars Number of variables in the fallback template.
117127 *
118128 * @codeGenApi
119129 */
@@ -127,16 +137,44 @@ export function ɵɵprojection(
127137 getOrCreateTNode ( tView , HEADER_OFFSET + nodeIndex , TNodeType . Projection , null , attrs || null ) ;
128138
129139 // We can't use viewData[HOST_NODE] because projection nodes can be nested in embedded views.
130- if ( tProjectionNode . projection === null ) tProjectionNode . projection = selectorIndex ;
140+ if ( tProjectionNode . projection === null ) {
141+ tProjectionNode . projection = selectorIndex ;
142+ }
131143
132- // `<ng-content>` has no content
144+ // `<ng-content>` has no content. Even if there's fallback
145+ // content, the fallback is shown next to it.
133146 setCurrentTNodeAsNotParent ( ) ;
134147
135148 const hydrationInfo = lView [ HYDRATION ] ;
136149 const isNodeCreationMode = ! hydrationInfo || isInSkipHydrationBlock ( ) ;
137- if ( isNodeCreationMode &&
150+ const componentNode = lView [ DECLARATION_COMPONENT_VIEW ] [ T_HOST ] as TElementNode ;
151+ const isEmpty = componentNode . projection ! [ tProjectionNode . projection ] === null ;
152+
153+ if ( isEmpty && fallbackTemplateFn ) {
154+ insertFallbackContent (
155+ lView , tView , nodeIndex , fallbackTemplateFn , fallbackDecls ! , fallbackVars ! ) ;
156+ } else if (
157+ isNodeCreationMode &&
138158 ( tProjectionNode . flags & TNodeFlags . isDetached ) !== TNodeFlags . isDetached ) {
139159 // re-distribution of projectable nodes is stored on a component's view level
140160 applyProjection ( tView , lView , tProjectionNode ) ;
141161 }
142162}
163+
164+ /** Inserts the fallback content of a projection slot. Assumes there's no projected content. */
165+ function insertFallbackContent (
166+ lView : LView , tView : TView , projectionIndex : number , templateFn : ComponentTemplate < unknown > ,
167+ decls : number , vars : number ) {
168+ const fallbackIndex = projectionIndex + 1 ;
169+ ɵɵtemplate ( fallbackIndex , templateFn , decls , vars ) ;
170+ const fallbackLContainer = lView [ HEADER_OFFSET + fallbackIndex ] ;
171+ ngDevMode && assertLContainer ( fallbackLContainer ) ;
172+ const fallbackTNode = getTNode ( tView , HEADER_OFFSET + fallbackIndex ) ;
173+ ngDevMode && assertTNode ( fallbackTNode ) ;
174+
175+ const dehydratedView = findMatchingDehydratedView ( fallbackLContainer , fallbackTNode . tView ! . ssrId ) ;
176+ const fallbackLView =
177+ createAndRenderEmbeddedLView ( lView , fallbackTNode , undefined , { dehydratedView} ) ;
178+ addLViewToLContainer (
179+ fallbackLContainer , fallbackLView , 0 , shouldAddViewToDom ( fallbackTNode , dehydratedView ) ) ;
180+ }
0 commit comments