diff --git a/packages/compiler-cli/test/compliance/test_cases/r3_compiler_compliance/components_and_directives/standalone/foreign_component.js b/packages/compiler-cli/test/compliance/test_cases/r3_compiler_compliance/components_and_directives/standalone/foreign_component.js index 664f97f63539..0950f74c63c9 100644 --- a/packages/compiler-cli/test/compliance/test_cases/r3_compiler_compliance/components_and_directives/standalone/foreign_component.js +++ b/packages/compiler-cli/test/compliance/test_cases/r3_compiler_compliance/components_and_directives/standalone/foreign_component.js @@ -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 diff --git a/packages/compiler-cli/test/compliance/test_cases/r3_compiler_compliance/components_and_directives/standalone/foreign_component.local.js b/packages/compiler-cli/test/compliance/test_cases/r3_compiler_compliance/components_and_directives/standalone/foreign_component.local.js index 9c3f8e8c4952..94d78e024124 100644 --- a/packages/compiler-cli/test/compliance/test_cases/r3_compiler_compliance/components_and_directives/standalone/foreign_component.local.js +++ b/packages/compiler-cli/test/compliance/test_cases/r3_compiler_compliance/components_and_directives/standalone/foreign_component.local.js @@ -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 diff --git a/packages/compiler/src/template/pipeline/src/phases/reify.ts b/packages/compiler/src/template/pipeline/src/phases/reify.ts index 2f340985cb30..197f52fdb7c3 100644 --- a/packages/compiler/src/template/pipeline/src/phases/reify.ts +++ b/packages/compiler/src/template/pipeline/src/phases/reify.ts @@ -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: diff --git a/packages/core/src/core.ts b/packages/core/src/core.ts index 00fd83f14849..f9d11a71af8a 100644 --- a/packages/core/src/core.ts +++ b/packages/core/src/core.ts @@ -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'; diff --git a/packages/core/src/interface/foreign_component.ts b/packages/core/src/interface/foreign_component.ts index 83d083e7773b..d52cbbb48641 100644 --- a/packages/core/src/interface/foreign_component.ts +++ b/packages/core/src/interface/foreign_component.ts @@ -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 = ( + 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 = (props: TProps) => [Node[], VoidFunction?]; +export type ForeignGetContextFn = () => TContext; /** * A function that allows a foreign component to register a destroy callback. @@ -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 { - readonly [RENDER]: ForeignRenderFn; +export interface ForeignComponent { + readonly [RENDER]: ForeignRenderFn; readonly [ON_DESTROY]: ForeignOnDestroyFn; + readonly [GET_CONTEXT]?: ForeignGetContextFn; } diff --git a/packages/core/src/metadata/directives.ts b/packages/core/src/metadata/directives.ts index 681337355c2b..ab1f8cd50b97 100644 --- a/packages/core/src/metadata/directives.ts +++ b/packages/core/src/metadata/directives.ts @@ -648,7 +648,7 @@ export interface Component extends Directive { * * @internal // 3p-only */ - foreignImports?: ForeignComponent[]; + foreignImports?: ForeignComponent[]; /** * The `deferredImports` property specifies a standalone component's template dependencies, diff --git a/packages/core/src/render3/foreign_context.ts b/packages/core/src/render3/foreign_context.ts new file mode 100644 index 000000000000..0dc09860c97d --- /dev/null +++ b/packages/core/src/render3/foreign_context.ts @@ -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('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(contextFactory: () => TContext): Provider { + return { + provide: FOREIGN_CONTEXT, + useFactory: contextFactory, + }; +} diff --git a/packages/core/src/render3/foreign_import.ts b/packages/core/src/render3/foreign_import.ts index 08e6cc9e4f2a..78069baa9ad4 100644 --- a/packages/core/src/render3/foreign_import.ts +++ b/packages/core/src/render3/foreign_import.ts @@ -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( - render: ForeignRenderFn, +export function foreignImport( + render: ForeignRenderFn, onDestroy: ForeignOnDestroyFn, -): ForeignComponent { + getContext?: ForeignGetContextFn, +): ForeignComponent { return { [RENDER]: render, [ON_DESTROY]: onDestroy, + [GET_CONTEXT]: getContext, }; } diff --git a/packages/core/src/render3/instructions/foreign_component.ts b/packages/core/src/render3/instructions/foreign_component.ts index 8d9dc8bf0785..970ee3df5fb4 100644 --- a/packages/core/src/render3/instructions/foreign_component.ts +++ b/packages/core/src/render3/instructions/foreign_component.ts @@ -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. @@ -45,7 +47,10 @@ export function ɵɵforeignComponent( const lView = getLView(); const tView = getTView(); const adjustedIndex = index + HEADER_OFFSET; - const foreignComponent = getConstant>(tView.consts, foreignComponentIndex)!; + const foreignComponent = getConstant>( + tView.consts, + foreignComponentIndex, + )!; // 1. Get or create TNode for this container slot let tNode: TContainerNode; @@ -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; @@ -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] { const lView = getLView(); const adjustedIndex = index + HEADER_OFFSET; @@ -109,10 +132,37 @@ export function ɵɵforeignContent(index: number): any[] { const tView = getTView(); const tNode = tView.data[adjustedIndex] as TContainerNode; + const foreignComponent = getConstant>( + 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 @@ -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>( - 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, diff --git a/packages/core/src/render3/interfaces/node.ts b/packages/core/src/render3/interfaces/node.ts index 318950f7c7ea..d6cca96f020b 100644 --- a/packages/core/src/render3/interfaces/node.ts +++ b/packages/core/src/render3/interfaces/node.ts @@ -229,7 +229,7 @@ export type TAttributes = (string | AttributeMarker | CssSelector)[]; * - Translated messages (i18n). * - Foreign components. */ -export type TConstants = (TAttributes | string | ForeignComponent)[]; +export type TConstants = (TAttributes | string | ForeignComponent)[]; /** * Factory function that returns an array of consts. Consts can be represented as a function in diff --git a/packages/core/test/acceptance/foreign_component/foreign_component_context_spec.ts b/packages/core/test/acceptance/foreign_component/foreign_component_context_spec.ts new file mode 100644 index 000000000000..a651b577874e --- /dev/null +++ b/packages/core/test/acceptance/foreign_component/foreign_component_context_spec.ts @@ -0,0 +1,164 @@ +/** + * @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 {Component, inject} from '@angular/core'; +import {TestBed} from '@angular/core/testing'; +import {ForeignComponent} from '../../../src/interface/foreign_component'; +import {foreignImport} from '../../../src/render3/foreign_import'; +import {FOREIGN_CONTEXT, provideForeignRootContext} from '../../../src/render3/foreign_context'; + +/** A minimal framework for testing purposes. */ +const microFramework = { + context: undefined as string | undefined, + + pushContext(context: string | undefined): string | undefined { + const previousContext = microFramework.context; + microFramework.context = context; + return previousContext; + }, + + popContext(context: string | undefined) { + microFramework.context = context; + }, +}; + +function microImport( + component: (props: TProps) => Node[], +): ForeignComponent { + return foreignImport( + (props, context) => { + const previousContext = microFramework.pushContext(context); + try { + return [component(props)]; + } finally { + microFramework.popContext(previousContext); + } + }, + () => {}, // No cleanup necessary. + () => microFramework.context, + ); +} + +describe('foreign component context', () => { + function Provider(props: {context?: string; children?: Node[]; render?: () => Node[]}): Node[] { + const previousContext = microFramework.pushContext(props.context); + try { + const nodes: Node[] = []; + if (props.children) { + nodes.push(...props.children); + } + if (props.render) { + nodes.push(...props.render()); + } + return nodes; + } finally { + microFramework.popContext(previousContext); + } + } + + function Consumer(): Node[] { + return [document.createTextNode(microFramework.context ?? '')]; + } + + it('should be undefined when not provided', async () => { + @Component({ + selector: 'no-ctx-cmp', + template: ``, + // @ts-ignore + foreignImports: [microImport(Consumer)], + }) + class NoCtxApp {} + + const fixture = TestBed.createComponent(NoCtxApp); + await fixture.whenStable(); + + expect(fixture.nativeElement.textContent).toBe(''); + }); + + it('should receive root context when provided', async () => { + @Component({ + selector: 'test-cmp', + // @ts-ignore + foreignImports: [microImport(Consumer)], + providers: [provideForeignRootContext(() => 'Hello, world!')], + template: ``, + }) + class App {} + + const fixture = TestBed.createComponent(App); + await fixture.whenStable(); + + expect(fixture.nativeElement.textContent).toBe('Hello, world!'); + }); + + // TODO: static content should be rendered lazily, inside the calling foreign context. + xit('should pass parent context to static children', async () => { + @Component({ + selector: 'test-app', + template: ` + + + + `, + // @ts-ignore + foreignImports: [microImport(Provider), microImport(Consumer)], + }) + class TestApp {} + + const fixture = TestBed.createComponent(TestApp); + await fixture.whenStable(); + + expect(fixture.nativeElement.textContent).toBe('Parent context'); + }); + + it('should pass parent context to dynamic children', async () => { + @Component({ + selector: 'test-app', + template: ` + + @content(render; let _) { + + } + + `, + // @ts-ignore + foreignImports: [microImport(Provider), microImport(Consumer)], + }) + class TestApp {} + + const fixture = TestBed.createComponent(TestApp); + await fixture.whenStable(); + + expect(fixture.nativeElement.textContent).toBe('Parent context'); + }); + + it('should receive context from nearest ancestor', async () => { + @Component({ + selector: 'test-app', + template: ` + + @content(render; let _) { + + @content(render; let _) { + + } + + } + + `, + // @ts-ignore + foreignImports: [microImport(Provider), microImport(Consumer)], + }) + class TestApp {} + + const fixture = TestBed.createComponent(TestApp); + await fixture.whenStable(); + + expect(fixture.nativeElement.textContent).toBe('Inner context'); + }); +}); diff --git a/packages/core/test/acceptance/foreign_component/foreign_component_spec.ts b/packages/core/test/acceptance/foreign_component/foreign_component_spec.ts index 41426add2608..e21b3efcf712 100644 --- a/packages/core/test/acceptance/foreign_component/foreign_component_spec.ts +++ b/packages/core/test/acceptance/foreign_component/foreign_component_spec.ts @@ -6,7 +6,16 @@ * found in the LICENSE file at https://angular.dev/license */ -import {Component, ElementRef, computed, effect, signal, viewChildren} from '@angular/core'; +import { + Component, + ElementRef, + Injector, + computed, + effect, + inject, + signal, + viewChildren, +} from '@angular/core'; import {TestBed} from '@angular/core/testing'; import {ForeignComponent} from '../../../src/interface/foreign_component'; import {foreignImport} from '../../../src/render3/foreign_import'; @@ -706,19 +715,22 @@ describe('foreign components', () => { } // A foreign component that acts like a conditional container (e.g. @if). - function Conditional(props: {when: () => boolean; then: () => Node[]}) { - effect((onCleanup) => { - // On cleanup (when `when()` changes or the component is destroyed), call the destroy - // callback registered by Angular for the projected content. - onCleanup(() => destroy?.()); - - if (props.when()) { - // Render the conditional content, instantiating any Angular views contained within. - // Internally this will call `frameworkOnDestroy` to register a callback to destroy the - // views when this effect's cleanup is run. - props.then(); - } - }); + function Conditional(props: {injector: Injector; when: () => boolean; then: () => Node[]}) { + effect( + (onCleanup) => { + // On cleanup (when `when()` changes or the component is destroyed), call the destroy + // callback registered by Angular for the projected content. + onCleanup(() => destroy?.()); + + if (props.when()) { + // Render the conditional content, instantiating any Angular views contained within. + // Internally this will call `frameworkOnDestroy` to register a callback to destroy the + // views when this effect's cleanup is run. + props.then(); + } + }, + {injector: props.injector}, + ); // We don't care about returning any nodes since we're just testing the content lifecycle. return []; @@ -738,7 +750,7 @@ describe('foreign components', () => { @Component({ template: ` - + @content (then; let _) { } @@ -749,6 +761,7 @@ describe('foreign components', () => { foreignImports: [frameworkImport(Conditional)], }) class TestDisposal { + readonly injector = inject(Injector); readonly visible = signal(true); } diff --git a/packages/core/test/render3/foreign_component_spec.ts b/packages/core/test/render3/foreign_component_spec.ts index 57d9dcec0781..473bcbd41a43 100644 --- a/packages/core/test/render3/foreign_component_spec.ts +++ b/packages/core/test/render3/foreign_component_spec.ts @@ -19,9 +19,6 @@ import {ɵɵelement, ɵɵelementEnd, ɵɵelementStart} from '../../src/render3/i import {ɵɵtext} from '../../src/render3/instructions/text'; import {ɵɵadvance} from '../../src/render3/instructions/advance'; import {ɵɵtextInterpolate2} from '../../src/render3/instructions/text_interpolation'; -import {inject, InjectionToken} from '../../src/di'; -import {ɵɵdefineDirective} from '../../src/render3/definition'; -import {ɵɵProvidersFeature} from '../../src/render3/features/providers_feature'; import {createLView} from '../../src/render3/view/construction'; import {renderView} from '../../src/render3/instructions/render'; import {LView, LViewFlags, PARENT, RENDERER, T_HOST} from '../../src/render3/interfaces/view'; @@ -168,44 +165,6 @@ describe('ɵɵforeignComponent', () => { ); }); - it('should execute the RENDER function inside the template injection context', () => { - const TEST_TOKEN = new InjectionToken('test-token'); - - const foreignComp = foreignImport( - () => { - const value = inject(TEST_TOKEN, {optional: true}) ?? 'null'; - const el = document.createElement('div'); - el.id = 'foreign-el'; - el.textContent = value; - return [[el]]; - }, - () => {}, - ); - - class ProviderDirective { - static ɵfac = () => new ProviderDirective(); - static ɵdir = ɵɵdefineDirective({ - type: ProviderDirective, - selectors: [['', 'provider-dir', '']], - features: [ɵɵProvidersFeature([{provide: TEST_TOKEN, useValue: 'templated-value'}])], - }); - } - - const fixture = new ViewFixture({ - decls: 2, - vars: 0, - consts: [['provider-dir', ''], foreignComp], - directives: [ProviderDirective], - create: () => { - ɵɵelementStart(0, 'div', 0); - ɵɵforeignComponent(1, 1); - ɵɵelementEnd(); - }, - }); - - expect(fixture.host.innerHTML).toContain('
templated-value
'); - }); - it('should support reusing the same template between multiple view instances', () => { const foreignComp1 = foreignImport( () => { @@ -309,9 +268,9 @@ describe('ɵɵforeignComponent', () => { ɵɵdomTemplate(1, descriptionTemplate, 2, 0); ɵɵdomTemplate(2, childrenTemplate, 2, 0); ɵɵforeignComponent(3, 0, { - icon: ɵɵforeignContent(0), - description: ɵɵforeignContent(1), - children: ɵɵforeignContent(2), + icon: ɵɵforeignContent(0, 0), + description: ɵɵforeignContent(1, 0), + children: ɵɵforeignContent(2, 0), }); }, });