Skip to content

Commit e8811a3

Browse files
committed
fix(core): introduce logical-only containers for foreign content
This commit introduces a logical-only container flag (`LContainerFlags.LogicalOnly`) to support Angular features (like change detection and queries) on projected content within foreign components, while relinquishing control over their placement in the DOM. When content is projected into a foreign component via `ɵɵforeignContent`, the foreign component receives the native DOM nodes directly and assumes control over their DOM placement. Therefore, Angular must skip all platform-level view operations (insert, move, delete) on these projected views. To achieve this: 1. Introduce Logical-Only Containers: - Added `LContainerFlags.LogicalOnly` to represent view containers whose nodes are managed logically (by the consuming foreign component) rather than by the renderer. - Flagged `ɵɵforeignContent` containers with the `LogicalOnly` annotation. - Updated `applyContainer` in `node_manipulation.ts` to return early and skip platform DOM manipulations (insert, detach, destroy) on containers marked as logical-only. 2. Guard `collectNativeNodes`: - Updated `collectNativeNodes` in `collect_native_nodes.ts` to skip descending into logical-only containers. This prevents nested projected child elements (which are already claimed and placed inside nested foreign components) from being re-collected at the parent component's projection root level. 3. Unit and Acceptance Tests: - Added a comprehensive set of categorized acceptance tests in `foreign_component_spec.ts` covering nested foreign projections, projecting foreign components into Angular components, Signal-based view queries (`viewChildren`), event handlers, and change detection.
1 parent 13b95c1 commit e8811a3

6 files changed

Lines changed: 468 additions & 10 deletions

File tree

packages/core/src/render3/collect_native_nodes.ts

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,11 +8,19 @@
88

99
import {assertParentView} from './assert';
1010
import {icuContainerIterate} from './i18n/i18n_tree_shaking';
11-
import {CONTAINER_HEADER_OFFSET, LContainer, NATIVE} from './interfaces/container';
11+
import {CONTAINER_HEADER_OFFSET, LContainer, LContainerFlags, NATIVE} from './interfaces/container';
1212
import {TIcuContainerNode, TNode, TNodeType} from './interfaces/node';
1313
import {RNode} from './interfaces/renderer_dom';
1414
import {isLContainer} from './interfaces/type_checks';
15-
import {DECLARATION_COMPONENT_VIEW, HOST, LView, TVIEW, TView, TViewType} from './interfaces/view';
15+
import {
16+
DECLARATION_COMPONENT_VIEW,
17+
FLAGS,
18+
HOST,
19+
LView,
20+
TVIEW,
21+
TView,
22+
TViewType,
23+
} from './interfaces/view';
1624
import {assertTNodeType} from './node_assert';
1725
import {getProjectionNodes} from './node_manipulation';
1826
import {getLViewParent, unwrapRNode} from './util/view_utils';
@@ -60,7 +68,7 @@ export function collectNativeNodes(
6068
// A given lNode can represent either a native node or a LContainer (when it is a host of a
6169
// ViewContainerRef). When we find a LContainer we need to descend into it to collect root nodes
6270
// from the views in this container.
63-
if (isLContainer(lNode)) {
71+
if (isLContainer(lNode) && !(lNode[FLAGS] & LContainerFlags.LogicalOnly)) {
6472
collectNativeNodesInLContainer(lNode, result);
6573
}
6674

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

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import {ForeignComponent, RENDER} from '../../interface/foreign_component';
1010
import {attachPatchData} from '../context_discovery';
1111
import {createForeignView} from '../foreign_view';
1212
import {TContainerNode, TNodeType} from '../interfaces/node';
13-
import {HEADER_OFFSET, RENDERER, TVIEW} from '../interfaces/view';
13+
import {HEADER_OFFSET, RENDERER, TVIEW, FLAGS} from '../interfaces/view';
1414
import {appendChild} from '../node_manipulation';
1515
import {nativeInsertBefore} from '../dom_node_manipulation';
1616
import {getLView, getTView, setCurrentTNode, setCurrentTNodeAsNotParent} from '../state';
@@ -20,11 +20,11 @@ import {createLContainer, addLViewToLContainer} from '../view/container';
2020
import {NodeInjector} from '../di';
2121
import {runInInjectionContext} from '../../di';
2222
import {Renderer} from '../interfaces/renderer';
23-
import {RNode} from '../interfaces/renderer_dom';
23+
import {RElement, RNode} from '../interfaces/renderer_dom';
2424
import {createAndRenderEmbeddedLView} from '../view_manipulation';
2525
import {collectNativeNodes} from '../collect_native_nodes';
2626
import {assertLContainer} from '../assert';
27-
import {LContainer} from '../interfaces/container';
27+
import {LContainer, LContainerFlags} from '../interfaces/container';
2828

2929
/**
3030
* Creation phase instruction to render a foreign component.
@@ -77,7 +77,7 @@ export function ɵɵforeignComponent(
7777
const parent = tail.parentNode;
7878
if (parent) {
7979
for (let i = 0; i < nodes.length; i++) {
80-
nativeInsertBefore(renderer, parent, nodes[i], tail, false);
80+
nativeInsertBefore(renderer, parent, nodes[i], tail, true);
8181
}
8282
}
8383

@@ -101,12 +101,13 @@ export function ɵɵforeignContent(index: number): any[] {
101101
// The template is already declared at adjustedIndex, so lContainer must exist.
102102
const lContainer = lView[adjustedIndex] as LContainer;
103103
ngDevMode && assertLContainer(lContainer);
104+
lContainer[FLAGS] |= LContainerFlags.LogicalOnly;
104105

105106
const tView = getTView();
106107
const tNode = tView.data[adjustedIndex] as TContainerNode;
107108

108-
// Instantiate and render the embedded view inside the container,
109-
// but do NOT add its elements to the DOM at the container anchor.
109+
// Instantiate and render the embedded view inside the container, but do not add its elements to
110+
// the DOM at the container anchor since the nodes will be projected into a foreign view.
110111
const embeddedLView = createAndRenderEmbeddedLView(lView, tNode, null);
111112
addLViewToLContainer(lContainer, embeddedLView, 0, /* addToDOM */ false);
112113

packages/core/src/render3/interfaces/container.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -127,4 +127,10 @@ export const enum LContainerFlags {
127127
* This flag, once set, is never unset for the `LContainer`.
128128
*/
129129
HasTransplantedViews = 1 << 1,
130+
131+
/**
132+
* Flag to signify that this `LContainer` is logical-only and its views should not be added
133+
* to or removed from the rendering tree by the platform renderer.
134+
*/
135+
LogicalOnly = 1 << 2,
130136
}

packages/core/src/render3/node_manipulation.ts

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,13 @@ import {
3535
nativeRemoveNode,
3636
} from './dom_node_manipulation';
3737
import {icuContainerIterate} from './i18n/i18n_tree_shaking';
38-
import {CONTAINER_HEADER_OFFSET, LContainer, MOVED_VIEWS, NATIVE} from './interfaces/container';
38+
import {
39+
CONTAINER_HEADER_OFFSET,
40+
LContainer,
41+
LContainerFlags,
42+
MOVED_VIEWS,
43+
NATIVE,
44+
} from './interfaces/container';
3945
import {ComponentDef} from './interfaces/definition';
4046
import {NodeInjectorFactory} from './interfaces/injector';
4147
import {unregisterLView} from './interfaces/lview_tracking';
@@ -1082,6 +1088,9 @@ function applyContainer(
10821088
beforeNode,
10831089
);
10841090
}
1091+
if ((lContainer[FLAGS] & LContainerFlags.LogicalOnly) !== 0) {
1092+
return;
1093+
}
10851094
for (let i = CONTAINER_HEADER_OFFSET; i < lContainer.length; i++) {
10861095
const lView = lContainer[i] as LView;
10871096
applyView(lView[TVIEW], lView, renderer, action, parentRElement, anchor);
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
load("//tools:defaults.bzl", "angular_jasmine_test", "ng_project", "ng_web_test_suite")
2+
3+
package(default_visibility = ["//visibility:private"])
4+
5+
ng_project(
6+
name = "foreign_component_test_lib",
7+
testonly = True,
8+
srcs = glob(["**/*.ts"]),
9+
visibility = ["//:__pkg__"],
10+
deps = [
11+
"//packages/core",
12+
"//packages/core/testing",
13+
],
14+
)
15+
16+
angular_jasmine_test(
17+
name = "foreign_component",
18+
data = [
19+
":foreign_component_test_lib",
20+
"//:node_modules/source-map",
21+
],
22+
)
23+
24+
ng_web_test_suite(
25+
name = "foreign_component_web",
26+
deps = [
27+
":foreign_component_test_lib",
28+
],
29+
)

0 commit comments

Comments
 (0)