diff --git a/packages/core/src/render3/instructions/defer.ts b/packages/core/src/render3/instructions/defer.ts index a97b6b033f7d9e..5e71eaa94b837f 100644 --- a/packages/core/src/render3/instructions/defer.ts +++ b/packages/core/src/render3/instructions/defer.ts @@ -153,7 +153,7 @@ export function ɵɵdeferPrefetchWhen(rawValue: unknown) { } /** - * Sets up handlers that represent `on idle` deferred trigger. + * Sets up logic to handle the `on idle` deferred trigger. * @codeGenApi */ export function ɵɵdeferOnIdle() { @@ -169,7 +169,7 @@ export function ɵɵdeferOnIdle() { } /** - * Creates runtime data structures for the `prefetch on idle` deferred trigger. + * Sets up logic to handle the `prefetch on idle` deferred trigger. * @codeGenApi */ export function ɵɵdeferPrefetchOnIdle() { @@ -191,17 +191,38 @@ export function ɵɵdeferPrefetchOnIdle() { } /** - * Creates runtime data structures for the `on immediate` deferred trigger. + * Sets up logic to handle the `on immediate` deferred trigger. * @codeGenApi */ -export function ɵɵdeferOnImmediate() {} // TODO: implement runtime logic. +export function ɵɵdeferOnImmediate() { + const lView = getLView(); + const tNode = getCurrentTNode()!; + const tView = lView[TVIEW]; + const tDetails = getTDeferBlockDetails(tView, tNode); + + // If loading template is present - skip rendering of a placeholder + // block, since it would be immediately replaced by the loading block. + if (tDetails.loadingTmplIndex !== null) { + renderPlaceholder(lView, tNode); + } + triggerDeferBlock(lView, tNode); +} /** - * Creates runtime data structures for the `prefetch on immediate` deferred trigger. + * Sets up logic to handle the `prefetch on immediate` deferred trigger. * @codeGenApi */ -export function ɵɵdeferPrefetchOnImmediate() {} // TODO: implement runtime logic. +export function ɵɵdeferPrefetchOnImmediate() { + const lView = getLView(); + const tNode = getCurrentTNode()!; + const tView = lView[TVIEW]; + const tDetails = getTDeferBlockDetails(tView, tNode); + + if (tDetails.loadingState === DeferDependenciesLoadingState.NOT_STARTED) { + triggerResourceLoading(tDetails, tView, lView); + } +} /** * Creates runtime data structures for the `on timer` deferred trigger. diff --git a/packages/core/test/acceptance/defer_spec.ts b/packages/core/test/acceptance/defer_spec.ts index fa7b3b673b5a85..9d515ac1fb2e74 100644 --- a/packages/core/test/acceptance/defer_spec.ts +++ b/packages/core/test/acceptance/defer_spec.ts @@ -44,6 +44,23 @@ function onIdle(callback: () => Promise): Promise { }); } +// Emulates a dynamic import promise. +// Note: `setTimeout` is used to make `fixture.whenStable()` function +// wait for promise resolution, since `whenStable()` relies on the state +// of a macrotask queue. +function dynamicImportOf(type: Type): Promise> { + return new Promise(resolve => { + setTimeout(() => resolve(type), 0); + }); +} + +// Emulates a failed dynamic import promise. +function failedDynamicImport(): Promise { + return new Promise((_, reject) => { + setTimeout(() => reject(), 0); + }); +} + // Set `PLATFORM_ID` to a browser platform value to trigger defer loading // while running tests in Node. const COMMON_PROVIDERS = [{provide: PLATFORM_ID, useValue: PLATFORM_BROWSER_ID}]; @@ -141,6 +158,79 @@ describe('#defer', () => { expect(fixture.nativeElement.outerHTML).toContain('Hi!'); }); + describe('`on` conditions', () => { + it('should support `on immediate` condition', async () => { + @Component({ + selector: 'nested-cmp', + standalone: true, + template: 'Rendering {{ block }} block.', + }) + class NestedCmp { + @Input() block!: string; + } + + @Component({ + standalone: true, + selector: 'root-app', + imports: [NestedCmp], + template: ` + {#defer on immediate} + + {:placeholder} + Placeholder + {:loading} + Loading + {/defer} + ` + }) + class RootCmp { + } + + let loadingFnInvokedTimes = 0; + const deferDepsInterceptor = { + intercept() { + return () => { + loadingFnInvokedTimes++; + return [dynamicImportOf(NestedCmp)]; + }; + } + }; + + TestBed.configureTestingModule({ + providers: [ + {provide: ɵDEFER_BLOCK_DEPENDENCY_INTERCEPTOR, useValue: deferDepsInterceptor}, + ] + }); + + clearDirectiveDefs(RootCmp); + + const fixture = TestBed.createComponent(RootCmp); + fixture.detectChanges(); + + // Expecting that no placeholder content would be rendered when + // a `{:loading}` block is present. + expect(fixture.nativeElement.outerHTML).toContain('Loading'); + + // Expecting loading function to be triggered right away. + expect(loadingFnInvokedTimes).toBe(1); + + await fixture.whenStable(); // loading dependencies of the defer block + fixture.detectChanges(); + + // Expect that the loading resources function was not invoked again. + expect(loadingFnInvokedTimes).toBe(1); + + // Verify primary block content. + const primaryBlockHTML = fixture.nativeElement.outerHTML; + expect(primaryBlockHTML) + .toContain( + 'Rendering primary block.'); + + // Expect that the loading resources function was not invoked again (counter remains 1). + expect(loadingFnInvokedTimes).toBe(1); + }); + }); + describe('directive matching', () => { it('should support directive matching in all blocks', async () => { @@ -235,8 +325,7 @@ describe('#defer', () => { const deferDepsInterceptor = { intercept() { - // Simulate loading failure. - return () => [Promise.reject()]; + return () => [failedDynamicImport()]; } }; @@ -559,7 +648,7 @@ describe('#defer', () => { intercept() { return () => { loadingFnInvokedTimes++; - return [Promise.resolve(NestedCmp)]; + return [dynamicImportOf(NestedCmp)]; }; } }; @@ -643,7 +732,7 @@ describe('#defer', () => { intercept() { return () => { loadingFnInvokedTimes++; - return [Promise.reject()]; + return [failedDynamicImport()]; }; } }; @@ -723,7 +812,7 @@ describe('#defer', () => { intercept() { return () => { loadingFnInvokedTimes++; - return [Promise.resolve(NestedCmp)]; + return [dynamicImportOf(NestedCmp)]; }; } }; @@ -791,7 +880,7 @@ describe('#defer', () => { intercept() { return () => { loadingFnInvokedTimes++; - return [Promise.resolve(NestedCmp)]; + return [dynamicImportOf(NestedCmp)]; }; } }; @@ -875,7 +964,7 @@ describe('#defer', () => { intercept() { return () => { loadingFnInvokedTimes++; - return [Promise.resolve(NestedCmp)]; + return [dynamicImportOf(NestedCmp)]; }; } }; @@ -959,7 +1048,7 @@ describe('#defer', () => { intercept() { return () => { loadingFnInvokedTimes++; - return [Promise.resolve(NestedCmp)]; + return [dynamicImportOf(NestedCmp)]; }; } }; @@ -1000,5 +1089,83 @@ describe('#defer', () => { expect(loadingFnInvokedTimes).toBe(1); }); }); + + it('should support `prefetch on immediate` condition', async () => { + @Component({ + selector: 'nested-cmp', + standalone: true, + template: 'Rendering {{ block }} block.', + }) + class NestedCmp { + @Input() block!: string; + } + + @Component({ + standalone: true, + selector: 'root-app', + imports: [NestedCmp], + template: ` + {#defer when deferCond; prefetch on immediate} + + {:placeholder} + Placeholder + {/defer} + ` + }) + class RootCmp { + deferCond = false; + } + + let loadingFnInvokedTimes = 0; + const deferDepsInterceptor = { + intercept() { + return () => { + loadingFnInvokedTimes++; + return [dynamicImportOf(NestedCmp)] + }; + } + }; + + TestBed.configureTestingModule({ + providers: [ + ...COMMON_PROVIDERS, + {provide: ɵDEFER_BLOCK_DEPENDENCY_INTERCEPTOR, useValue: deferDepsInterceptor}, + ] + }); + + clearDirectiveDefs(RootCmp); + + const fixture = TestBed.createComponent(RootCmp); + fixture.detectChanges(); + + expect(fixture.nativeElement.outerHTML).toContain('Placeholder'); + + // Expecting loading function to be triggered right away. + expect(loadingFnInvokedTimes).toBe(1); + + await fixture.whenStable(); // prefetching dependencies of the defer block + fixture.detectChanges(); + + // Expect that the loading resources function was invoked once. + expect(loadingFnInvokedTimes).toBe(1); + + // Expect that placeholder content is still rendered. + expect(fixture.nativeElement.outerHTML).toContain('Placeholder'); + + // Trigger main content. + fixture.componentInstance.deferCond = true; + fixture.detectChanges(); + + await fixture.whenStable(); + + // Verify primary block content. + const primaryBlockHTML = fixture.nativeElement.outerHTML; + expect(primaryBlockHTML) + .toContain( + 'Rendering primary block.'); + + // Expect that the loading resources function was not invoked again (counter remains 1). + expect(loadingFnInvokedTimes).toBe(1); + }); }); });