Skip to content

Commit a600a39

Browse files
crisbetodylhunn
authored andcommitted
feat(core): add support for fallback content in ng-content (#54854)
Adds the ability to specify content that Angular should fall back to if nothing is projected into an `ng-content` slot. For example, if we have the following setup ``` @component({ selector: 'my-comp', template: ` <ng-content select="header">Default header</ng-content> <ng-content select="footer">Default footer</ng-content> ` }) class MyComp {} @component({ template: ` <my-comp> <footer>New footer</footer> </my-comp> ` }) class MyApp {} ``` The instance of `my-comp` in the app will have the default header and the new footer. **Note:** Angular's content projection happens during creation time. This means that dynamically changing the contents of the slot will not cause the default content to show up, e.g. if a `if` block goes from `true` to `false`. Fixes #12530. PR Close #54854
1 parent cf8fb33 commit a600a39

3 files changed

Lines changed: 547 additions & 11 deletions

File tree

packages/core/src/render3/instructions/projection.ts

Lines changed: 46 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -5,16 +5,21 @@
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';
89
import {newArray} from '../../util/array_utils';
10+
import {assertLContainer, assertTNode} from '../assert';
911
import {ComponentTemplate} from '../interfaces/definition';
1012
import {TAttributes, TElementNode, TNode, TNodeFlags, TNodeType} from '../interfaces/node';
1113
import {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';
1315
import {applyProjection} from '../node_manipulation';
1416
import {getProjectAsAttrValue, isNodeMatchingSelectorList, isSelectorInSelectorList} from '../node_selector_matcher';
1517
import {getLView, getTView, isInSkipHydrationBlock, setCurrentTNodeAsNotParent} from '../state';
18+
import {getTNode} from '../util/view_utils';
19+
import {addLViewToLContainer, createAndRenderEmbeddedLView, shouldAddViewToDom} from '../view_manipulation';
1620

1721
import {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

Comments
 (0)