diff --git a/packages/compiler-cli/linker/src/file_linker/partial_linkers/partial_component_linker_1.ts b/packages/compiler-cli/linker/src/file_linker/partial_linkers/partial_component_linker_1.ts index fff4380605e5..cb78ec151349 100644 --- a/packages/compiler-cli/linker/src/file_linker/partial_linkers/partial_component_linker_1.ts +++ b/packages/compiler-cli/linker/src/file_linker/partial_linkers/partial_component_linker_1.ts @@ -254,6 +254,7 @@ export class PartialComponentLinkerVersion1< i18nUseExternalIds: false, declarations, hasDirectiveDependencies: !baseMeta.isStandalone || hasDirectiveDependencies, + foreignImports: null, }; } diff --git a/packages/compiler-cli/src/ngtsc/annotations/component/src/handler.ts b/packages/compiler-cli/src/ngtsc/annotations/component/src/handler.ts index 2ddfb7190c36..a612e4ef2918 100644 --- a/packages/compiler-cli/src/ngtsc/annotations/component/src/handler.ts +++ b/packages/compiler-cli/src/ngtsc/annotations/component/src/handler.ts @@ -39,6 +39,7 @@ import { R3TemplateDependency, R3TemplateDependencyKind, R3TemplateDependencyMetadata, + R3ForeignComponentMetadata, SchemaMetadata, SelectorlessMatcher, SelectorMatcher, @@ -1023,6 +1024,7 @@ export class ComponentDecoratorHandler implements DecoratorHandler< relativeContextFilePath, rawImports: rawImports !== null ? new o.WrappedNodeExpr(rawImports) : undefined, relativeTemplatePath, + foreignImports: null, }, typeCheckMeta: extractDirectiveTypeCheckMeta(node, inputs, this.reflector), classMetadata: this.includeClassMetadata @@ -1495,10 +1497,12 @@ export class ComponentDecoratorHandler implements DecoratorHandler< ? this.resolveAllDeferredDependencies(resolution) : null; const defer = this.compileDeferBlocks(resolution); + const foreignImports = this.resolveForeignComponentImports(node, analysis); const meta: R3ComponentMetadata = { ...analysis.meta, ...resolution, defer, + foreignImports, }; const fac = compileNgFactoryDefField(toFactoryMetadata(meta, FactoryTarget.Component)); @@ -1625,10 +1629,12 @@ export class ComponentDecoratorHandler implements DecoratorHandler< const deferrableTypes = this.canDeferDeps ? analysis.explicitlyDeferredTypes : null; const defer = this.compileDeferBlocks(resolution); + const foreignImports = this.resolveForeignComponentImports(node, analysis); const meta = { ...analysis.meta, ...resolution, defer, + foreignImports, } as R3ComponentMetadata; if (deferrableTypes !== null) { @@ -1688,10 +1694,12 @@ export class ComponentDecoratorHandler implements DecoratorHandler< // Create a brand-new constant pool since there shouldn't be any constant sharing. const pool = new ConstantPool(); const defer = this.compileDeferBlocks(resolution); + const foreignImports = this.resolveForeignComponentImports(node, analysis); const meta: R3ComponentMetadata = { ...analysis.meta, ...resolution, defer, + foreignImports, }; const fac = compileNgFactoryDefField(toFactoryMetadata(meta, FactoryTarget.Component)); const def = compileComponentFromMetadata(meta, pool, this.getNewBindingParser()); @@ -2325,6 +2333,33 @@ export class ComponentDecoratorHandler implements DecoratorHandler< this.cycleAnalyzer.recordSyntheticImport(origin, imported); } + /** + * Resolves imported foreign components for code generation. + */ + private resolveForeignComponentImports( + node: ClassDeclaration, + analysis: Readonly, + ): R3ForeignComponentMetadata[] | null { + if (analysis.foreignImports === null || analysis.foreignImports.length === 0) { + return null; + } + const context = getSourceFile(node); + + return analysis.foreignImports.map((foreignMeta) => { + const {name, ref, rawExpression} = foreignMeta; + + const emittedRef = this.refEmitter.emit(ref, context); + assertSuccessfulReferenceEmit(emittedRef, node.name, 'foreign component'); + + ts.setEmitFlags(rawExpression, ts.EmitFlags.NoComments | ts.EmitFlags.NoNestedComments); + + return { + name, + component: new o.WrappedNodeExpr(rawExpression), + } satisfies R3ForeignComponentMetadata; + }); + } + /** * Resolves information about defer blocks dependencies to make it * available for the final `compile` step. diff --git a/packages/compiler-cli/src/ngtsc/scope/src/typecheck.ts b/packages/compiler-cli/src/ngtsc/scope/src/typecheck.ts index 5a7bde1d3b05..105961c1e341 100644 --- a/packages/compiler-cli/src/ngtsc/scope/src/typecheck.ts +++ b/packages/compiler-cli/src/ngtsc/scope/src/typecheck.ts @@ -12,13 +12,13 @@ import { SchemaMetadata, SelectorlessMatcher, SelectorMatcher, - ForeignComponentMeta, } from '@angular/compiler'; import {Reference} from '../../imports'; import { DirectiveMeta, flattenInheritedDirectiveMetadata, + ForeignComponentMeta, HostDirectivesResolver, MetadataReader, MetaKind, diff --git a/packages/compiler-cli/test/compliance/test_cases/r3_compiler_compliance/components_and_directives/standalone/GOLDEN_PARTIAL.js b/packages/compiler-cli/test/compliance/test_cases/r3_compiler_compliance/components_and_directives/standalone/GOLDEN_PARTIAL.js index dd21cf288989..c102a57fbcac 100644 --- a/packages/compiler-cli/test/compliance/test_cases/r3_compiler_compliance/components_and_directives/standalone/GOLDEN_PARTIAL.js +++ b/packages/compiler-cli/test/compliance/test_cases/r3_compiler_compliance/components_and_directives/standalone/GOLDEN_PARTIAL.js @@ -375,3 +375,56 @@ export declare class StandaloneComponent { static ɵcmp: i0.ɵɵComponentDeclaration; } +/**************************************************************************************************** + * PARTIAL FILE: foreign_component.js + ****************************************************************************************************/ +import { Component } from '@angular/core'; +import * as i0 from "@angular/core"; +export function FancyButton() { } +// @angular/core does not expose the `ForeignComponent` type this should return. +function frameworkImport(component) { + return () => { }; +} +export class TestCmp { + title = 'Submit'; + static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "0.0.0-PLACEHOLDER", ngImport: i0, type: TestCmp, deps: [], target: i0.ɵɵFactoryTarget.Component }); + static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "0.0.0-PLACEHOLDER", type: TestCmp, isStandalone: true, selector: "main", ngImport: i0, template: ` + + `, isInline: true }); +} +i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "0.0.0-PLACEHOLDER", ngImport: i0, type: TestCmp, decorators: [{ + type: Component, + args: [{ + selector: 'main', + template: ` + + `, + // @ts-ignore: @angular/core does not expose the `foreignImports` property. + foreignImports: [ + // @ts-ignore: @angular/core does not expose the `ForeignComponent` type this expects. + frameworkImport(FancyButton) + ], + }] + }] }); + +/**************************************************************************************************** + * PARTIAL FILE: foreign_component.d.ts + ****************************************************************************************************/ +import * as i0 from "@angular/core"; +export declare function FancyButton(): void; +export declare class TestCmp { + title: string; + static ɵfac: i0.ɵɵFactoryDeclaration; + static ɵcmp: i0.ɵɵComponentDeclaration; +} + diff --git a/packages/compiler-cli/test/compliance/test_cases/r3_compiler_compliance/components_and_directives/standalone/TEST_CASES.json b/packages/compiler-cli/test/compliance/test_cases/r3_compiler_compliance/components_and_directives/standalone/TEST_CASES.json index 81bd37b78135..4093dbfc449b 100644 --- a/packages/compiler-cli/test/compliance/test_cases/r3_compiler_compliance/components_and_directives/standalone/TEST_CASES.json +++ b/packages/compiler-cli/test/compliance/test_cases/r3_compiler_compliance/components_and_directives/standalone/TEST_CASES.json @@ -3,106 +3,85 @@ "cases": [ { "description": "should properly compile a standalone component", - "inputFiles": [ - "component.ts" - ], + "inputFiles": ["component.ts"], "expectations": [ { "failureMessage": "Invalid component definition", - "files": [ - "component.js" - ] + "files": ["component.js"] } ], - "compilationModeFilter": [ - "full compile", - "local compile", - "declaration-only emit" - ] + "compilationModeFilter": ["full compile", "local compile", "declaration-only emit"] }, { "description": "should properly compile a standalone directive", - "inputFiles": [ - "directive.ts" - ], + "inputFiles": ["directive.ts"], "expectations": [ { "failureMessage": "Invalid directive definition", - "files": [ - "directive.js" - ] + "files": ["directive.js"] } ] }, { "description": "should properly compile a standalone pipe", - "inputFiles": [ - "pipe.ts" - ], + "inputFiles": ["pipe.ts"], "expectations": [ { "failureMessage": "Invalid pipe definition", - "files": [ - "pipe.js" - ] + "files": ["pipe.js"] } ] }, { "description": "should generate dependencies array from imports", - "inputFiles": [ - "imports.ts" - ], + "inputFiles": ["imports.ts"], "expectations": [ { "failureMessage": "Invalid standalone component dependencies", - "files": [ - "imports.js" - ] + "files": ["imports.js"] } ] }, { "description": "should support recursivity in templates", - "inputFiles": [ - "recursive.ts" - ], + "inputFiles": ["recursive.ts"], "expectations": [ { "failureMessage": "Recursive usage not accounted for", - "files": [ - "recursive.js" - ] + "files": ["recursive.js"] } ] }, { "description": "should optimize injector imports", - "inputFiles": [ - "module_optimization.ts" - ], + "inputFiles": ["module_optimization.ts"], "expectations": [ { "failureMessage": "Injector imports not optimized", - "files": [ - "module_optimization.js" - ] + "files": ["module_optimization.js"] } ] }, { "description": "should handle a forwardRef in the imports of a standalone component", - "inputFiles": [ - "forward_ref.ts" - ], + "inputFiles": ["forward_ref.ts"], "expectations": [ { "failureMessage": "Invalid component definition", - "files": [ - "forward_ref.js" - ] + "files": ["forward_ref.js"] } ] + }, + { + "description": "should properly compile foreign component imports in a standalone component", + "inputFiles": ["foreign_component.ts"], + "expectations": [ + { + "failureMessage": "Invalid foreign component definition", + "files": ["foreign_component.js"] + } + ], + "compilationModeFilter": ["full compile", "local compile"] } ] -} \ No newline at end of file +} 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 new file mode 100644 index 000000000000..1ddad2ca8bad --- /dev/null +++ b/packages/compiler-cli/test/compliance/test_cases/r3_compiler_compliance/components_and_directives/standalone/foreign_component.js @@ -0,0 +1,15 @@ +export class TestCmp { + // ... + static ɵcmp = /*@__PURE__*/ i0.ɵɵdefineComponent({ + type: TestCmp, + selectors: [["main"]], + decls: 1, + vars: 0, + template: function TestCmp_Template(rf, ctx) { + if (rf & 1) { + i0.ɵɵforeignComponent(0, frameworkImport(FancyButton), { class: "btn-cls", "unsafe-attr": "value", label: ctx.title, "unsafe-input": ctx.title }); + } + }, + 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 new file mode 100644 index 000000000000..1ddad2ca8bad --- /dev/null +++ b/packages/compiler-cli/test/compliance/test_cases/r3_compiler_compliance/components_and_directives/standalone/foreign_component.local.js @@ -0,0 +1,15 @@ +export class TestCmp { + // ... + static ɵcmp = /*@__PURE__*/ i0.ɵɵdefineComponent({ + type: TestCmp, + selectors: [["main"]], + decls: 1, + vars: 0, + template: function TestCmp_Template(rf, ctx) { + if (rf & 1) { + i0.ɵɵforeignComponent(0, frameworkImport(FancyButton), { class: "btn-cls", "unsafe-attr": "value", label: ctx.title, "unsafe-input": ctx.title }); + } + }, + encapsulation: 2 + }); +} diff --git a/packages/compiler-cli/test/compliance/test_cases/r3_compiler_compliance/components_and_directives/standalone/foreign_component.ts b/packages/compiler-cli/test/compliance/test_cases/r3_compiler_compliance/components_and_directives/standalone/foreign_component.ts new file mode 100644 index 000000000000..7e3ea2ed3984 --- /dev/null +++ b/packages/compiler-cli/test/compliance/test_cases/r3_compiler_compliance/components_and_directives/standalone/foreign_component.ts @@ -0,0 +1,28 @@ +import {Component} from '@angular/core'; + +export function FancyButton() {} + +// @angular/core does not expose the `ForeignComponent` type this should return. +function frameworkImport(component: {}): Function { + return () => {}; +} + +@Component({ + selector: 'main', + template: ` + + `, + // @ts-ignore: @angular/core does not expose the `foreignImports` property. + foreignImports: [ + // @ts-ignore: @angular/core does not expose the `ForeignComponent` type this expects. + frameworkImport(FancyButton) + ], +}) +export class TestCmp { + title = 'Submit'; +} diff --git a/packages/compiler/src/jit_compiler_facade.ts b/packages/compiler/src/jit_compiler_facade.ts index 641379c8c014..993a2cfee828 100644 --- a/packages/compiler/src/jit_compiler_facade.ts +++ b/packages/compiler/src/jit_compiler_facade.ts @@ -360,6 +360,7 @@ export class CompilerFacadeImpl implements CompilerFacade { relativeContextFilePath: '', i18nUseExternalIds: true, relativeTemplatePath: null, + foreignImports: null, }; const jitExpressionSourceMap = `ng:///${facade.name}.js`; return this.compileComponentFromMeta(angularCoreEnv, jitExpressionSourceMap, meta); @@ -717,6 +718,7 @@ function convertDeclareComponentFacadeToMetadata( relativeTemplatePath: null, hasDirectiveDependencies, legacyOptionalChaining: decl.legacyOptionalChaining ?? LEGACY_OPTIONAL_CHAINING_DEFAULT, + foreignImports: null, }; } diff --git a/packages/compiler/src/render3/r3_identifiers.ts b/packages/compiler/src/render3/r3_identifiers.ts index 82d96103762f..73fdad09c1af 100644 --- a/packages/compiler/src/render3/r3_identifiers.ts +++ b/packages/compiler/src/render3/r3_identifiers.ts @@ -25,6 +25,8 @@ export class Identifiers { static elementEnd: o.ExternalReference = {name: 'ɵɵelementEnd', moduleName: CORE}; + static foreignComponent: o.ExternalReference = {name: 'ɵɵforeignComponent', moduleName: CORE}; + static domElement: o.ExternalReference = {name: 'ɵɵdomElement', moduleName: CORE}; static domElementStart: o.ExternalReference = {name: 'ɵɵdomElementStart', moduleName: CORE}; static domElementEnd: o.ExternalReference = {name: 'ɵɵdomElementEnd', moduleName: CORE}; diff --git a/packages/compiler/src/render3/view/api.ts b/packages/compiler/src/render3/view/api.ts index 216825b0f8b2..d161e8b8366f 100644 --- a/packages/compiler/src/render3/view/api.ts +++ b/packages/compiler/src/render3/view/api.ts @@ -305,6 +305,11 @@ export interface R3ComponentMetadata< * not be set. If component has empty array imports then this field is not set. */ rawImports?: o.Expression; + + /** + * Foreign components imported by the component. + */ + foreignImports: R3ForeignComponentMetadata[] | null; } /** @@ -406,6 +411,21 @@ export interface R3NgModuleDependencyMetadata extends R3TemplateDependency { kind: R3TemplateDependencyKind.NgModule; } +/** + * Information about a foreign component that is used in a component template. + */ +export interface R3ForeignComponentMetadata { + /** + * The foreign component's name. + */ + name: string; + + /** + * The expression used to refer to this foreign component. + */ + component: o.Expression; +} + /** * Information needed to compile a query (view or content). */ diff --git a/packages/compiler/src/render3/view/compiler.ts b/packages/compiler/src/render3/view/compiler.ts index 2a68935853c8..9959c84773d4 100644 --- a/packages/compiler/src/render3/view/compiler.ts +++ b/packages/compiler/src/render3/view/compiler.ts @@ -216,6 +216,7 @@ export function compileComponentFromMetadata( meta.relativeTemplatePath, getTemplateSourceLocationsEnabled(), meta.legacyOptionalChaining, + meta.foreignImports, ); // Then the IR is transformed to prepare it for code generation. diff --git a/packages/compiler/src/template/pipeline/ir/src/enums.ts b/packages/compiler/src/template/pipeline/ir/src/enums.ts index c10097f9e833..c36a5fd32670 100644 --- a/packages/compiler/src/template/pipeline/ir/src/enums.ts +++ b/packages/compiler/src/template/pipeline/ir/src/enums.ts @@ -38,6 +38,11 @@ export enum OpKind { */ Element, + /** + * An operation to render a foreign component. + */ + ForeignComponent, + /** * An operation which declares an embedded view. */ diff --git a/packages/compiler/src/template/pipeline/ir/src/expression.ts b/packages/compiler/src/template/pipeline/ir/src/expression.ts index 4c3a094b5b58..5221bb7c6bc0 100644 --- a/packages/compiler/src/template/pipeline/ir/src/expression.ts +++ b/packages/compiler/src/template/pipeline/ir/src/expression.ts @@ -1266,6 +1266,11 @@ export function transformExpressionsInOp( case OpKind.StoreLet: op.value = transformExpressionsInExpression(op.value, transform, flags); break; + case OpKind.ForeignComponent: + if (op.props !== null) { + op.props = transformExpressionsInExpression(op.props, transform, flags); + } + break; case OpKind.Advance: case OpKind.Container: case OpKind.ContainerEnd: diff --git a/packages/compiler/src/template/pipeline/ir/src/ops/create.ts b/packages/compiler/src/template/pipeline/ir/src/ops/create.ts index c104e9d2a6e8..51276d58d95f 100644 --- a/packages/compiler/src/template/pipeline/ir/src/ops/create.ts +++ b/packages/compiler/src/template/pipeline/ir/src/ops/create.ts @@ -44,6 +44,7 @@ export type CreateOp = | ElementOp | ElementStartOp | ElementEndOp + | ForeignComponentOp | ContainerOp | ContainerStartOp | ContainerEndOp @@ -237,6 +238,51 @@ export function createElementStartOp( }; } +/** + * Logical operation representing a foreign component in the creation IR. + */ +export interface ForeignComponentOp extends Op, ConsumesSlotOpTrait { + kind: OpKind.ForeignComponent; + + /** + * The `XrefId` allocated for this foreign component. + */ + xref: XrefId; + + /** + * Reference to the foreign component class/function itself as an output AST expression. + */ + foreignComponentRef: o.Expression; + + /** + * Static attributes and property bindings aggregated as an object literal. + */ + props: o.Expression | null; + + sourceSpan: ParseSourceSpan | null; +} + +/** + * Create a `ForeignComponentOp`. + */ +export function createForeignComponentOp( + xref: XrefId, + foreignComponentRef: o.Expression, + props: o.Expression | null, + sourceSpan: ParseSourceSpan | null, +): ForeignComponentOp { + return { + kind: OpKind.ForeignComponent, + xref, + handle: new SlotHandle(), + foreignComponentRef, + props, + sourceSpan, + ...TRAIT_CONSUMES_SLOT, + ...NEW_OP, + }; +} + /** * Logical operation representing an element with no children in the creation IR. */ diff --git a/packages/compiler/src/template/pipeline/src/compilation.ts b/packages/compiler/src/template/pipeline/src/compilation.ts index 0a1753507d8a..220d24b8be0c 100644 --- a/packages/compiler/src/template/pipeline/src/compilation.ts +++ b/packages/compiler/src/template/pipeline/src/compilation.ts @@ -7,8 +7,10 @@ */ import {ConstantPool} from '../../../constant_pool'; +import {SelectorlessMatcher} from '../../../directive_matching'; import * as o from '../../../output/output_ast'; -import {R3ComponentDeferMetadata} from '../../../render3/view/api'; +import {R3ComponentDeferMetadata, R3ForeignComponentMetadata} from '../../../render3/view/api'; +import * as t from '../../../render3/r3_ast'; import * as ir from '../ir'; export enum CompilationJobKind { @@ -75,6 +77,8 @@ export abstract class CompilationJob { * embedded views or host bindings. */ export class ComponentCompilationJob extends CompilationJob { + private foreignMatcher: SelectorlessMatcher | null; + constructor( componentName: string, pool: ConstantPool, @@ -86,10 +90,29 @@ export class ComponentCompilationJob extends CompilationJob { readonly relativeTemplatePath: string | null, readonly enableDebugLocations: boolean, legacyOptionalChaining: boolean, + readonly foreignImports: R3ForeignComponentMetadata[] | null, ) { super(componentName, pool, mode, legacyOptionalChaining); this.root = new ViewCompilationUnit(this, this.allocateXrefId(), null); this.views.set(this.root.xref, this.root); + + if (foreignImports && foreignImports.length > 0) { + const registry = new Map(); + for (const meta of foreignImports) { + registry.set(meta.name, [meta]); + } + this.foreignMatcher = new SelectorlessMatcher(registry); + } else { + this.foreignMatcher = null; + } + } + + getForeignComponent(element: t.Element): R3ForeignComponentMetadata | null { + if (this.foreignMatcher === null) { + return null; + } + const matches = this.foreignMatcher.match(element.name); + return matches.length > 0 ? matches[0] : null; } override kind = CompilationJobKind.Tmpl; diff --git a/packages/compiler/src/template/pipeline/src/ingest.ts b/packages/compiler/src/template/pipeline/src/ingest.ts index 30a5da127527..9f0b703c152e 100644 --- a/packages/compiler/src/template/pipeline/src/ingest.ts +++ b/packages/compiler/src/template/pipeline/src/ingest.ts @@ -14,7 +14,12 @@ import {splitNsName} from '../../../ml_parser/tags'; import * as o from '../../../output/output_ast'; import {ParseSourceSpan} from '../../../parse_util'; import * as t from '../../../render3/r3_ast'; -import {DeferBlockDepsEmitMode, R3ComponentDeferMetadata} from '../../../render3/view/api'; +import {isUnsafeObjectKey} from '../../../render3/util'; +import { + DeferBlockDepsEmitMode, + R3ComponentDeferMetadata, + R3ForeignComponentMetadata, +} from '../../../render3/view/api'; import {icuFromI18nMessage} from '../../../render3/view/i18n/util'; import {DomElementSchemaRegistry} from '../../../schema/dom_element_schema_registry'; import {BindingParser} from '../../../template_parser/binding_parser'; @@ -65,6 +70,7 @@ export function ingestComponent( relativeTemplatePath: string | null, enableDebugLocations: boolean, legacyOptionalChaining: boolean, + foreignImports: R3ForeignComponentMetadata[] | null, ): ComponentCompilationJob { const job = new ComponentCompilationJob( componentName, @@ -77,6 +83,7 @@ export function ingestComponent( relativeTemplatePath, enableDebugLocations, legacyOptionalChaining, + foreignImports, ); ingestNodes(job.root, template); return job; @@ -283,8 +290,35 @@ function ingestElement(unit: ViewCompilationUnit, element: t.Element): void { const id = unit.job.allocateXrefId(); - const [namespaceKey, elementName] = splitNsName(element.name); + const foreignComp = unit.job.getForeignComponent(element); + if (foreignComp) { + const propEntries: {key: string; quoted: boolean; value: o.Expression}[] = []; + for (const attr of element.attributes) { + propEntries.push({ + key: attr.name, + value: o.literal(attr.value), + quoted: isUnsafeObjectKey(attr.name), + }); + } + for (const input of element.inputs) { + propEntries.push({ + key: input.name, + value: convertAst(input.value, unit.job, input.sourceSpan), + quoted: isUnsafeObjectKey(input.name), + }); + } + const props = propEntries.length > 0 ? o.literalMap(propEntries) : null; + // Foreign components are created in the creation block. Updates are triggered reactively + // through directly passed signal properties, alleviating the need for any explicit update + // operations. + unit.create.push( + ir.createForeignComponentOp(id, foreignComp.component, props, element.startSourceSpan), + ); + return; + } + + const [namespaceKey, elementName] = splitNsName(element.name); const startOp = ir.createElementStartOp( elementName, id, diff --git a/packages/compiler/src/template/pipeline/src/instruction.ts b/packages/compiler/src/template/pipeline/src/instruction.ts index a9e2573fc021..ac9f2d85aae2 100644 --- a/packages/compiler/src/template/pipeline/src/instruction.ts +++ b/packages/compiler/src/template/pipeline/src/instruction.ts @@ -49,6 +49,19 @@ export function elementStart( ); } +export function foreignComponent( + slot: number, + foreignComponentRef: o.Expression, + props: o.Expression | null, + sourceSpan: ParseSourceSpan | null, +): ir.CreateOp { + const args = [o.literal(slot), foreignComponentRef]; + if (props !== null) { + args.push(props); + } + return call(Identifiers.foreignComponent, args, sourceSpan); +} + function elementOrContainerBase( instruction: o.ExternalReference, slot: number, diff --git a/packages/compiler/src/template/pipeline/src/phases/reify.ts b/packages/compiler/src/template/pipeline/src/phases/reify.ts index 7a00b45aff59..929950c54ab4 100644 --- a/packages/compiler/src/template/pipeline/src/phases/reify.ts +++ b/packages/compiler/src/template/pipeline/src/phases/reify.ts @@ -142,6 +142,12 @@ function reifyCreateOperations(unit: CompilationUnit, ops: ir.OpList( + index: number, + foreignComponent: ForeignComponent, + props: TProps, +): void { + // No-op for now! +} diff --git a/packages/core/src/render3/jit/environment.ts b/packages/core/src/render3/jit/environment.ts index 2f0029a89264..67b3417085b4 100644 --- a/packages/core/src/render3/jit/environment.ts +++ b/packages/core/src/render3/jit/environment.ts @@ -55,6 +55,7 @@ export const angularCoreEnv: {[name: string]: unknown} = (() => ({ 'ɵɵelementStart': r3.ɵɵelementStart, 'ɵɵelementEnd': r3.ɵɵelementEnd, 'ɵɵelement': r3.ɵɵelement, + 'ɵɵforeignComponent': r3.ɵɵforeignComponent, 'ɵɵelementContainerStart': r3.ɵɵelementContainerStart, 'ɵɵelementContainerEnd': r3.ɵɵelementContainerEnd, 'ɵɵdomElement': r3.ɵɵdomElement, diff --git a/packages/core/test/render3/instructions_spec.ts b/packages/core/test/render3/instructions_spec.ts index 7edd0be76634..48b2d34ddb9c 100644 --- a/packages/core/test/render3/instructions_spec.ts +++ b/packages/core/test/render3/instructions_spec.ts @@ -17,6 +17,7 @@ import { ɵɵproperty, ɵɵstyleMap, ɵɵstyleProp, + ɵɵforeignComponent, } from '../../src/render3/index'; import {AttributeMarker} from '../../src/render3/interfaces/attribute_marker'; import {