Add support for projecting content into foreign component properties#68996
Add support for projecting content into foreign component properties#68996atscott merged 7 commits intoangular:mainangular/angular:mainfrom leonsenft:foreign-component-childrenleonsenft/angular:foreign-component-childrenCopy head branch name to clipboard
Conversation
e084e9b to
6955467
Compare
Previously, any children nested inside a foreign component were ignored during template ingestion. With this change, the compiler now: 1. Identifies when a foreign component has children in the template AST. 2. Compiles these children into a separate template view (using the standard TemplateOp). 3. Passes a `ɵɵforeignContent` expression under the `children` prop inside the foreign component's `props` object. At runtime, the new `ɵɵforeignContent(index)` instruction instantiates the template at the specified slot index in memory (detached from the DOM), extracts its root DOM nodes, and returns them. These root nodes are then passed directly to the foreign component's `props.children` so they can be rendered by the foreign framework. The instantiated children view is registered in the parent LView's child tree, ensuring its change detection and destruction are managed automatically as part of the standard Angular view tree lifecycle.
6955467 to
146e109
Compare
|
@alxhub I addressed your comment from #69104 (comment) in this PR. |
f634490 to
ef8a9cd
Compare
…nent props Add `@content(propName)` blocks for passing template content to foreign component properties by name. Previously, only a single set of direct children could be passed to a foreign component via the default `children` property. With this change, developers can project distinct template content to multiple specific properties on the foreign component: ```html <FancyButton [label]="title"> @content(icon) { <span>Icon</span> } @content(description) { <span>Description text</span> } <span>Other children</span> </FancyButton> ``` Specifically: - Add support to the HTML lexer for `@content` blocks. - Introduce `ContentBlock` AST node to represent `@content` blocks. - Implement validation ensuring `@content` blocks have exactly one parameter representing a valid JS identifier. - Throw an error during ingestion if a `@content` block is placed anywhere other than as a direct child of a foreign component. - Map `@content` blocks to properties of the props object passed to `ɵɵforeignComponent`. - Update compliance and unit tests to cover these changes. ```
ef8a9cd to
2f221e3
Compare
|
|
||
| // 6. Insert the returned nodes into the foreign view, between its head and tail comment anchors. | ||
| const tail = viewRef.tail as RNode; | ||
| const parent = tail.parentNode; |
There was a problem hiding this comment.
This is related to the previous change, but Gemini noticed that this should be renderer.parentNode(tail)
There was a problem hiding this comment.
I think this is fine, since it's directly defined by RNode and documented that it can be used at runtime:
angular/packages/core/src/render3/interfaces/renderer_dom.ts
Lines 16 to 25 in af04e26
0a08871 to
3e99940
Compare
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.
…alysis Refactors the unsupported bindings validation for foreign components from the template semantics checker phase (during type-checking) to the component analysis phase. Surfacing this check during component analysis means it will be correctly reported during local compilation (which skips full template type-checking). Specifically: - Creates a new helper `analyzeForeignComponentFeatures` in `foreign_component.ts` that traverses template elements and checks for unsupported outputs, references, and non-property inputs on foreign components. - Removes the legacy validation from `template_semantics_checker.ts`. - Invokes the validation during component analysis in `ComponentDecoratorHandler.analyze()`.
Adds validation to verify that `@content` blocks are only used as direct children of foreign components. Specifically: - Defines a new compile diagnostic code `INVALID_CONTENT_PLACEMENT = 8026`. - Updates `ForeignComponentFeatureAnalyzer` to traverse content blocks and report `INVALID_CONTENT_PLACEMENT` diagnostics if they are placed incorrectly. - Removes the raw error thrown during ingestion in `packages/compiler/src/template/pipeline/src/ingest.ts`. - Adds integration tests in `template_typecheck_spec.ts`.
…plicit children Defining a `@content (children)` block explicitly is unnecessary because children should always be passed implicitly as direct nested content of the foreign component. Using an explicit block could also lead to conflicts and silent template rendering issues where implicit content (like whitespace) accidentally overwrote the explicit block in the compiler's template representation. This change introduces a compilation error (`FOREIGN_COMPONENT_CONTENT_UNNECESSARY_FOR_CHILDREN`) when an explicit `@content (children)` block is detected, guiding developers to pass children implicitly instead.
Ensures `@content` blocks on foreign components have unique names and do not conflict with static attributes or input property bindings. Specifically, this commit introduces two new template diagnostics: 1. `CONFLICTING_CONTENT_DECLARATION` (8028): Raised when multiple `@content` blocks with the same name are defined under the same foreign component. 2. `CONFLICTING_CONTENT_AND_PROPERTY` (8029): Raised when a `@content` block's name matches an attribute or input property binding on the parent foreign component. Both diagnostics include related information pointing to the location of the conflicting declaration or property.
3e99940 to
8821d3b
Compare
| this.currentParent = prevParent; | ||
| } | ||
|
|
||
| private validateForeignComponent(element: TmplAstElement): void { |
There was a problem hiding this comment.
✨ AOT-only Validation Discrepancy
Note: This is not a blocker for this PR and can be addressed in a follow-up.
Currently, the validation logic (duplicate content blocks, conflicts with property bindings) is implemented inside the compiler-cli AOT analysis step. In JIT compilation mode, these checks will not be executed. This means JIT users will experience silent runtime misbehavior (e.g. duplicate content blocks overwriting each other in the properties map) instead of clear compilation errors.
In other parts of Angular, JIT compilation performs parallel checks to approximate AOT validation errors. For example:
- Standalone schemas checks in JIT
directive.ts: directive.ts#L207-L211 - Decorator validations in JIT
directive.ts: directive.ts#L463-L465 - Module validations in JIT
module.ts: module.ts#L138-L142
Consider adding a lightweight validation or JIT assert check (e.g. during runtime properties mapping or block instantiation) in a future PR to align JIT and AOT behaviors.
There was a problem hiding this comment.
Fortunately this feature doesn't support JIT 😄
There was a problem hiding this comment.
Ah sweet, I incorrectly assumed that the instruction presence in
angular/packages/core/src/render3/jit/environment.ts
Lines 58 to 59 in 79e5d5d
There was a problem hiding this comment.
Yeah that's a good point. Earlier during the implementation of this feature there was an explicit error throw if you tried importing foreign components in JIT. Due to refactor how we collected those, we ended up removing that error case... I actually realize now I have no idea what would happen if you try to use it in JIT mode. I'll investigate later.
|
This PR was merged into the repository. The changes were merged into the following branches:
|
| text: 'Child nodes are defined here.', | ||
| start: firstChild.sourceSpan.start.offset, | ||
| end: firstChild.sourceSpan.end.offset, | ||
| sourceFile: this.sourceMapping.node.getSourceFile(), |
There was a problem hiding this comment.
AGENT: For external templates (using templateUrl), the main diagnostic is reported on the HTML template file. However, secondary "related messages" in foreign_component.ts are constructed passing the component's TypeScript file:
sourceFile: this.sourceMapping.node.getSourceFile()
But they use character offsets from the HTML template file (e.g., firstChild.sourceSpan.start.offset). This mismatch causes IDEs and CLI outputs to map HTML template offsets onto the TypeScript file, resulting in corrupted warning/error locations (pointing to incorrect or out-of-bound characters in the .ts file).
- Modify
makeTemplateDiagnostic's signature indiagnostic.tsto make thesourceFileproperty inrelatedMessagesoptional. - In
makeTemplateDiagnostic's implementation, defaultrelatedMessage.sourceFileto the template's source file(sf ?? mapping.node.getSourceFile())when it is not provided. - Remove the explicit
sourceFile: this.sourceMapping.node.getSourceFile()mapping from all related messages inforeign_component.tsto allow them to fall back to the template file.

Previously, any children nested inside a foreign component were ignored during template ingestion. With this change, the compiler now:
@contentblocks inside the foreign component body. All other nodes are implicitly assigned to thechildrencontent block.ɵɵforeignContentexpression for each content block to its corresponding property in the foreign component'spropsargument.See individual commit messages for more detailed descriptions.