Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ export class TestCmpChildren {
template: function TestCmpChildren_Template(rf, ctx) {
if (rf & 1) {
i0.ɵɵdomTemplate(0, TestCmpChildren_Icon_0_Template, 2, 0)(1, TestCmpChildren_Description_1_Template, 2, 0)(2, TestCmpChildren_Children_2_Template, 2, 0);
i0.ɵɵforeignComponent(3, 0, { label: ctx.title, icon: i0.ɵɵforeignContent(0), description: i0.ɵɵforeignContent(1), children: i0.ɵɵforeignContent(2) });
i0.ɵɵforeignComponent(3, 0, { label: ctx.title, icon: i0.ɵɵforeignContent(0, 0), description: i0.ɵɵforeignContent(1, 0), children: i0.ɵɵforeignContent(2, 0) });
}
},
encapsulation: 2
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ export class TestCmpChildren {
template: function TestCmpChildren_Template(rf, ctx) {
if (rf & 1) {
i0.ɵɵtemplate(0, TestCmpChildren_Icon_0_Template, 2, 0)(1, TestCmpChildren_Description_1_Template, 2, 0)(2, TestCmpChildren_Children_2_Template, 2, 0);
i0.ɵɵforeignComponent(3, 0, { label: ctx.title, icon: i0.ɵɵforeignContent(0), description: i0.ɵɵforeignContent(1), children: i0.ɵɵforeignContent(2) });
i0.ɵɵforeignComponent(3, 0, { label: ctx.title, icon: i0.ɵɵforeignContent(0, 0), description: i0.ɵɵforeignContent(1, 0), children: i0.ɵɵforeignContent(2, 0) });
}
},
encapsulation: 2
Expand Down
13 changes: 8 additions & 5 deletions packages/compiler/src/template/pipeline/src/phases/reify.ts
Original file line number Diff line number Diff line change
Expand Up @@ -803,13 +803,16 @@ function reifyIrExpression(unit: CompilationUnit, expr: o.Expression): o.Express
if (!(unit instanceof ViewCompilationUnit)) {
throw new Error(`AssertionError: must be compiling a component`);
}
// Check whether the projected content view declares context variables (`contextVariables.size > 0`).
// If context variables are present, the content is dynamic and expects arguments passed at runtime,
// dictating that the compiler emit `foreignContentFn` (which wraps the template in a function).
// Conversely, if no context variables are declared (`contextVariables.size === 0`), the content is
// static and the compiler emits `foreignContent` directly.
const isFn = unit.job.views.get(expr.childrenViewXref)!.contextVariables.size > 0;
const slot = o.literal(expr.childrenViewHandle.slot!);
return isFn
? o
.importExpr(Identifiers.foreignContentFn)
.callFn([slot, o.literal(expr.foreignComponentConstIndex)])
: o.importExpr(Identifiers.foreignContent).callFn([slot]);
return o
.importExpr(isFn ? Identifiers.foreignContentFn : Identifiers.foreignContent)
.callFn([slot, o.literal(expr.foreignComponentConstIndex)]);
case ir.ExpressionKind.LexicalRead:
throw new Error(`AssertionError: unresolved LexicalRead of ${expr.name}`);
case ir.ExpressionKind.TwoWayBindingSet:
Expand Down
1 change: 1 addition & 0 deletions packages/core/src/core.ts
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,7 @@ export {
outputBinding,
twoWayBinding,
} from './render3/dynamic_bindings';
// g3-only export {provideForeignRootContext} from './render3/foreign_context';
// g3-only export {foreignImport} from './render3/foreign_import';
export {createEnvironmentInjector, createNgModule} from './render3/ng_module_ref';
export {publishNonCoreGlobalUtil as ɵpublishNonCoreGlobalUtil} from './render3/util/global_utils';
Expand Down
24 changes: 20 additions & 4 deletions packages/core/src/interface/foreign_component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,16 +12,30 @@ export const RENDER: unique symbol = Symbol('RENDER');
/** Symbol used to store and retrieve the disposal registration function for a foreign component. */
export const ON_DESTROY: unique symbol = Symbol('ON_DESTROY');

/** Symbol used to store and retrieve the context retrieval function for a foreign component. */
export const GET_CONTEXT: unique symbol = Symbol('GET_CONTEXT');

/**
* A function used to render a foreign component in an Angular template.
*
* The function accepts the component's properties as its only argument. It should return an array
* The function accepts the component's properties and optional context. It should return an array
* of nodes rendered and owned by the foreign component. It may also return a callback to perform
* any necessary cleanup when the component is destroyed.
*
* @template TProps The properties of the foreign component.
* @template TContext The context passed to the foreign component.
*/
export type ForeignRenderFn<TProps, TContext> = (
props: TProps,
context?: TContext,
) => [Node[], VoidFunction?];

/**
* A function that captures the runtime context of a foreign component.
*
* @template TContext The captured context type.
*/
export type ForeignRenderFn<TProps> = (props: TProps) => [Node[], VoidFunction?];
export type ForeignGetContextFn<TContext> = () => TContext;

/**
* A function that allows a foreign component to register a destroy callback.
Expand All @@ -37,8 +51,10 @@ export type ForeignOnDestroyFn = (destroy: VoidFunction) => void;
* Represents a component from another framework that Angular can import and render.
*
* @template TProps The properties of the foreign component.
* @template TContext The context passed to the foreign component.
*/
export interface ForeignComponent<TProps> {
readonly [RENDER]: ForeignRenderFn<TProps>;
export interface ForeignComponent<TProps = {}, TContext = unknown> {
readonly [RENDER]: ForeignRenderFn<TProps, TContext>;
readonly [ON_DESTROY]: ForeignOnDestroyFn;
readonly [GET_CONTEXT]?: ForeignGetContextFn<TContext>;
}
2 changes: 1 addition & 1 deletion packages/core/src/metadata/directives.ts
Original file line number Diff line number Diff line change
Expand Up @@ -648,7 +648,7 @@ export interface Component extends Directive {
*
* @internal // 3p-only
*/
foreignImports?: ForeignComponent<any>[];
foreignImports?: ForeignComponent<any, any>[];

/**
* The `deferredImports` property specifies a standalone component's template dependencies,
Expand Down
29 changes: 29 additions & 0 deletions packages/core/src/render3/foreign_context.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
/**
* @license
* Copyright Google LLC All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.dev/license
*/

import {InjectionToken} from '../di/injection_token';
import {Provider} from '../di/interface/provider';

/**
* Internal token used to resolve foreign framework context providers.
*/
export const FOREIGN_CONTEXT = new InjectionToken<unknown>('FOREIGN_CONTEXT');

/**
* Configures an opaque root context for a foreign framework within Angular's dependency injection
* hierarchy.
*
* @param contextFactory The factory function that creates the root context object.
* @template TContext The foreign context type.
*/
export function provideForeignRootContext<TContext>(contextFactory: () => TContext): Provider {
return {
provide: FOREIGN_CONTEXT,
useFactory: contextFactory,
};
}
14 changes: 10 additions & 4 deletions packages/core/src/render3/foreign_import.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,23 +10,29 @@ import {
ForeignComponent,
ForeignRenderFn,
ForeignOnDestroyFn,
ForeignGetContextFn,
RENDER,
ON_DESTROY,
GET_CONTEXT,
} from '../interface/foreign_component';

/**
* Returns a {@link ForeignComponent} for use in Angular components.
*
* @template TProps The properties of the foreign component.
* @param render A function that renders a foreign component.
* @param onDestroy A function for foreign content to register a destroy callback.
* @param getContext An optional function that captures runtime context.
* @template TProps The properties of the foreign component.
* @template TContext The context passed to the foreign component.
*/
export function foreignImport<TProps>(
render: ForeignRenderFn<TProps>,
export function foreignImport<TProps, TContext = unknown>(
render: ForeignRenderFn<TProps, TContext>,
onDestroy: ForeignOnDestroyFn,
): ForeignComponent<TProps> {
getContext?: ForeignGetContextFn<TContext>,
): ForeignComponent<TProps, TContext> {
return {
[RENDER]: render,
[ON_DESTROY]: onDestroy,
[GET_CONTEXT]: getContext,
};
}
123 changes: 84 additions & 39 deletions packages/core/src/render3/instructions/foreign_component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,28 +6,30 @@
* found in the LICENSE file at https://angular.dev/license
*/

import {ForeignComponent, RENDER, ON_DESTROY} from '../../interface/foreign_component';
import {Injector} from '../../di/injector';
import {InternalInjectFlags} from '../../di/interface/injector';
import {ForeignComponent, GET_CONTEXT, ON_DESTROY, RENDER} from '../../interface/foreign_component';
import {assertDefined, assertNotSame} from '../../util/assert';
import {assertLContainer} from '../assert';
import {collectNativeNodes} from '../collect_native_nodes';
import {attachPatchData} from '../context_discovery';
import {getOrCreateInjectable} from '../di';
import {nativeInsertBefore} from '../dom_node_manipulation';
import {FOREIGN_CONTEXT} from '../foreign_context';
import {createForeignView} from '../foreign_view';
import {CONTAINER_HEADER_OFFSET, LContainer, LContainerFlags} from '../interfaces/container';
import {TContainerNode, TNodeType} from '../interfaces/node';
import {HEADER_OFFSET, RENDERER, TVIEW, FLAGS, LViewFlags} from '../interfaces/view';
import {appendChild, destroyLView} from '../node_manipulation';
import {Renderer} from '../interfaces/renderer';
import {RNode} from '../interfaces/renderer_dom';
import {isDestroyed} from '../interfaces/type_checks';
import {FLAGS, HEADER_OFFSET, LView, RENDERER, TVIEW} from '../interfaces/view';
import {appendChild} from '../node_manipulation';
import {getLView, getTView, setCurrentTNode, setCurrentTNodeAsNotParent} from '../state';
import {getOrCreateTNode} from '../tnode_manipulation';
import {getConstant} from '../util/view_utils';
import {addToEndOfViewTree} from '../view/construction';
import {createLContainer, addLViewToLContainer, removeLViewFromLContainer} from '../view/container';
import {NodeInjector} from '../di';
import {runInInjectionContext} from '../../di';
import {Renderer} from '../interfaces/renderer';
import {RNode} from '../interfaces/renderer_dom';
import {addLViewToLContainer, createLContainer, removeLViewFromLContainer} from '../view/container';
import {createAndRenderEmbeddedLView} from '../view_manipulation';
import {collectNativeNodes} from '../collect_native_nodes';
import {assertLContainer} from '../assert';
import {CONTAINER_HEADER_OFFSET, LContainer, LContainerFlags} from '../interfaces/container';
import {getConstant} from '../util/view_utils';
import {isDestroyed} from '../interfaces/type_checks';
import {assertNotEqual, assertNotSame} from '../../util/assert';

/**
* Creation phase instruction to render a foreign component.
Expand All @@ -45,7 +47,10 @@ export function ɵɵforeignComponent(
const lView = getLView();
const tView = getTView();
const adjustedIndex = index + HEADER_OFFSET;
const foreignComponent = getConstant<ForeignComponent<any>>(tView.consts, foreignComponentIndex)!;
const foreignComponent = getConstant<ForeignComponent<any, any>>(
tView.consts,
foreignComponentIndex,
)!;

// 1. Get or create TNode for this container slot
let tNode: TContainerNode;
Expand All @@ -72,9 +77,16 @@ export function ɵɵforeignComponent(
// 4. Create the Foreign View and insert it at index 0 of the container
const viewRef = createForeignView(lContainer, 0);

// 5. Call the RENDER function to get the nodes and DisposeFn
const injector = new NodeInjector(tNode, lView);
const [nodes, dispose] = runInInjectionContext(injector, () => foreignComponent[RENDER](props));
// 5. Resolve context and call the RENDER function to get the nodes and DisposeFn
// Context is optional because foreign components may not require context or a FOREIGN_CONTEXT
// provider might not be configured in the component/element injector hierarchy.
const context = getOrCreateInjectable(
tNode,
lView,
FOREIGN_CONTEXT,
InternalInjectFlags.Optional,
);
const [nodes, dispose] = foreignComponent[RENDER](props, context ?? undefined);

// 6. Insert the returned nodes into the foreign view, between its head and tail comment anchors.
const tail = viewRef.tail as RNode;
Expand All @@ -92,13 +104,24 @@ export function ɵɵforeignComponent(
}

/**
* Creation phase instruction to render foreign content (children of a foreign component)
* and extract its root DOM nodes.
*
* @param index The index of the container in the data array.
* @codeGenApi
* Reusable injector class that intercepts requests for {@link FOREIGN_CONTEXT} during
* embedded view creation and returns the captured runtime context.
*/
class ForeignContextInjector implements Injector {
constructor(private context: unknown) {}

get(token: any, notFoundValue?: any): any {
return token === FOREIGN_CONTEXT ? this.context : notFoundValue;
}
}

/**
* Resolves container and foreign component metadata for foreign content projection instructions.
*/
export function ɵɵforeignContent(index: number): any[] {
function resolveForeignContentContainer(
index: number,
foreignComponentConstIndex: number,
): [LView, LContainer, TContainerNode, ForeignComponent<any, any>] {
const lView = getLView();
const adjustedIndex = index + HEADER_OFFSET;

Expand All @@ -109,10 +132,37 @@ export function ɵɵforeignContent(index: number): any[] {

const tView = getTView();
const tNode = tView.data[adjustedIndex] as TContainerNode;
const foreignComponent = getConstant<ForeignComponent<any, any>>(
tView.consts,
foreignComponentConstIndex,
)!;
ngDevMode &&
assertDefined(foreignComponent, 'Foreign component must be defined in constant pool.');

return [lView, lContainer, tNode, foreignComponent];
}

/**
* Creation phase instruction to render foreign content (children of a foreign component)
* and extract its root DOM nodes.
*
* @param index The index of the container in the data array.
* @param foreignComponentConstIndex The index of the matched foreign component in the constant pool.
* @codeGenApi
*/
export function ɵɵforeignContent(index: number, foreignComponentConstIndex: number): any[] {
const [lView, lContainer, tNode, foreignComponent] = resolveForeignContentContainer(
index,
foreignComponentConstIndex,
);
const getContext = foreignComponent[GET_CONTEXT];
const options = getContext
? {embeddedViewInjector: new ForeignContextInjector(getContext())}
: undefined;

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

// Extract and return the root nodes of the created view
Expand All @@ -132,26 +182,21 @@ export function ɵɵforeignContentFn(
index: number,
foreignComponentConstIndex: number,
): (...args: any[]) => any[] {
const lView = getLView();
const adjustedIndex = index + HEADER_OFFSET;

// The template is already declared at adjustedIndex, so lContainer must exist.
const lContainer = lView[adjustedIndex] as LContainer;
ngDevMode && assertLContainer(lContainer);
lContainer[FLAGS] |= LContainerFlags.LogicalOnly;

const tView = getTView();
const tNode = tView.data[adjustedIndex] as TContainerNode;
const foreignComponent = getConstant<ForeignComponent<any>>(
tView.consts,
const [lView, lContainer, tNode, foreignComponent] = resolveForeignContentContainer(
index,
foreignComponentConstIndex,
)!;
);
const onDestroy = foreignComponent[ON_DESTROY];
const getContext = foreignComponent[GET_CONTEXT];

return (...args: any[]) => {
const options = getContext
? {embeddedViewInjector: new ForeignContextInjector(getContext())}
: undefined;

// When the function is called, instantiate and render a new embedded view inside the container.
// The arguments are passed directly as the context of the view.
const embeddedLView = createAndRenderEmbeddedLView(lView, tNode, args);
const embeddedLView = createAndRenderEmbeddedLView(lView, tNode, args, options);

addLViewToLContainer(
lContainer,
Expand Down
2 changes: 1 addition & 1 deletion packages/core/src/render3/interfaces/node.ts
Original file line number Diff line number Diff line change
Expand Up @@ -229,7 +229,7 @@ export type TAttributes = (string | AttributeMarker | CssSelector)[];
* - Translated messages (i18n).
* - Foreign components.
*/
export type TConstants = (TAttributes | string | ForeignComponent<any>)[];
export type TConstants = (TAttributes | string | ForeignComponent<any, any>)[];

/**
* Factory function that returns an array of consts. Consts can be represented as a function in
Expand Down
Loading
Loading