mirror of
https://github.com/angular/angular.git
synced 2026-09-14 13:54:52 +08:00
refactor(core): defer foreign component rendering to post-update pass
Foreign components imported via foreignImports and created by the
ɵɵforeignComponent instruction were previously rendered eagerly in the
creation phase (rf & 1) of the template. This restricted which
properties could be passed to foreign component props, as parent-bound
inputs (@Input(), input(), input.required()), properties initialized in
ngOnInit(), and pull-based view queries (viewChild()) were not yet
initialized at creation time.
This change defers foreign component rendering to run as a view effect
during the update pass:
- Update ɵɵforeignComponent in core to schedule component rendering via
createViewEffect (executed in runEffectsInView during refreshView),
executed with setActiveConsumer(null) to prevent reactive context
leakage and destroyed immediately upon first execution.
- Update ɵɵforeignComponent to strictly accept props as a factory function
(() => props) or null.
- Update the compiler template pipeline to wrap foreign component props
in an arrow function closure (() => ({ ... })).
- Hoist creation-time foreign content projection instructions
(ɵɵforeignContent, ɵɵforeignContentFn) into creation-phase variable
declarations before ɵɵforeignComponent so creation-time context is
captured safely.
(cherry picked from commit 34817da735)
This commit is contained in:
+8
-4
@@ -39,7 +39,7 @@ function TestCmpRenderProps_Items_0_Template(rf, ctx) {
|
||||
function TestCmpConditional_Conditional_0_Conditional_0_Template(rf, ctx) {
|
||||
if (rf & 1) {
|
||||
const ctx_r0 = i0.ɵɵnextContext(2);
|
||||
i0.ɵɵforeignComponent(0, 0, { label: ctx_r0.title });
|
||||
i0.ɵɵforeignComponent(0, 0, () => ({ label: ctx_r0.title }));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -65,7 +65,7 @@ export class TestCmp {
|
||||
consts: [frameworkImport(FancyButton)],
|
||||
template: function TestCmp_Template(rf, ctx) {
|
||||
if (rf & 1) {
|
||||
i0.ɵɵforeignComponent(0, 0, { class: "btn-cls", "unsafe-attr": "value", label: ctx.title, "unsafe-input": ctx.title });
|
||||
i0.ɵɵforeignComponent(0, 0, () => ({ class: "btn-cls", "unsafe-attr": "value", label: ctx.title, "unsafe-input": ctx.title }));
|
||||
}
|
||||
},
|
||||
encapsulation: 2
|
||||
@@ -85,7 +85,10 @@ 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, 0), description: i0.ɵɵforeignContent(1, 0), children: i0.ɵɵforeignContent(2, 0) });
|
||||
const icon_r1 = i0.ɵɵforeignContent(0, 0);
|
||||
const description_r2 = i0.ɵɵforeignContent(1, 0);
|
||||
const children_r3 = i0.ɵɵforeignContent(2, 0);
|
||||
i0.ɵɵforeignComponent(3, 0, () => ({ label: ctx.title, icon: icon_r1, description: description_r2, children: children_r3 }));
|
||||
}
|
||||
},
|
||||
encapsulation: 2
|
||||
@@ -105,7 +108,8 @@ export class TestCmpRenderProps {
|
||||
template: function TestCmpRenderProps_Template(rf, ctx) {
|
||||
if (rf & 1) {
|
||||
i0.ɵɵdomTemplate(0, TestCmpRenderProps_Items_0_Template, 2, 2);
|
||||
i0.ɵɵforeignComponent(1, 0, { label: ctx.title, items: i0.ɵɵforeignContentFn(0, 0) });
|
||||
const items_r3 = i0.ɵɵforeignContentFn(0, 0);
|
||||
i0.ɵɵforeignComponent(1, 0, () => ({ label: ctx.title, items: items_r3 }));
|
||||
}
|
||||
},
|
||||
encapsulation: 2
|
||||
|
||||
+7
-3
@@ -48,7 +48,7 @@ export class TestCmp {
|
||||
consts: [frameworkImport(FancyButton)],
|
||||
template: function TestCmp_Template(rf, ctx) {
|
||||
if (rf & 1) {
|
||||
i0.ɵɵforeignComponent(0, 0, { class: "btn-cls", "unsafe-attr": "value", label: ctx.title, "unsafe-input": ctx.title });
|
||||
i0.ɵɵforeignComponent(0, 0, () => ({ class: "btn-cls", "unsafe-attr": "value", label: ctx.title, "unsafe-input": ctx.title }));
|
||||
}
|
||||
},
|
||||
encapsulation: 2
|
||||
@@ -68,7 +68,10 @@ 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, 0), description: i0.ɵɵforeignContent(1, 0), children: i0.ɵɵforeignContent(2, 0) });
|
||||
const icon_r1 = i0.ɵɵforeignContent(0, 0);
|
||||
const description_r2 = i0.ɵɵforeignContent(1, 0);
|
||||
const children_r3 = i0.ɵɵforeignContent(2, 0);
|
||||
i0.ɵɵforeignComponent(3, 0, () => ({ label: ctx.title, icon: icon_r1, description: description_r2, children: children_r3 }));
|
||||
}
|
||||
},
|
||||
encapsulation: 2
|
||||
@@ -88,7 +91,8 @@ export class TestCmpRenderProps {
|
||||
template: function TestCmpRenderProps_Template(rf, ctx) {
|
||||
if (rf & 1) {
|
||||
i0.ɵɵtemplate(0, TestCmpRenderProps_Items_0_Template, 2, 2);
|
||||
i0.ɵɵforeignComponent(1, 0, { label: ctx.title, items: i0.ɵɵforeignContentFn(0, 0) });
|
||||
const items_r3 = i0.ɵɵforeignContentFn(0, 0);
|
||||
i0.ɵɵforeignComponent(1, 0, () => ({ label: ctx.title, items: items_r3 }));
|
||||
}
|
||||
},
|
||||
encapsulation: 2
|
||||
|
||||
@@ -144,7 +144,7 @@ function reifyCreateOperations(unit: CompilationUnit, ops: ir.OpList<ir.CreateOp
|
||||
);
|
||||
break;
|
||||
case ir.OpKind.ForeignComponent:
|
||||
const propsExpr =
|
||||
const propsMap =
|
||||
op.props.size > 0
|
||||
? o.literalMap(
|
||||
Array.from(op.props.entries()).map(([key, value]) => ({
|
||||
@@ -154,6 +154,7 @@ function reifyCreateOperations(unit: CompilationUnit, ops: ir.OpList<ir.CreateOp
|
||||
})),
|
||||
)
|
||||
: null;
|
||||
const propsExpr = propsMap !== null ? o.arrowFn([], propsMap) : null;
|
||||
ir.OpList.replace(
|
||||
op,
|
||||
ng.foreignComponent(op.handle.slot!, o.literal(op.constIndex), propsExpr, op.sourceSpan),
|
||||
|
||||
@@ -52,7 +52,23 @@ export function resolveForeignContent(job: CompilationJob): void {
|
||||
templateOp.handle,
|
||||
target.constIndex,
|
||||
);
|
||||
target.props.set(op.propertyName, foreignContent);
|
||||
|
||||
const varXref = job.allocateXrefId();
|
||||
const variable: ir.SemanticVariable = {
|
||||
kind: ir.SemanticVariableKind.Identifier,
|
||||
name: null,
|
||||
identifier: op.propertyName,
|
||||
local: true,
|
||||
};
|
||||
const varOp = ir.createVariableOp<ir.CreateOp>(
|
||||
varXref,
|
||||
variable,
|
||||
foreignContent,
|
||||
ir.VariableFlags.None,
|
||||
);
|
||||
ir.OpList.insertBefore<ir.CreateOp>(varOp, target);
|
||||
|
||||
target.props.set(op.propertyName, new ir.ReadVariableExpr(varXref));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
* found in the LICENSE file at https://angular.dev/license
|
||||
*/
|
||||
|
||||
import {setActiveConsumer} from '../../../primitives/signals';
|
||||
import {Injector} from '../../di/injector';
|
||||
import {InternalInjectFlags} from '../../di/interface/injector';
|
||||
import {
|
||||
@@ -28,8 +29,9 @@ import {TContainerNode, TNodeType} from '../interfaces/node';
|
||||
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 {ENVIRONMENT, FLAGS, HEADER_OFFSET, LView, RENDERER, TVIEW} from '../interfaces/view';
|
||||
import {appendChild} from '../node_manipulation';
|
||||
import {createViewEffect} from '../reactivity/effect';
|
||||
import {getLView, getTView, setCurrentTNode, setCurrentTNodeAsNotParent} from '../state';
|
||||
import {getOrCreateTNode} from '../tnode_manipulation';
|
||||
import {getConstant} from '../util/view_utils';
|
||||
@@ -42,13 +44,13 @@ import {createAndRenderEmbeddedLView} from '../view_manipulation';
|
||||
*
|
||||
* @param index The index of the container in the data array.
|
||||
* @param foreignComponentIndex The index of the matched foreign component in the constant pool.
|
||||
* @param props Aggregate properties and static attributes.
|
||||
* @param props A factory function returning aggregate properties/static attributes.
|
||||
* @codeGenApi
|
||||
*/
|
||||
export function ɵɵforeignComponent(
|
||||
index: number,
|
||||
foreignComponentIndex: number,
|
||||
props?: any,
|
||||
props?: (() => any) | null,
|
||||
): void {
|
||||
const lView = getLView();
|
||||
const tView = getTView();
|
||||
@@ -83,30 +85,41 @@ export function ɵɵforeignComponent(
|
||||
// 4. Create the Foreign View and insert it at index 0 of the container
|
||||
const viewRef = createForeignView(lContainer, 0);
|
||||
|
||||
// 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);
|
||||
// 5. Create a run-once view effect to render the foreign component during runEffectsInView
|
||||
const node = createViewEffect(lView, lView[ENVIRONMENT].changeDetectionScheduler!, () => {
|
||||
// Destroy this effect the first time it's called to ensure it doesn't run more than once.
|
||||
node.destroy();
|
||||
|
||||
// 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;
|
||||
if (parent) {
|
||||
for (let i = 0; i < nodes.length; i++) {
|
||||
nativeInsertBefore(renderer, parent, nodes[i], tail, false);
|
||||
if (isDestroyed(lView)) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// 7. Register the DisposeFn in the foreign view's LView destroy hooks.
|
||||
if (dispose) {
|
||||
viewRef.onDestroy(dispose);
|
||||
}
|
||||
const prevConsumer = setActiveConsumer(null);
|
||||
try {
|
||||
const resolvedProps = props ? props() : undefined;
|
||||
const context = getOrCreateInjectable(
|
||||
tNode,
|
||||
lView,
|
||||
FOREIGN_CONTEXT,
|
||||
InternalInjectFlags.Optional,
|
||||
);
|
||||
const [nodes, dispose] = foreignComponent[RENDER](resolvedProps, context ?? undefined);
|
||||
|
||||
const tail = viewRef.tail as RNode;
|
||||
const parent = tail.parentNode;
|
||||
if (parent) {
|
||||
for (let i = 0; i < nodes.length; i++) {
|
||||
nativeInsertBefore(renderer, parent, nodes[i], tail, false);
|
||||
}
|
||||
}
|
||||
|
||||
if (dispose) {
|
||||
viewRef.onDestroy(dispose);
|
||||
}
|
||||
} finally {
|
||||
setActiveConsumer(prevConsumer);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -188,8 +201,8 @@ export function ɵɵforeignContent(index: number, foreignComponentConstIndex: nu
|
||||
}
|
||||
});
|
||||
|
||||
// Extract and return the root nodes of the created view
|
||||
const embeddedTView = embeddedLView[TVIEW];
|
||||
// Extract and return the root nodes of the created view
|
||||
return collectNativeNodes(embeddedTView, embeddedLView, embeddedTView.firstChild, []);
|
||||
};
|
||||
|
||||
|
||||
@@ -7,14 +7,20 @@
|
||||
*/
|
||||
|
||||
import {
|
||||
AfterViewInit,
|
||||
Component,
|
||||
ElementRef,
|
||||
Injector,
|
||||
Input,
|
||||
OnInit,
|
||||
ViewChild,
|
||||
computed,
|
||||
effect,
|
||||
inject,
|
||||
input,
|
||||
signal,
|
||||
untracked,
|
||||
viewChild,
|
||||
viewChildren,
|
||||
} from '@angular/core';
|
||||
import {TestBed} from '@angular/core/testing';
|
||||
@@ -1210,4 +1216,278 @@ describe('foreign components', () => {
|
||||
expect(fixture.nativeElement.textContent).toBe('true');
|
||||
});
|
||||
});
|
||||
|
||||
// Verify deferred rendering and property initialization timing
|
||||
describe('deferred rendering and property initialization', () => {
|
||||
function TitleWidget(props: {title: string}): Node[] {
|
||||
const span = document.createElement('span');
|
||||
span.id = 'title-display';
|
||||
span.textContent = props.title ?? 'UNDEFINED';
|
||||
return [span];
|
||||
}
|
||||
|
||||
function QueryWidget(props: {target: any}): Node[] {
|
||||
const span = document.createElement('span');
|
||||
span.id = 'query-display';
|
||||
span.textContent = props.target ? 'HAS_TARGET' : 'NO_TARGET';
|
||||
return [span];
|
||||
}
|
||||
|
||||
function ReactiveTitleWidget(props: {title: () => string; injector: Injector}): Node[] {
|
||||
const span = document.createElement('span');
|
||||
span.id = 'title-display';
|
||||
effect(
|
||||
() => {
|
||||
span.textContent = props.title();
|
||||
},
|
||||
{injector: props.injector},
|
||||
);
|
||||
return [span];
|
||||
}
|
||||
|
||||
function ReactiveQueryWidget(props: {target: () => any; injector: Injector}): Node[] {
|
||||
const span = document.createElement('span');
|
||||
span.id = 'query-display';
|
||||
effect(
|
||||
() => {
|
||||
const target = props.target();
|
||||
span.textContent = target ? 'HAS_TARGET' : 'NO_TARGET';
|
||||
},
|
||||
{injector: props.injector},
|
||||
);
|
||||
return [span];
|
||||
}
|
||||
|
||||
it('should support passing required inputs to foreign component props', async () => {
|
||||
@Component({
|
||||
selector: 'foreign-host',
|
||||
template: `<TitleWidget [title]="title()" />`,
|
||||
// @ts-ignore
|
||||
foreignImports: [frameworkImport(TitleWidget)],
|
||||
})
|
||||
class ForeignHost {
|
||||
readonly title = input.required<string>();
|
||||
}
|
||||
|
||||
@Component({
|
||||
imports: [ForeignHost],
|
||||
template: `<foreign-host [title]="parentTitle()" />`,
|
||||
})
|
||||
class App {
|
||||
readonly parentTitle = signal('Required Title');
|
||||
}
|
||||
|
||||
const fixture = TestBed.createComponent(App);
|
||||
await fixture.whenStable();
|
||||
|
||||
const span = fixture.nativeElement.querySelector('#title-display');
|
||||
expect(span).toBeTruthy();
|
||||
expect(span.textContent).toBe('Required Title');
|
||||
});
|
||||
|
||||
it('should support passing raw input.required signal to foreign component props and track reactively', async () => {
|
||||
@Component({
|
||||
selector: 'foreign-host',
|
||||
template: `<ReactiveTitleWidget [title]="title" [injector]="injector" />`,
|
||||
// @ts-ignore
|
||||
foreignImports: [frameworkImport(ReactiveTitleWidget)],
|
||||
})
|
||||
class ForeignHost {
|
||||
readonly title = input.required<string>();
|
||||
readonly injector = inject(Injector);
|
||||
}
|
||||
|
||||
@Component({
|
||||
imports: [ForeignHost],
|
||||
template: `<foreign-host [title]="parentTitle()" />`,
|
||||
})
|
||||
class App {
|
||||
readonly parentTitle = signal('Initial Title');
|
||||
}
|
||||
|
||||
const fixture = TestBed.createComponent(App);
|
||||
await fixture.whenStable();
|
||||
|
||||
const span = fixture.nativeElement.querySelector('#title-display');
|
||||
expect(span).toBeTruthy();
|
||||
expect(span.textContent).toBe('Initial Title');
|
||||
|
||||
fixture.componentInstance.parentTitle.set('Updated Title');
|
||||
await fixture.whenStable();
|
||||
|
||||
expect(span.textContent).toBe('Updated Title');
|
||||
});
|
||||
|
||||
it('should support passing @Input() properties to foreign component props', async () => {
|
||||
@Component({
|
||||
selector: 'foreign-input-host',
|
||||
template: `<TitleWidget [title]="title" />`,
|
||||
// @ts-ignore
|
||||
foreignImports: [frameworkImport(TitleWidget)],
|
||||
})
|
||||
class ForeignInputHost {
|
||||
@Input() title = 'default';
|
||||
}
|
||||
|
||||
@Component({
|
||||
imports: [ForeignInputHost],
|
||||
template: `<foreign-input-host [title]="parentTitle" />`,
|
||||
})
|
||||
class App {
|
||||
parentTitle = 'Passed from Parent';
|
||||
}
|
||||
|
||||
const fixture = TestBed.createComponent(App);
|
||||
await fixture.whenStable();
|
||||
|
||||
const span = fixture.nativeElement.querySelector('#title-display');
|
||||
expect(span).toBeTruthy();
|
||||
expect(span.textContent).toBe('Passed from Parent');
|
||||
});
|
||||
|
||||
it('should support passing ngOnInit initialized properties to foreign component props', async () => {
|
||||
@Component({
|
||||
template: `<TitleWidget [title]="title" />`,
|
||||
// @ts-ignore
|
||||
foreignImports: [frameworkImport(TitleWidget)],
|
||||
})
|
||||
class App implements OnInit {
|
||||
title!: string;
|
||||
|
||||
ngOnInit() {
|
||||
this.title = 'Initialized in ngOnInit';
|
||||
}
|
||||
}
|
||||
|
||||
const fixture = TestBed.createComponent(App);
|
||||
await fixture.whenStable();
|
||||
|
||||
const span = fixture.nativeElement.querySelector('#title-display');
|
||||
expect(span).toBeTruthy();
|
||||
expect(span.textContent).toBe('Initialized in ngOnInit');
|
||||
});
|
||||
|
||||
it('should support passing viewChild query results to foreign component props', async () => {
|
||||
@Component({
|
||||
template: `
|
||||
<div #myDiv id="my-div">Hello</div>
|
||||
<QueryWidget [target]="myDivRef()" />
|
||||
`,
|
||||
// @ts-ignore
|
||||
foreignImports: [frameworkImport(QueryWidget)],
|
||||
})
|
||||
class App {
|
||||
readonly myDivRef = viewChild<ElementRef>('myDiv');
|
||||
}
|
||||
|
||||
const fixture = TestBed.createComponent(App);
|
||||
await fixture.whenStable();
|
||||
|
||||
const span = fixture.nativeElement.querySelector('#query-display');
|
||||
expect(span).toBeTruthy();
|
||||
expect(span.textContent).toBe('HAS_TARGET');
|
||||
});
|
||||
|
||||
it('should support passing viewChild.required query results to foreign component props', async () => {
|
||||
@Component({
|
||||
template: `
|
||||
<div #myDiv id="my-div">Hello</div>
|
||||
<QueryWidget [target]="myDivRef()" />
|
||||
`,
|
||||
// @ts-ignore
|
||||
foreignImports: [frameworkImport(QueryWidget)],
|
||||
})
|
||||
class App {
|
||||
readonly myDivRef = viewChild.required<ElementRef>('myDiv');
|
||||
}
|
||||
|
||||
const fixture = TestBed.createComponent(App);
|
||||
await fixture.whenStable();
|
||||
|
||||
const span = fixture.nativeElement.querySelector('#query-display');
|
||||
expect(span).toBeTruthy();
|
||||
expect(span.textContent).toBe('HAS_TARGET');
|
||||
});
|
||||
|
||||
it('should support passing raw viewChild signal to foreign component props and track reactively when query changes', async () => {
|
||||
@Component({
|
||||
template: `
|
||||
@if (showDiv()) {
|
||||
<div #myDiv id="my-div">Hello</div>
|
||||
}
|
||||
<ReactiveQueryWidget [target]="myDivRef" [injector]="injector" />
|
||||
`,
|
||||
// @ts-ignore
|
||||
foreignImports: [frameworkImport(ReactiveQueryWidget)],
|
||||
})
|
||||
class App {
|
||||
readonly showDiv = signal(true);
|
||||
readonly myDivRef = viewChild<ElementRef>('myDiv');
|
||||
readonly injector = inject(Injector);
|
||||
}
|
||||
|
||||
const fixture = TestBed.createComponent(App);
|
||||
await fixture.whenStable();
|
||||
|
||||
const span = fixture.nativeElement.querySelector('#query-display');
|
||||
expect(span).toBeTruthy();
|
||||
expect(span.textContent).toBe('HAS_TARGET');
|
||||
|
||||
fixture.componentInstance.showDiv.set(false);
|
||||
await fixture.whenStable();
|
||||
|
||||
expect(span.textContent).toBe('NO_TARGET');
|
||||
|
||||
fixture.componentInstance.showDiv.set(true);
|
||||
await fixture.whenStable();
|
||||
|
||||
expect(span.textContent).toBe('HAS_TARGET');
|
||||
});
|
||||
|
||||
it('should support passing raw viewChild.required signal to foreign component props and track reactively', async () => {
|
||||
@Component({
|
||||
template: `
|
||||
<div #myDiv id="my-div">Hello</div>
|
||||
<ReactiveQueryWidget [target]="myDivRef" [injector]="injector" />
|
||||
`,
|
||||
// @ts-ignore
|
||||
foreignImports: [frameworkImport(ReactiveQueryWidget)],
|
||||
})
|
||||
class App {
|
||||
readonly myDivRef = viewChild.required<ElementRef>('myDiv');
|
||||
readonly injector = inject(Injector);
|
||||
}
|
||||
|
||||
const fixture = TestBed.createComponent(App);
|
||||
await fixture.whenStable();
|
||||
|
||||
const span = fixture.nativeElement.querySelector('#query-display');
|
||||
expect(span).toBeTruthy();
|
||||
expect(span.textContent).toBe('HAS_TARGET');
|
||||
});
|
||||
|
||||
it('should attach foreign component DOM before ngAfterViewInit', async () => {
|
||||
let textInAfterViewInit = '';
|
||||
|
||||
@Component({
|
||||
selector: 'test-cmp',
|
||||
template: `<TitleWidget title="Rendered Before ViewInit" />`,
|
||||
// @ts-ignore
|
||||
foreignImports: [frameworkImport(TitleWidget)],
|
||||
})
|
||||
class TestCmp implements AfterViewInit {
|
||||
private readonly elementRef = inject(ElementRef);
|
||||
|
||||
ngAfterViewInit() {
|
||||
const span = this.elementRef.nativeElement.querySelector('#title-display');
|
||||
textInAfterViewInit = span?.textContent ?? '';
|
||||
}
|
||||
}
|
||||
|
||||
const fixture = TestBed.createComponent(TestCmp);
|
||||
await fixture.whenStable();
|
||||
|
||||
expect(textInAfterViewInit).toBe('Rendered Before ViewInit');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -20,6 +20,7 @@ import {ɵɵtext} from '../../src/render3/instructions/text';
|
||||
import {ɵɵadvance} from '../../src/render3/instructions/advance';
|
||||
import {ɵɵtextInterpolate2} from '../../src/render3/instructions/text_interpolation';
|
||||
import {createLView} from '../../src/render3/view/construction';
|
||||
import {refreshView} from '../../src/render3/instructions/change_detection';
|
||||
import {renderView} from '../../src/render3/instructions/render';
|
||||
import {LView, LViewFlags, PARENT, RENDERER, T_HOST} from '../../src/render3/interfaces/view';
|
||||
|
||||
@@ -46,6 +47,7 @@ describe('ɵɵforeignComponent', () => {
|
||||
ɵɵforeignComponent(0, 0);
|
||||
},
|
||||
});
|
||||
fixture.update(() => {});
|
||||
|
||||
expect(fixture.host.innerHTML).toContain('<div id="foreign-el">Foreign Content</div>');
|
||||
});
|
||||
@@ -54,16 +56,43 @@ describe('ɵɵforeignComponent', () => {
|
||||
const render = jasmine.createSpy('render').and.returnValue([[]]);
|
||||
const foreignComp = foreignImport<{name: string}>(render, noopOnDestroy, eagerContentAdapter);
|
||||
|
||||
new ViewFixture({
|
||||
const fixture = new ViewFixture({
|
||||
decls: 1,
|
||||
vars: 0,
|
||||
consts: [foreignComp],
|
||||
create: () => {
|
||||
ɵɵforeignComponent(0, 0, {name: 'Angular'});
|
||||
ɵɵforeignComponent(0, 0, () => ({name: 'Angular'}));
|
||||
},
|
||||
});
|
||||
expect(render).not.toHaveBeenCalled();
|
||||
|
||||
fixture.update(() => {});
|
||||
expect(render).toHaveBeenCalledOnceWith({name: 'Angular'}, /* context= */ undefined);
|
||||
});
|
||||
|
||||
it('should support passing props as a factory function and evaluate during update pass', () => {
|
||||
const render = jasmine.createSpy('render').and.returnValue([[]]);
|
||||
const propsFactory = jasmine
|
||||
.createSpy('propsFactory')
|
||||
.and.returnValue({name: 'Deferred Angular'});
|
||||
const foreignComp = foreignImport<{name: string}>(render, noopOnDestroy, eagerContentAdapter);
|
||||
|
||||
const fixture = new ViewFixture({
|
||||
decls: 1,
|
||||
vars: 0,
|
||||
consts: [foreignComp],
|
||||
create: () => {
|
||||
ɵɵforeignComponent(0, 0, propsFactory);
|
||||
},
|
||||
});
|
||||
|
||||
expect(render).toHaveBeenCalledOnceWith({name: 'Angular'}, /* context= */ undefined);
|
||||
expect(propsFactory).not.toHaveBeenCalled();
|
||||
expect(render).not.toHaveBeenCalled();
|
||||
|
||||
fixture.update(() => {});
|
||||
|
||||
expect(propsFactory).toHaveBeenCalledTimes(1);
|
||||
expect(render).toHaveBeenCalledOnceWith({name: 'Deferred Angular'}, /* context= */ undefined);
|
||||
});
|
||||
|
||||
it('should call the dispose function when the containing view is destroyed', () => {
|
||||
@@ -84,6 +113,7 @@ describe('ɵɵforeignComponent', () => {
|
||||
ɵɵforeignComponent(0, 0);
|
||||
},
|
||||
});
|
||||
fixture.update(() => {});
|
||||
|
||||
expect(dispose).toHaveBeenCalledTimes(0);
|
||||
|
||||
@@ -113,6 +143,7 @@ describe('ɵɵforeignComponent', () => {
|
||||
ɵɵelement(2, 'span');
|
||||
},
|
||||
});
|
||||
fixture.update(() => {});
|
||||
|
||||
expect(fixture.host.innerHTML).toContain(
|
||||
'' +
|
||||
@@ -146,6 +177,7 @@ describe('ɵɵforeignComponent', () => {
|
||||
ɵɵelementEnd();
|
||||
},
|
||||
});
|
||||
fixture.update(() => {});
|
||||
|
||||
expect(fixture.host.innerHTML).toContain(
|
||||
'' +
|
||||
@@ -185,6 +217,7 @@ describe('ɵɵforeignComponent', () => {
|
||||
consts: [foreignComp1],
|
||||
create: createFn,
|
||||
});
|
||||
fixture.update(() => {});
|
||||
expect(fixture.host.innerHTML).toContain(expectedHtml);
|
||||
|
||||
// Create second instance reusing the TView
|
||||
@@ -262,13 +295,17 @@ describe('ɵɵforeignComponent', () => {
|
||||
ɵɵdomTemplate(0, iconTemplate, 2, 0);
|
||||
ɵɵdomTemplate(1, descriptionTemplate, 2, 0);
|
||||
ɵɵdomTemplate(2, childrenTemplate, 2, 0);
|
||||
ɵɵforeignComponent(3, 0, {
|
||||
icon: ɵɵforeignContent(0, 0),
|
||||
description: ɵɵforeignContent(1, 0),
|
||||
children: ɵɵforeignContent(2, 0),
|
||||
});
|
||||
const iconContent = ɵɵforeignContent(0, 0);
|
||||
const descriptionContent = ɵɵforeignContent(1, 0);
|
||||
const childrenContent = ɵɵforeignContent(2, 0);
|
||||
ɵɵforeignComponent(3, 0, () => ({
|
||||
icon: iconContent,
|
||||
description: descriptionContent,
|
||||
children: childrenContent,
|
||||
}));
|
||||
},
|
||||
});
|
||||
fixture.update(() => {});
|
||||
|
||||
expect(fixture.host.innerHTML).toContain(
|
||||
'' +
|
||||
@@ -324,9 +361,10 @@ describe('ɵɵforeignComponent', () => {
|
||||
consts: [foreignComp],
|
||||
create: () => {
|
||||
ɵɵdomTemplate(0, itemTemplate, 2, 2);
|
||||
ɵɵforeignComponent(1, 0, {
|
||||
renderItem: ɵɵforeignContentFn(0, 0),
|
||||
});
|
||||
const renderItem = ɵɵforeignContentFn(0, 0);
|
||||
ɵɵforeignComponent(1, 0, () => ({
|
||||
renderItem,
|
||||
}));
|
||||
},
|
||||
});
|
||||
|
||||
@@ -365,5 +403,6 @@ function renderSecondInstance(fixture: ViewFixture): HTMLElement {
|
||||
);
|
||||
|
||||
renderView(fixture.tView, lView, {});
|
||||
refreshView(fixture.tView, lView, fixture.tView.template, {});
|
||||
return host;
|
||||
}
|
||||
|
||||
@@ -115,7 +115,7 @@ export class ViewFixture {
|
||||
{
|
||||
rendererFactory,
|
||||
sanitizer: sanitizer || null,
|
||||
changeDetectionScheduler: null,
|
||||
changeDetectionScheduler: {notify: noop, runningTick: false},
|
||||
ngReflect: false,
|
||||
tracingService: null,
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user