mirror of
https://github.com/angular/angular
synced 2026-05-24 09:28:37 +00:00
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:
parent
bf9663847d
commit
c4deaac5b0
15 changed files with 1062 additions and 40 deletions
|
|
@ -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>
|
||||
`,
|
||||
}]
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ import {Component} from '@angular/core';
|
|||
<div>
|
||||
{{message}}
|
||||
{#defer}Deferred content{/defer}
|
||||
<p>Content after defer block</p>
|
||||
</div>
|
||||
`,
|
||||
})
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -68,6 +68,8 @@ export {
|
|||
setClassMetadataAsync as ɵsetClassMetadataAsync,
|
||||
setLocaleId as ɵsetLocaleId,
|
||||
store as ɵstore,
|
||||
ɵDeferBlockDependencyInterceptor,
|
||||
ɵDEFER_BLOCK_DEPENDENCY_INTERCEPTOR,
|
||||
ɵɵadvance,
|
||||
ɵɵattribute,
|
||||
ɵɵattributeInterpolate1,
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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';
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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' : '');
|
||||
|
|
|
|||
129
packages/core/src/render3/interfaces/defer.ts
Normal file
129
packages/core/src/render3/interfaces/defer.ts
Normal 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;
|
||||
}
|
||||
|
|
@ -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);
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
486
packages/core/test/acceptance/defer_spec.ts
Normal file
486
packages/core/test/acceptance/defer_spec.ts
Normal 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>');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -1977,7 +1977,7 @@
|
|||
"name": "tap"
|
||||
},
|
||||
{
|
||||
"name": "throwError5"
|
||||
"name": "throwError6"
|
||||
},
|
||||
{
|
||||
"name": "throwIfEmpty"
|
||||
|
|
|
|||
Loading…
Reference in a new issue