refactor(core): add on immediate support for defer blocks (#51630)

This commit adds a logic to handle `on immediate` conditions both as a main condition, as well as a prefetching condition (i.e. `prefetch on immediate`).

PR Close #51630
This commit is contained in:
Andrew Kushnir 2023-09-01 13:03:34 -07:00 committed by Dylan Hunn
parent 16f5fc40a4
commit baaaa6daf6
2 changed files with 211 additions and 14 deletions

View file

@ -158,7 +158,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() {
@ -174,7 +174,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() {
@ -202,17 +202,39 @@ 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);
// Render placeholder block only if loading template is not present
// to avoid content flickering, 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, lView);
}
}
/**
* Creates runtime data structures for the `on timer` deferred trigger.

View file

@ -44,6 +44,28 @@ function onIdle(callback: () => Promise<void>): Promise<void> {
});
}
/**
* 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<T>(type: T): Promise<T> {
return new Promise<T>(resolve => {
setTimeout(() => resolve(type));
});
}
/**
* Emulates a failed dynamic import promise.
*/
function failedDynamicImport(): Promise<void> {
return new Promise((_, reject) => {
setTimeout(() => reject());
});
}
// 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}];
@ -142,6 +164,81 @@ describe('#defer', () => {
expect(fixture.nativeElement.outerHTML).toContain('<my-lazy-cmp>Hi!</my-lazy-cmp>');
});
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}
<nested-cmp [block]="'primary'" />
{:placeholder}
Placeholder
{:loading}
Loading
{/defer}
`
})
class RootCmp {
}
let loadingFnInvokedTimes = 0;
const deferDepsInterceptor = {
intercept() {
return () => {
loadingFnInvokedTimes++;
return [dynamicImportOf(NestedCmp)];
};
}
};
TestBed.configureTestingModule({
providers: [
...COMMON_PROVIDERS,
{provide: ɵDEFER_BLOCK_DEPENDENCY_INTERCEPTOR, useValue: deferDepsInterceptor},
],
deferBlockBehavior: DeferBlockBehavior.Playthrough,
});
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(
'<nested-cmp ng-reflect-block="primary">Rendering primary block.</nested-cmp>');
// 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 () => {
@ -236,8 +333,7 @@ describe('#defer', () => {
const deferDepsInterceptor = {
intercept() {
// Simulate loading failure.
return () => [Promise.reject()];
return () => [failedDynamicImport()];
}
};
@ -561,7 +657,7 @@ describe('#defer', () => {
intercept() {
return () => {
loadingFnInvokedTimes++;
return [Promise.resolve(NestedCmp)];
return [dynamicImportOf(NestedCmp)];
};
}
};
@ -646,7 +742,7 @@ describe('#defer', () => {
intercept() {
return () => {
loadingFnInvokedTimes++;
return [Promise.reject()];
return [failedDynamicImport()];
};
}
};
@ -727,7 +823,7 @@ describe('#defer', () => {
intercept() {
return () => {
loadingFnInvokedTimes++;
return [Promise.resolve(NestedCmp)];
return [dynamicImportOf(NestedCmp)];
};
}
};
@ -796,7 +892,7 @@ describe('#defer', () => {
intercept() {
return () => {
loadingFnInvokedTimes++;
return [Promise.resolve(NestedCmp)];
return [dynamicImportOf(NestedCmp)];
};
}
};
@ -881,7 +977,7 @@ describe('#defer', () => {
intercept() {
return () => {
loadingFnInvokedTimes++;
return [Promise.resolve(NestedCmp)];
return [dynamicImportOf(NestedCmp)];
};
}
};
@ -966,7 +1062,7 @@ describe('#defer', () => {
intercept() {
return () => {
loadingFnInvokedTimes++;
return [Promise.resolve(NestedCmp)];
return [dynamicImportOf(NestedCmp)];
};
}
};
@ -1008,6 +1104,85 @@ 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}
<nested-cmp [block]="'primary'" />
{: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},
],
deferBlockBehavior: DeferBlockBehavior.Playthrough,
});
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(
'<nested-cmp ng-reflect-block="primary">Rendering primary block.</nested-cmp>');
// Expect that the loading resources function was not invoked again (counter remains 1).
expect(loadingFnInvokedTimes).toBe(1);
});
});
// Note: these cases specifically use `on interaction`, however