mirror of
https://github.com/angular/angular
synced 2026-05-24 09:28:37 +00:00
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:
parent
16f5fc40a4
commit
baaaa6daf6
2 changed files with 211 additions and 14 deletions
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Reference in a new issue