Skip to content

Commit

Permalink
refactor(core): add on immediate support for defer blocks
Browse files Browse the repository at this point in the history
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`).
  • Loading branch information
AndrewKushnir committed Sep 10, 2023
1 parent 05762b9 commit 4dd321c
Show file tree
Hide file tree
Showing 2 changed files with 202 additions and 14 deletions.
33 changes: 27 additions & 6 deletions packages/core/src/render3/instructions/defer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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() {
Expand All @@ -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() {
Expand All @@ -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.
Expand Down
183 changes: 175 additions & 8 deletions packages/core/test/acceptance/defer_spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,23 @@ 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(type: Type<unknown>): Promise<Type<unknown>> {
return new Promise(resolve => {
setTimeout(() => resolve(type), 0);
});
}

// Emulates a failed dynamic import promise.
function failedDynamicImport(): Promise<void> {
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}];
Expand Down Expand Up @@ -141,6 +158,79 @@ 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: [
{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(
'<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 () => {
Expand Down Expand Up @@ -235,8 +325,7 @@ describe('#defer', () => {

const deferDepsInterceptor = {
intercept() {
// Simulate loading failure.
return () => [Promise.reject()];
return () => [failedDynamicImport()];
}
};

Expand Down Expand Up @@ -559,7 +648,7 @@ describe('#defer', () => {
intercept() {
return () => {
loadingFnInvokedTimes++;
return [Promise.resolve(NestedCmp)];
return [dynamicImportOf(NestedCmp)];
};
}
};
Expand Down Expand Up @@ -643,7 +732,7 @@ describe('#defer', () => {
intercept() {
return () => {
loadingFnInvokedTimes++;
return [Promise.reject()];
return [failedDynamicImport()];
};
}
};
Expand Down Expand Up @@ -723,7 +812,7 @@ describe('#defer', () => {
intercept() {
return () => {
loadingFnInvokedTimes++;
return [Promise.resolve(NestedCmp)];
return [dynamicImportOf(NestedCmp)];
};
}
};
Expand Down Expand Up @@ -791,7 +880,7 @@ describe('#defer', () => {
intercept() {
return () => {
loadingFnInvokedTimes++;
return [Promise.resolve(NestedCmp)];
return [dynamicImportOf(NestedCmp)];
};
}
};
Expand Down Expand Up @@ -875,7 +964,7 @@ describe('#defer', () => {
intercept() {
return () => {
loadingFnInvokedTimes++;
return [Promise.resolve(NestedCmp)];
return [dynamicImportOf(NestedCmp)];
};
}
};
Expand Down Expand Up @@ -959,7 +1048,7 @@ describe('#defer', () => {
intercept() {
return () => {
loadingFnInvokedTimes++;
return [Promise.resolve(NestedCmp)];
return [dynamicImportOf(NestedCmp)];
};
}
};
Expand Down Expand Up @@ -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}
<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},
]
});

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);
});
});
});

0 comments on commit 4dd321c

Please sign in to comment.