refactor(core): initial implementation of {#defer} block runtime (#51347)

This commit adds an initial implementation of the `{#defer}` block runtime, which supports the `when` conditions. More conditions and basic prefetching support will be added in followup PRs.

PR Close #51347
This commit is contained in:
Andrew Kushnir 2023-08-09 18:40:10 -07:00 committed by Jessica Janiuk
parent bf9663847d
commit c4deaac5b0
15 changed files with 1062 additions and 40 deletions

View file

@ -13,6 +13,7 @@ MyApp.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "0.0.0-
<div>
{{message}}
{#defer}Deferred content{/defer}
<p>Content after defer block</p>
</div>
`, isInline: true });
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "0.0.0-PLACEHOLDER", ngImport: i0, type: MyApp, decorators: [{
@ -22,6 +23,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "0.0.0-PLACEHOLDE
<div>
{{message}}
{#defer}Deferred content{/defer}
<p>Content after defer block</p>
</div>
`,
}]

View file

@ -5,6 +5,7 @@ import {Component} from '@angular/core';
<div>
{{message}}
{#defer}Deferred content{/defer}
<p>Content after defer block</p>
</div>
`,
})

View file

@ -13,7 +13,9 @@ MyApp.ɵcmp = /*@__PURE__*/ $r3$.ɵɵdefineComponent({
$r3$.ɵɵtemplate(2, MyApp_Defer_2_Template, 1, 0);
$r3$.ɵɵdefer(3, 2);
$r3$.ɵɵdeferOnIdle();
$r3$.ɵɵelementEnd();
$r3$.ɵɵelementStart(5, "p");
$r3$.ɵɵtext(6, "Content after defer block");
$r3$.ɵɵelementEnd()();
}
if (rf & 2) {
$r3$.ɵɵadvance(1);

View file

@ -1300,6 +1300,10 @@ export class TemplateDefinitionBuilder implements t.Visitor<void>, LocalResolver
this.createDeferTriggerInstructions(deferredIndex, triggers, false);
this.createDeferTriggerInstructions(deferredIndex, prefetchTriggers, true);
// Allocate an extra data slot right after a defer block slot to store
// instance-specific state of that defer block at runtime.
this.allocateDataSlot();
}
private createDeferredDepsFunction(name: string, deferred: t.DeferredBlock) {

View file

@ -68,6 +68,8 @@ export {
setClassMetadataAsync as ɵsetClassMetadataAsync,
setLocaleId as ɵsetLocaleId,
store as ɵstore,
ɵDeferBlockDependencyInterceptor,
ɵDEFER_BLOCK_DEPENDENCY_INTERCEPTOR,
ɵɵadvance,
ɵɵattribute,
ɵɵattributeInterpolate1,

View file

@ -111,8 +111,7 @@ export function assertDirectiveDef<T>(obj: any): asserts obj is DirectiveDef<T>
}
}
export function assertIndexInDeclRange(lView: LView, index: number) {
const tView = lView[1];
export function assertIndexInDeclRange(tView: TView, index: number) {
assertBetween(HEADER_OFFSET, tView.bindingStartIndex, index);
}

View file

@ -151,7 +151,10 @@ export {
ɵgetUnknownElementStrictMode,
ɵsetUnknownElementStrictMode,
ɵgetUnknownPropertyStrictMode,
ɵsetUnknownPropertyStrictMode
ɵsetUnknownPropertyStrictMode,
DeferBlockDependencyInterceptor as ɵDeferBlockDependencyInterceptor,
DEFER_BLOCK_DEPENDENCY_INTERCEPTOR as ɵDEFER_BLOCK_DEPENDENCY_INTERCEPTOR,
} from './instructions/all';
export {ɵɵi18n, ɵɵi18nApply, ɵɵi18nAttributes, ɵɵi18nEnd, ɵɵi18nExp,ɵɵi18nPostprocess, ɵɵi18nStart} from './instructions/i18n';
export {RenderFlags} from './interfaces/definition';

View file

@ -8,7 +8,7 @@
import {assertGreaterThan} from '../../util/assert';
import {assertIndexInDeclRange} from '../assert';
import {executeCheckHooks, executeInitAndCheckHooks} from '../hooks';
import {FLAGS, InitPhaseState, LView, LViewFlags, TView} from '../interfaces/view';
import {FLAGS, InitPhaseState, LView, LViewFlags, TVIEW, TView} from '../interfaces/view';
import {getLView, getSelectedIndex, getTView, isInCheckNoChangesMode, setSelectedIndex} from '../state';
@ -43,7 +43,7 @@ export function ɵɵadvance(delta: number): void {
export function selectIndexInternal(
tView: TView, lView: LView, index: number, checkNoChangesMode: boolean) {
ngDevMode && assertIndexInDeclRange(lView, index);
ngDevMode && assertIndexInDeclRange(lView[TVIEW], index);
// Flush the initial hooks for elements in the view that have been added up to this point.
// PERF WARNING: do NOT extract this to a separate function without running benchmarks

View file

@ -6,26 +6,43 @@
* found in the LICENSE file at https://angular.io/license
*/
import {Type} from '../../interface/type';
import {InjectionToken, Injector} from '../../di';
import {assertDefined, assertEqual, throwError} from '../../util/assert';
import {assertIndexInDeclRange, assertLContainer, assertTNodeForLView} from '../assert';
import {bindingUpdated} from '../bindings';
import {getComponentDef, getDirectiveDef, getPipeDef} from '../definition';
import {LContainer} from '../interfaces/container';
import {DEFER_BLOCK_STATE, DeferBlockInstanceState, DeferDependenciesLoadingState, DeferredLoadingBlockConfig, DeferredPlaceholderBlockConfig, DependencyResolverFn, LDeferBlockDetails, TDeferBlockDetails} from '../interfaces/defer';
import {DirectiveDefList, PipeDefList} from '../interfaces/definition';
import {TContainerNode, TNode} from '../interfaces/node';
import {isDestroyed} from '../interfaces/type_checks';
import {HEADER_OFFSET, INJECTOR, LView, PARENT, TVIEW, TView} from '../interfaces/view';
import {getCurrentTNode, getLView, getSelectedTNode, getTView, nextBindingIndex} from '../state';
import {NO_CHANGE} from '../tokens';
import {getConstant, getTNode, removeLViewOnDestroy, storeLViewOnDestroy} from '../util/view_utils';
import {addLViewToLContainer, createAndRenderEmbeddedLView, removeLViewFromLContainer} from '../view_manipulation';
export type DeferredDepsFn = () => Array<Promise<Type<unknown>>|Type<unknown>>;
import {ɵɵtemplate} from './template';
/** Configuration object for a `{:loading}` block as it is stored in the component constants. */
type DeferredLoadingConfig = [minimumTime: number|null, afterTime: number|null];
/** Configuration object for a `{:placeholder}` block as it is stored in the component constants. */
type DeferredPlaceholderConfig = [afterTime: number|null];
/**
* Shims for the `requestIdleCallback` and `cancelIdleCallback` functions for environments
* where those functions are not available (e.g. Node.js).
*/
const _requestIdleCallback =
typeof requestIdleCallback !== 'undefined' ? requestIdleCallback : setTimeout;
const _cancelIdleCallback =
typeof requestIdleCallback !== 'undefined' ? cancelIdleCallback : clearTimeout;
/**
* Creates runtime data structures for `{#defer}` blocks.
*
* @param deferIndex Index of the underlying deferred block data structure.
* @param primaryTemplateIndex Index of the template function with the block's content.
* @param deferredDepsFn Function that contains dependencies for this defer block
* @param loadingIndex Index of the template with the `{:loading}` block content.
* @param placeholderIndex Index of the template with the `{:placeholder}` block content.
* @param error Index of the template with the `{:error}` block content.
* @param loadingConfigIndex Index in the constants array of the configuration of the `{:loading}`
* @param index Index of the `defer` instruction.
* @param primaryTmplIndex Index of the template with the primary block content.
* @param dependencyResolverFn Function that contains dependencies for this defer block.
* @param loadingTmplIndex Index of the template with the `{:loading}` block content.
* @param placeholderTmplIndex Index of the template with the `{:placeholder}` block content.
* @param errorTmplIndex Index of the template with the `{:error}` block content.
* @param loadingConfigIndex Index in the constants array of the configuration of the `{:loading}`.
* block.
* @param placeholderConfigIndexIndex in the constants array of the configuration of the
* `{:placeholder}` block.
@ -33,21 +50,70 @@ type DeferredPlaceholderConfig = [afterTime: number|null];
* @codeGenApi
*/
export function ɵɵdefer(
deferIndex: number,
primaryTemplateIndex: number,
deferredDepsFn?: DeferredDepsFn|null,
loadingIndex?: number|null,
placeholderIndex?: number|null,
errorIndex?: number|null,
loadingConfigIndex?: number|null,
placeholderConfigIndex?: number|null,
) {} // TODO: implement runtime logic.
index: number, primaryTmplIndex: number, dependencyResolverFn?: DependencyResolverFn|null,
loadingTmplIndex?: number|null, placeholderTmplIndex?: number|null,
errorTmplIndex?: number|null, loadingConfigIndex?: number|null,
placeholderConfigIndex?: number|null) {
const lView = getLView();
const tView = getTView();
const tViewConsts = tView.consts;
const adjustedIndex = index + HEADER_OFFSET;
ɵɵtemplate(index, null, 0, 0);
if (tView.firstCreatePass) {
const deferBlockConfig: TDeferBlockDetails = {
primaryTmplIndex,
loadingTmplIndex: loadingTmplIndex ?? null,
placeholderTmplIndex: placeholderTmplIndex ?? null,
errorTmplIndex: errorTmplIndex ?? null,
placeholderBlockConfig: placeholderConfigIndex != null ?
getConstant<DeferredPlaceholderBlockConfig>(tViewConsts, placeholderConfigIndex) :
null,
loadingBlockConfig: loadingConfigIndex != null ?
getConstant<DeferredLoadingBlockConfig>(tViewConsts, loadingConfigIndex) :
null,
dependencyResolverFn: dependencyResolverFn ?? null,
loadingState: DeferDependenciesLoadingState.NOT_STARTED,
loadingPromise: null,
};
setTDeferBlockDetails(tView, adjustedIndex, deferBlockConfig);
}
// Init instance-specific defer details and store it.
const lDetails = [];
lDetails[DEFER_BLOCK_STATE] = DeferBlockInstanceState.INITIAL;
setLDeferBlockDetails(lView, adjustedIndex, lDetails as LDeferBlockDetails);
}
/**
* Loads the deferred content when a value becomes truthy.
* Loads defer block dependencies when a trigger value becomes truthy.
* @codeGenApi
*/
export function ɵɵdeferWhen(value: unknown) {} // TODO: implement runtime logic.
export function ɵɵdeferWhen(rawValue: unknown) {
const lView = getLView();
const bindingIndex = nextBindingIndex();
if (bindingUpdated(lView, bindingIndex, rawValue)) {
const value = Boolean(rawValue); // handle truthy or falsy values
const tNode = getSelectedTNode();
const lDetails = getLDeferBlockDetails(lView, tNode);
const renderedState = lDetails[DEFER_BLOCK_STATE];
if (value === false && renderedState === DeferBlockInstanceState.INITIAL) {
// If nothing is rendered yet, render a placeholder (if defined).
renderPlaceholder(lView, tNode);
} else if (
value === true &&
(renderedState === DeferBlockInstanceState.INITIAL ||
renderedState === DeferBlockInstanceState.PLACEHOLDER)) {
// The `when` condition has changed to `true`, trigger defer block loading
// if the block is either in initial (nothing is rendered) or a placeholder
// state.
triggerDeferBlock(lView, tNode);
}
}
}
/**
* Prefetches the deferred content when a value becomes truthy.
@ -56,13 +122,33 @@ export function ɵɵdeferWhen(value: unknown) {} // TODO: implement runtime log
export function ɵɵdeferPrefetchWhen(value: unknown) {} // TODO: implement runtime logic.
/**
* Creates runtime data structures for the `on idle` deferred trigger.
* Sets up handlers that represent `on idle` deferred trigger.
* @codeGenApi
*/
export function ɵɵdeferOnIdle() {} // TODO: implement runtime logic.
export function ɵɵdeferOnIdle() {
const lView = getLView();
const tNode = getCurrentTNode()!;
renderPlaceholder(lView, tNode);
let id: number;
const removeIdleCallback = () => _cancelIdleCallback(id);
id = _requestIdleCallback(() => {
removeIdleCallback();
// The idle callback is invoked, we no longer need
// to retain a cleanup callback in an LView.
removeLViewOnDestroy(lView, removeIdleCallback);
triggerDeferBlock(lView, tNode);
}) as number;
// Store a cleanup function on LView, so that we cancel idle
// callback in case this LView was destroyed before a callback
// was invoked.
storeLViewOnDestroy(lView, removeIdleCallback);
}
/**
* Creates runtime data structures for the `prefetech on idle` deferred trigger.
* Creates runtime data structures for the `prefetch on idle` deferred trigger.
* @codeGenApi
*/
export function ɵɵdeferPrefetchOnIdle() {} // TODO: implement runtime logic.
@ -75,7 +161,7 @@ export function ɵɵdeferOnImmediate() {} // TODO: implement runtime logic.
/**
* Creates runtime data structures for the `prefetech on immediate` deferred trigger.
* Creates runtime data structures for the `prefetch on immediate` deferred trigger.
* @codeGenApi
*/
export function ɵɵdeferPrefetchOnImmediate() {} // TODO: implement runtime logic.
@ -101,7 +187,7 @@ export function ɵɵdeferPrefetchOnTimer(delay: number) {} // TODO: implement r
export function ɵɵdeferOnHover() {} // TODO: implement runtime logic.
/**
* Creates runtime data structures for the `prefetech on hover` deferred trigger.
* Creates runtime data structures for the `prefetch on hover` deferred trigger.
* @codeGenApi
*/
export function ɵɵdeferPrefetchOnHover() {} // TODO: implement runtime logic.
@ -133,3 +219,303 @@ export function ɵɵdeferOnViewport(target?: unknown) {} // TODO: implement run
* @codeGenApi
*/
export function ɵɵdeferPrefetchOnViewport(target?: unknown) {} // TODO: implement runtime logic.
/********** Helper functions **********/
/**
* Calculates a data slot index for defer block info (either static or
* instance-specific), given an index of a defer instruction.
*/
function getDeferBlockDataIndex(deferBlockIndex: number) {
// Instance state is located at the *next* position
// after the defer block slot in an LView or TView.data.
return deferBlockIndex + 1;
}
/** Retrieves a defer block state from an LView, given a TNode that represents a block. */
function getLDeferBlockDetails(lView: LView, tNode: TNode): LDeferBlockDetails {
const tView = lView[TVIEW];
const slotIndex = getDeferBlockDataIndex(tNode.index);
ngDevMode && assertIndexInDeclRange(tView, slotIndex);
return lView[slotIndex];
}
/** Stores a defer block instance state in LView. */
function setLDeferBlockDetails(
lView: LView, deferBlockIndex: number, lDetails: LDeferBlockDetails) {
const tView = lView[TVIEW];
const slotIndex = getDeferBlockDataIndex(deferBlockIndex);
ngDevMode && assertIndexInDeclRange(tView, slotIndex);
lView[slotIndex] = lDetails;
}
/** Retrieves static info about a defer block, given a TView and a TNode that represents a block. */
function getTDeferBlockDetails(tView: TView, tNode: TNode): TDeferBlockDetails {
const slotIndex = getDeferBlockDataIndex(tNode.index);
ngDevMode && assertIndexInDeclRange(tView, slotIndex);
return tView.data[slotIndex] as TDeferBlockDetails;
}
/** Stores a defer block static info in `TView.data`. */
function setTDeferBlockDetails(
tView: TView, deferBlockIndex: number, deferBlockConfig: TDeferBlockDetails) {
const slotIndex = getDeferBlockDataIndex(deferBlockIndex);
ngDevMode && assertIndexInDeclRange(tView, slotIndex);
tView.data[slotIndex] = deferBlockConfig;
}
/**
* Transitions a defer block to the new state. Updates the necessary
* data structures and renders corresponding block.
*
* @param newState New state that should be applied to the defer block.
* @param tNode TNode that represents a defer block.
* @param lContainer Represents an instance of a defer block.
* @param stateTmplIndex Index of a template that should be rendered.
*/
function renderDeferBlockState(
newState: DeferBlockInstanceState, tNode: TNode, lContainer: LContainer,
stateTmplIndex: number|null): void {
const hostLView = lContainer[PARENT];
// Check if this view is not destroyed. Since the loading process was async,
// the view might end up being destroyed by the time rendering happens.
if (isDestroyed(hostLView)) return;
// Make sure this TNode belongs to TView that represents host LView.
ngDevMode && assertTNodeForLView(tNode, hostLView);
const lDetails = getLDeferBlockDetails(hostLView, tNode);
ngDevMode && assertDefined(lDetails, 'Expected a defer block state defined');
// Note: we transition to the next state if the previous state was represented
// with a number that is less than the next state. For example, if the current
// state is "loading" (represented as `2`), we should not show a placeholder
// (represented as `1`).
if (lDetails[DEFER_BLOCK_STATE] < newState && stateTmplIndex !== null) {
lDetails[DEFER_BLOCK_STATE] = newState;
const hostTView = hostLView[TVIEW];
const adjustedIndex = stateTmplIndex + HEADER_OFFSET;
const tNode = getTNode(hostTView, adjustedIndex) as TContainerNode;
// There is only 1 view that can be present in an LContainer that
// represents a `{#defer}` block, so always refer to the first one.
const viewIndex = 0;
removeLViewFromLContainer(lContainer, viewIndex);
const embeddedLView = createAndRenderEmbeddedLView(hostLView, tNode, null);
addLViewToLContainer(lContainer, embeddedLView, viewIndex);
}
}
/**
* Trigger loading of defer block dependencies if the process hasn't started yet.
*
* @param tDetails Static information about this defer block.
* @param primaryBlockTNode TNode of a primary block template.
* @param injector Environment injector of the application.
*/
function triggerResourceLoading(
tDetails: TDeferBlockDetails, primaryBlockTNode: TNode, injector: Injector) {
const tView = primaryBlockTNode.tView!;
if (tDetails.loadingState !== DeferDependenciesLoadingState.NOT_STARTED) {
// If the loading status is different from initial one, it means that
// the loading of dependencies is in progress and there is nothing to do
// in this function. All details can be obtained from the `tDetails` object.
return;
}
// Switch from NOT_STARTED -> IN_PROGRESS state.
tDetails.loadingState = DeferDependenciesLoadingState.IN_PROGRESS;
// Check if dependency function interceptor is configured.
const deferDependencyInterceptor =
injector.get(DEFER_BLOCK_DEPENDENCY_INTERCEPTOR, null, {optional: true});
const dependenciesFn = deferDependencyInterceptor ?
deferDependencyInterceptor.intercept(tDetails.dependencyResolverFn) :
tDetails.dependencyResolverFn;
// The `dependenciesFn` might be `null` when all dependencies within
// a given `{#defer}` block were eagerly references elsewhere in a file,
// thus no dynamic `import()`s were produced.
if (!dependenciesFn) {
tDetails.loadingPromise = Promise.resolve().then(() => {
tDetails.loadingState = DeferDependenciesLoadingState.COMPLETE;
});
return;
}
// Start downloading of defer block dependencies.
tDetails.loadingPromise = Promise.allSettled(dependenciesFn()).then(results => {
let failed = false;
const directiveDefs: DirectiveDefList = [];
const pipeDefs: PipeDefList = [];
for (const result of results) {
if (result.status === 'fulfilled') {
const dependency = result.value;
const directiveDef = getComponentDef(dependency) || getDirectiveDef(dependency);
if (directiveDef) {
directiveDefs.push(directiveDef);
} else {
const pipeDef = getPipeDef(dependency);
if (pipeDef) {
pipeDefs.push(pipeDef);
}
}
} else {
failed = true;
break;
}
}
// Loading is completed, we no longer need this Promise.
tDetails.loadingPromise = null;
if (failed) {
tDetails.loadingState = DeferDependenciesLoadingState.FAILED;
} else {
tDetails.loadingState = DeferDependenciesLoadingState.COMPLETE;
// Update directive and pipe registries to add newly downloaded dependencies.
if (directiveDefs.length > 0) {
tView.directiveRegistry = tView.directiveRegistry ?
[...tView.directiveRegistry, ...directiveDefs] :
directiveDefs;
}
if (pipeDefs.length > 0) {
tView.pipeRegistry = tView.pipeRegistry ? [...tView.pipeRegistry, ...pipeDefs] : pipeDefs;
}
}
});
}
/** Utility function to render `{:placeholder}` content (if present) */
function renderPlaceholder(lView: LView, tNode: TNode) {
const tView = lView[TVIEW];
const lContainer = lView[tNode.index];
ngDevMode && assertLContainer(lContainer);
const tDetails = getTDeferBlockDetails(tView, tNode);
renderDeferBlockState(
DeferBlockInstanceState.PLACEHOLDER, tNode, lContainer, tDetails.placeholderTmplIndex);
}
/**
* Subscribes to the "loading" Promise and renders corresponding defer sub-block,
* based on the loading results.
*
* @param lContainer Represents an instance of a defer block.
* @param tNode Represents defer block info shared across all instances.
*/
function renderDeferStateAfterResourceLoading(
tDetails: TDeferBlockDetails, tNode: TNode, lContainer: LContainer) {
ngDevMode &&
assertDefined(
tDetails.loadingPromise, 'Expected loading Promise to exist on this defer block');
tDetails.loadingPromise!.then(() => {
if (tDetails.loadingState === DeferDependenciesLoadingState.COMPLETE) {
ngDevMode && assertDeferredDependenciesLoaded(tDetails);
// Everything is loaded, show the primary block content
renderDeferBlockState(
DeferBlockInstanceState.COMPLETE, tNode, lContainer, tDetails.primaryTmplIndex);
} else if (tDetails.loadingState === DeferDependenciesLoadingState.FAILED) {
renderDeferBlockState(
DeferBlockInstanceState.ERROR, tNode, lContainer, tDetails.errorTmplIndex);
}
});
}
/**
* Attempts to trigger loading of defer block dependencies.
* If the block is already in a loading, completed or an error state -
* no additional actions are taken.
*/
function triggerDeferBlock(lView: LView, tNode: TNode) {
const tView = lView[TVIEW];
const lContainer = lView[tNode.index];
ngDevMode && assertLContainer(lContainer);
const tDetails = getTDeferBlockDetails(tView, tNode);
// Condition is triggered, try to render loading state and start downloading.
// Note: if a block is in a loading, completed or an error state, this call would be a noop.
renderDeferBlockState(
DeferBlockInstanceState.LOADING, tNode, lContainer, tDetails.loadingTmplIndex);
switch (tDetails.loadingState) {
case DeferDependenciesLoadingState.NOT_STARTED:
const adjustedIndex = tDetails.primaryTmplIndex + HEADER_OFFSET;
const primaryBlockTNode = getTNode(lView[TVIEW], adjustedIndex) as TContainerNode;
triggerResourceLoading(tDetails, primaryBlockTNode, lView[INJECTOR]!);
// The `loadingState` might have changed to "loading".
if ((tDetails.loadingState as DeferDependenciesLoadingState) ===
DeferDependenciesLoadingState.IN_PROGRESS) {
renderDeferStateAfterResourceLoading(tDetails, tNode, lContainer);
}
break;
case DeferDependenciesLoadingState.IN_PROGRESS:
renderDeferStateAfterResourceLoading(tDetails, tNode, lContainer);
break;
case DeferDependenciesLoadingState.COMPLETE:
ngDevMode && assertDeferredDependenciesLoaded(tDetails);
renderDeferBlockState(
DeferBlockInstanceState.COMPLETE, tNode, lContainer, tDetails.primaryTmplIndex);
break;
case DeferDependenciesLoadingState.FAILED:
renderDeferBlockState(
DeferBlockInstanceState.ERROR, tNode, lContainer, tDetails.errorTmplIndex);
break;
default:
if (ngDevMode) {
throwError('Unknown defer block state');
}
}
}
/**
* Asserts whether all dependencies for a defer block are loaded.
* Always run this function (in dev mode) before rendering a defer
* block in completed state.
*/
function assertDeferredDependenciesLoaded(tDetails: TDeferBlockDetails) {
assertEqual(
tDetails.loadingState, DeferDependenciesLoadingState.COMPLETE,
'Expecting all deferred dependencies to be loaded.');
}
/**
* **INTERNAL**, avoid referencing it in application code.
*
* Describes a helper class that allows to intercept a call to retrieve current
* dependency loading function and replace it with a different implementation.
* This interceptor class is needed to allow testing blocks in different states
* by simulating loading response.
*/
export interface DeferBlockDependencyInterceptor {
/**
* Invoked for each defer block when dependency loading function is accessed.
*/
intercept(dependencyFn: DependencyResolverFn|null): DependencyResolverFn|null;
/**
* Allows to configure an interceptor function.
*/
setInterceptor(interceptorFn: (current: DependencyResolverFn) => DependencyResolverFn): void;
}
/**
* **INTERNAL**, avoid referencing it in application code.
*
* Injector token that allows to provide `DeferBlockDependencyInterceptor` class
* implementation.
*/
export const DEFER_BLOCK_DEPENDENCY_INTERCEPTOR =
new InjectionToken<DeferBlockDependencyInterceptor>(
ngDevMode ? 'DEFER_BLOCK_DEPENDENCY_INTERCEPTOR' : '');

View file

@ -0,0 +1,129 @@
/**
* @license
* Copyright Google LLC All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
import type {DependencyType} from './definition';
/**
* Describes the shape of a function generated by the compiler
* to download dependencies that can be defer-loaded.
*/
export type DependencyResolverFn = () => Array<Promise<DependencyType>>;
/**
* Describes the state of defer block dependency loading.
*/
export const enum DeferDependenciesLoadingState {
/** Initial state, dependency loading is not yet triggered */
NOT_STARTED,
/** Dependency loading is in progress */
IN_PROGRESS,
/** Dependency loading has completed successfully */
COMPLETE,
/** Dependency loading has failed */
FAILED,
}
/** Configuration object for a `{:loading}` block as it is stored in the component constants. */
export type DeferredLoadingBlockConfig = [minimumTime: number|null, afterTime: number|null];
/** Configuration object for a `{:placeholder}` block as it is stored in the component constants. */
export type DeferredPlaceholderBlockConfig = [afterTime: number|null];
/**
* Describes the data shared across all instances of a {#defer} block.
*/
export interface TDeferBlockDetails {
/**
* Index in an LView and TData arrays where a template for the primary content
* can be found.
*/
primaryTmplIndex: number;
/**
* Index in an LView and TData arrays where a template for the `{:loading}`
* block can be found.
*/
loadingTmplIndex: number|null;
/**
* Extra configuration parameters (such as `after` and `minimum`)
* for the `{:loading}` block.
*/
loadingBlockConfig: DeferredLoadingBlockConfig|null;
/**
* Index in an LView and TData arrays where a template for the `{:placeholder}`
* block can be found.
*/
placeholderTmplIndex: number|null;
/**
* Extra configuration parameters (such as `after` and `minimum`)
* for the `{:placeholder}` block.
*/
placeholderBlockConfig: DeferredPlaceholderBlockConfig|null;
/**
* Index in an LView and TData arrays where a template for the `{:error}`
* block can be found.
*/
errorTmplIndex: number|null;
/**
* Compiler-generated function that loads all dependencies for a `{#defer}` block.
*/
dependencyResolverFn: DependencyResolverFn|null;
/**
* Keeps track of the current loading state of defer block dependencies.
*/
loadingState: DeferDependenciesLoadingState;
/**
* Dependency loading Promise. This Promise is helpful for cases when there
* are multiple instances of a defer block (e.g. if it was used inside of an *ngFor),
* which all await the same set of dependencies.
*/
loadingPromise: Promise<unknown>|null;
}
/**
* Describes the current state of this {#defer} block instance.
*/
export const enum DeferBlockInstanceState {
/** Initial state, nothing is rendered yet */
INITIAL,
/** The {:placeholder} block content is rendered */
PLACEHOLDER,
/** The {:loading} block content is rendered */
LOADING,
/** The main content block content is rendered */
COMPLETE,
/** The {:error} block content is rendered */
ERROR
}
export const DEFER_BLOCK_STATE = 0;
/**
* Describes instance-specific {#defer} block data.
*
* Note: currently there is only the `state` slot, but more slots
* would be added later to keep track of `after` and `maximum` features
* (which would require per-instance state).
*/
export interface LDeferBlockDetails extends Array<unknown> {
[DEFER_BLOCK_STATE]: DeferBlockInstanceState;
}

View file

@ -491,7 +491,9 @@ export type DirectiveTypeList =
(DirectiveType<any>|ComponentType<any>|
Type<any>/* Type as workaround for: Microsoft/TypeScript/issues/4881 */)[];
export type DependencyTypeList = (DirectiveType<any>|ComponentType<any>|PipeType<any>|Type<any>)[];
export type DependencyType = DirectiveType<any>|ComponentType<any>|PipeType<any>|Type<any>;
export type DependencyTypeList = Array<DependencyType>;
export type TypeOrFactory<T> = T|(() => T);

View file

@ -56,3 +56,7 @@ export function isProjectionTNode(tNode: TNode): boolean {
export function hasI18n(lView: LView): boolean {
return (lView[FLAGS] & LViewFlags.HasI18n) === LViewFlags.HasI18n;
}
export function isDestroyed(lView: LView): boolean {
return (lView[FLAGS] & LViewFlags.Destroyed) === LViewFlags.Destroyed;
}

View file

@ -23,6 +23,7 @@ import {LQueries, TQueries} from './query';
import {Renderer, RendererFactory} from './renderer';
import {RElement} from './renderer_dom';
import {TStylingKey, TStylingRange} from './styling';
import {TDeferBlockDetails} from './defer';
@ -908,8 +909,9 @@ export type DestroyHookData = (HookEntry|HookData)[];
*
* Injector bloom filters are also stored here.
*/
export type TData = (TNode|PipeDef<any>|DirectiveDef<any>|ComponentDef<any>|number|TStylingRange|
TStylingKey|ProviderToken<any>|TI18n|I18nUpdateOpCodes|TIcu|null|string)[];
export type TData =
(TNode|PipeDef<any>|DirectiveDef<any>|ComponentDef<any>|number|TStylingRange|TStylingKey|
ProviderToken<any>|TI18n|I18nUpdateOpCodes|TIcu|null|string|TDeferBlockDetails)[];
// Note: This hack is necessary so we don't erroneously get a circular dependency
// failure based on types.

View file

@ -0,0 +1,486 @@
/**
* @license
* Copyright Google LLC All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
import {ɵsetEnabledBlockTypes as setEnabledBlockTypes} from '@angular/compiler/src/jit_compiler_facade';
import {Component, Input, QueryList, ViewChildren, ɵDEFER_BLOCK_DEPENDENCY_INTERCEPTOR} from '@angular/core';
import {TestBed} from '@angular/core/testing';
describe('#defer', () => {
beforeEach(() => setEnabledBlockTypes(['defer']));
afterEach(() => setEnabledBlockTypes([]));
it('should transition between placeholder, loading and loaded states', async () => {
@Component({
selector: 'my-lazy-cmp',
standalone: true,
template: 'Hi!',
})
class MyLazyCmp {
}
@Component({
standalone: true,
selector: 'simple-app',
imports: [MyLazyCmp],
template: `
{#defer when isVisible}
<my-lazy-cmp />
{:loading}
Loading...
{:placeholder}
Placeholder!
{:error}
Failed to load dependencies :(
{/defer}
`
})
class MyCmp {
isVisible = false;
}
const fixture = TestBed.createComponent(MyCmp);
fixture.detectChanges();
expect(fixture.nativeElement.outerHTML).toContain('Placeholder');
fixture.componentInstance.isVisible = true;
fixture.detectChanges();
expect(fixture.nativeElement.outerHTML).toContain('Loading');
// Wait for dependencies to load.
await fixture.whenStable();
fixture.detectChanges();
expect(fixture.nativeElement.outerHTML).toContain('<my-lazy-cmp>Hi!</my-lazy-cmp>');
});
it('should work when only main block is present', async () => {
@Component({
selector: 'my-lazy-cmp',
standalone: true,
template: 'Hi!',
})
class MyLazyCmp {
}
@Component({
standalone: true,
selector: 'simple-app',
imports: [MyLazyCmp],
template: `
<p>Text outside of a defer block</p>
{#defer when isVisible}
<my-lazy-cmp />
{/defer}
`
})
class MyCmp {
isVisible = false;
}
const fixture = TestBed.createComponent(MyCmp);
fixture.detectChanges();
expect(fixture.nativeElement.outerHTML).toContain('Text outside of a defer block');
fixture.componentInstance.isVisible = true;
fixture.detectChanges();
// Wait for dependencies to load.
await fixture.whenStable();
fixture.detectChanges();
expect(fixture.nativeElement.outerHTML).toContain('<my-lazy-cmp>Hi!</my-lazy-cmp>');
});
describe('directive matching', () => {
it('should support directive matching in all blocks', async () => {
@Component({
selector: 'nested-cmp',
standalone: true,
template: 'Rendering {{ block }} block.',
})
class NestedCmp {
@Input() block!: string;
}
@Component({
standalone: true,
selector: 'simple-app',
imports: [NestedCmp],
template: `
{#defer when isVisible}
<nested-cmp [block]="'primary'" />
{:loading}
Loading...
<nested-cmp [block]="'loading'" />
{:placeholder}
Placeholder!
<nested-cmp [block]="'placeholder'" />
{:error}
Failed to load dependencies :(
<nested-cmp [block]="'error'" />
{/defer}
`
})
class MyCmp {
isVisible = false;
}
const fixture = TestBed.createComponent(MyCmp);
fixture.detectChanges();
expect(fixture.nativeElement.outerHTML)
.toContain(
'<nested-cmp ng-reflect-block="placeholder">Rendering placeholder block.</nested-cmp>');
fixture.componentInstance.isVisible = true;
fixture.detectChanges();
expect(fixture.nativeElement.outerHTML)
.toContain(
'<nested-cmp ng-reflect-block="loading">Rendering loading block.</nested-cmp>');
// Wait for dependencies to load.
await fixture.whenStable();
fixture.detectChanges();
expect(fixture.nativeElement.outerHTML)
.toContain(
'<nested-cmp ng-reflect-block="primary">Rendering primary block.</nested-cmp>');
});
});
describe('error handling', () => {
it('should render an error block when loading fails', async () => {
@Component({
selector: 'nested-cmp',
standalone: true,
template: 'Rendering {{ block }} block.',
})
class NestedCmp {
@Input() block!: string;
}
@Component({
standalone: true,
selector: 'simple-app',
imports: [NestedCmp],
template: `
{#defer when isVisible}
<nested-cmp [block]="'primary'" />
{:loading}
Loading...
{:placeholder}
Placeholder!
{:error}
Failed to load dependencies :(
<nested-cmp [block]="'error'" />
{/defer}
`
})
class MyCmp {
isVisible = false;
@ViewChildren(NestedCmp) cmps!: QueryList<NestedCmp>;
}
const deferDepsInterceptor = {
intercept() {
// Simulate loading failure.
return () => [Promise.reject()];
}
};
TestBed.configureTestingModule({
providers: [
{provide: ɵDEFER_BLOCK_DEPENDENCY_INTERCEPTOR, useValue: deferDepsInterceptor},
]
});
const fixture = TestBed.createComponent(MyCmp);
fixture.detectChanges();
expect(fixture.nativeElement.outerHTML).toContain('Placeholder');
fixture.componentInstance.isVisible = true;
fixture.detectChanges();
expect(fixture.nativeElement.outerHTML).toContain('Loading');
// Wait for dependencies to load.
await fixture.whenStable();
fixture.detectChanges();
// Verify that the error block is rendered.
// Also verify that selector matching works in an error block.
expect(fixture.nativeElement.outerHTML)
.toContain('<nested-cmp ng-reflect-block="error">Rendering error block.</nested-cmp>');
// Verify that queries work within an error block.
expect(fixture.componentInstance.cmps.length).toBe(1);
expect(fixture.componentInstance.cmps.get(0)?.block).toBe('error');
});
});
describe('queries', () => {
it('should query for components within each block', async () => {
@Component({
selector: 'nested-cmp',
standalone: true,
template: 'Rendering {{ block }} block.',
})
class NestedCmp {
@Input() block!: string;
}
@Component({
standalone: true,
selector: 'simple-app',
imports: [NestedCmp],
template: `
{#defer when isVisible}
<nested-cmp [block]="'primary'" />
{:loading}
Loading...
<nested-cmp [block]="'loading'" />
{:placeholder}
Placeholder!
<nested-cmp [block]="'placeholder'" />
{:error}
Failed to load dependencies :(
<nested-cmp [block]="'error'" />
{/defer}
`
})
class MyCmp {
isVisible = false;
@ViewChildren(NestedCmp) cmps!: QueryList<NestedCmp>;
}
const fixture = TestBed.createComponent(MyCmp);
fixture.detectChanges();
expect(fixture.componentInstance.cmps.length).toBe(1);
expect(fixture.componentInstance.cmps.get(0)?.block).toBe('placeholder');
expect(fixture.nativeElement.outerHTML)
.toContain(
'<nested-cmp ng-reflect-block="placeholder">Rendering placeholder block.</nested-cmp>');
fixture.componentInstance.isVisible = true;
fixture.detectChanges();
expect(fixture.componentInstance.cmps.length).toBe(1);
expect(fixture.componentInstance.cmps.get(0)?.block).toBe('loading');
expect(fixture.nativeElement.outerHTML)
.toContain(
'<nested-cmp ng-reflect-block="loading">Rendering loading block.</nested-cmp>');
// Wait for dependencies to load.
await fixture.whenStable();
fixture.detectChanges();
expect(fixture.componentInstance.cmps.length).toBe(1);
expect(fixture.componentInstance.cmps.get(0)?.block).toBe('primary');
expect(fixture.nativeElement.outerHTML)
.toContain(
'<nested-cmp ng-reflect-block="primary">Rendering primary block.</nested-cmp>');
});
});
describe('content projection', () => {
it('should be able to project content into each block', async () => {
@Component({
selector: 'cmp-a',
standalone: true,
template: 'CmpA',
})
class CmpA {
}
@Component({
selector: 'cmp-b',
standalone: true,
template: 'CmpB',
})
class CmpB {
}
@Component({
selector: 'nested-cmp',
standalone: true,
template: 'Rendering {{ block }} block.',
})
class NestedCmp {
@Input() block!: string;
}
@Component({
standalone: true,
selector: 'my-app',
imports: [NestedCmp],
template: `
{#defer when isVisible}
<nested-cmp [block]="'primary'" />
<ng-content />
{:loading}
Loading...
<nested-cmp [block]="'loading'" />
{:placeholder}
Placeholder!
<nested-cmp [block]="'placeholder'" />
{:error}
Failed to load dependencies :(
<nested-cmp [block]="'error'" />
{/defer}
`
})
class MyCmp {
@Input() isVisible = false;
}
@Component({
standalone: true,
selector: 'root-app',
imports: [MyCmp, CmpA, CmpB],
template: `
<my-app [isVisible]="isVisible">
Projected content.
<b>Including tags</b>
<cmp-a />
{#defer when isInViewport}
<cmp-b />
{:placeholder}
Projected defer block placeholder.
{/defer}
</my-app>
`
})
class RootCmp {
isVisible = false;
isInViewport = false;
}
const fixture = TestBed.createComponent(RootCmp);
fixture.detectChanges();
expect(fixture.nativeElement.outerHTML)
.toContain(
'<nested-cmp ng-reflect-block="placeholder">Rendering placeholder block.</nested-cmp>');
fixture.componentInstance.isVisible = true;
fixture.detectChanges();
expect(fixture.nativeElement.outerHTML)
.toContain(
'<nested-cmp ng-reflect-block="loading">Rendering loading block.</nested-cmp>');
// Wait for dependencies to load.
await fixture.whenStable();
fixture.detectChanges();
// Verify primary block content.
const primaryBlockHTML = fixture.nativeElement.outerHTML;
expect(primaryBlockHTML)
.toContain(
'<nested-cmp ng-reflect-block="primary">Rendering primary block.</nested-cmp>');
expect(primaryBlockHTML).toContain('Projected content.');
expect(primaryBlockHTML).toContain('<b>Including tags</b>');
expect(primaryBlockHTML).toContain('<cmp-a>CmpA</cmp-a>');
expect(primaryBlockHTML).toContain('Projected defer block placeholder.');
fixture.componentInstance.isInViewport = true;
fixture.detectChanges();
// Wait for projected block dependencies to load.
await fixture.whenStable();
fixture.detectChanges();
// Nested defer block was triggered and the `CmpB` content got rendered.
expect(fixture.nativeElement.outerHTML).toContain('<cmp-b>CmpB</cmp-b>');
});
});
describe('nested blocks', () => {
it('should be able to have nested blocks', async () => {
@Component({
selector: 'cmp-a',
standalone: true,
template: 'CmpA',
})
class CmpA {
}
@Component({
selector: 'nested-cmp',
standalone: true,
template: 'Rendering {{ block }} block.',
})
class NestedCmp {
@Input() block!: string;
}
@Component({
standalone: true,
selector: 'root-app',
imports: [NestedCmp, CmpA],
template: `
{#defer when isVisible}
<nested-cmp [block]="'primary'" />
{#defer when isInViewport}
<cmp-a />
{:placeholder}
Nested defer block placeholder.
{/defer}
{:placeholder}
<nested-cmp [block]="'placeholder'" />
{/defer}
`
})
class RootCmp {
isVisible = false;
isInViewport = false;
}
const fixture = TestBed.createComponent(RootCmp);
fixture.detectChanges();
expect(fixture.nativeElement.outerHTML)
.toContain(
'<nested-cmp ng-reflect-block="placeholder">Rendering placeholder block.</nested-cmp>');
fixture.componentInstance.isVisible = true;
fixture.detectChanges();
await fixture.whenStable(); // loading dependencies for the defer block within MyCmp...
fixture.detectChanges();
// Verify primary block content.
const primaryBlockHTML = fixture.nativeElement.outerHTML;
expect(primaryBlockHTML)
.toContain(
'<nested-cmp ng-reflect-block="primary">Rendering primary block.</nested-cmp>');
// Make sure we have a nested block in a placeholder state.
expect(primaryBlockHTML).toContain('Nested defer block placeholder.');
// Trigger condition for the nested block.
fixture.componentInstance.isInViewport = true;
fixture.detectChanges();
// Wait for nested block dependencies to load.
await fixture.whenStable();
fixture.detectChanges();
// Nested defer block was triggered and the `CmpB` content got rendered.
expect(fixture.nativeElement.outerHTML).toContain('<cmp-a>CmpA</cmp-a>');
});
});
});

View file

@ -1977,7 +1977,7 @@
"name": "tap"
},
{
"name": "throwError5"
"name": "throwError6"
},
{
"name": "throwIfEmpty"