diff --git a/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler_deferred/GOLDEN_PARTIAL.js b/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler_deferred/GOLDEN_PARTIAL.js
index e4bcf5223e2..69fbedda22f 100644
--- a/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler_deferred/GOLDEN_PARTIAL.js
+++ b/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler_deferred/GOLDEN_PARTIAL.js
@@ -13,6 +13,7 @@ MyApp.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "0.0.0-
{{message}}
{#defer}Deferred content{/defer}
+
Content after defer block
`, 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
{{message}}
{#defer}Deferred content{/defer}
+
Content after defer block
`,
}]
diff --git a/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler_deferred/basic_deferred.ts b/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler_deferred/basic_deferred.ts
index 316a889e9cf..9063b510adb 100644
--- a/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler_deferred/basic_deferred.ts
+++ b/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler_deferred/basic_deferred.ts
@@ -5,6 +5,7 @@ import {Component} from '@angular/core';
{{message}}
{#defer}Deferred content{/defer}
+
Content after defer block
`,
})
diff --git a/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler_deferred/basic_deferred_template.js b/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler_deferred/basic_deferred_template.js
index d28a3a68387..00d4b1884a7 100644
--- a/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler_deferred/basic_deferred_template.js
+++ b/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler_deferred/basic_deferred_template.js
@@ -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);
diff --git a/packages/compiler/src/render3/view/template.ts b/packages/compiler/src/render3/view/template.ts
index 8c166438173..36a937e929e 100644
--- a/packages/compiler/src/render3/view/template.ts
+++ b/packages/compiler/src/render3/view/template.ts
@@ -1300,6 +1300,10 @@ export class TemplateDefinitionBuilder implements t.Visitor, 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) {
diff --git a/packages/core/src/core_render3_private_export.ts b/packages/core/src/core_render3_private_export.ts
index 60fee54593f..e437c48bfb1 100644
--- a/packages/core/src/core_render3_private_export.ts
+++ b/packages/core/src/core_render3_private_export.ts
@@ -68,6 +68,8 @@ export {
setClassMetadataAsync as ɵsetClassMetadataAsync,
setLocaleId as ɵsetLocaleId,
store as ɵstore,
+ ɵDeferBlockDependencyInterceptor,
+ ɵDEFER_BLOCK_DEPENDENCY_INTERCEPTOR,
ɵɵadvance,
ɵɵattribute,
ɵɵattributeInterpolate1,
diff --git a/packages/core/src/render3/assert.ts b/packages/core/src/render3/assert.ts
index c4212adb957..b8de75922a4 100644
--- a/packages/core/src/render3/assert.ts
+++ b/packages/core/src/render3/assert.ts
@@ -111,8 +111,7 @@ export function assertDirectiveDef(obj: any): asserts obj is DirectiveDef
}
}
-export function assertIndexInDeclRange(lView: LView, index: number) {
- const tView = lView[1];
+export function assertIndexInDeclRange(tView: TView, index: number) {
assertBetween(HEADER_OFFSET, tView.bindingStartIndex, index);
}
diff --git a/packages/core/src/render3/index.ts b/packages/core/src/render3/index.ts
index 359aead787b..7c40c622d12 100644
--- a/packages/core/src/render3/index.ts
+++ b/packages/core/src/render3/index.ts
@@ -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';
diff --git a/packages/core/src/render3/instructions/advance.ts b/packages/core/src/render3/instructions/advance.ts
index 8ff38fd1828..23f091f64d7 100644
--- a/packages/core/src/render3/instructions/advance.ts
+++ b/packages/core/src/render3/instructions/advance.ts
@@ -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
diff --git a/packages/core/src/render3/instructions/defer.ts b/packages/core/src/render3/instructions/defer.ts
index 3e9b3eebb23..cf3ff2ba926 100644
--- a/packages/core/src/render3/instructions/defer.ts
+++ b/packages/core/src/render3/instructions/defer.ts
@@ -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>|Type>;
+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(tViewConsts, placeholderConfigIndex) :
+ null,
+ loadingBlockConfig: loadingConfigIndex != null ?
+ getConstant(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(
+ ngDevMode ? 'DEFER_BLOCK_DEPENDENCY_INTERCEPTOR' : '');
diff --git a/packages/core/src/render3/interfaces/defer.ts b/packages/core/src/render3/interfaces/defer.ts
new file mode 100644
index 00000000000..9fdb0891043
--- /dev/null
+++ b/packages/core/src/render3/interfaces/defer.ts
@@ -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>;
+
+/**
+ * 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|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 {
+ [DEFER_BLOCK_STATE]: DeferBlockInstanceState;
+}
diff --git a/packages/core/src/render3/interfaces/definition.ts b/packages/core/src/render3/interfaces/definition.ts
index 946053b5af7..43bf1db22f3 100644
--- a/packages/core/src/render3/interfaces/definition.ts
+++ b/packages/core/src/render3/interfaces/definition.ts
@@ -491,7 +491,9 @@ export type DirectiveTypeList =
(DirectiveType|ComponentType|
Type/* Type as workaround for: Microsoft/TypeScript/issues/4881 */)[];
-export type DependencyTypeList = (DirectiveType|ComponentType|PipeType|Type)[];
+export type DependencyType = DirectiveType|ComponentType|PipeType|Type;
+
+export type DependencyTypeList = Array;
export type TypeOrFactory = T|(() => T);
diff --git a/packages/core/src/render3/interfaces/type_checks.ts b/packages/core/src/render3/interfaces/type_checks.ts
index 5d2a67434e8..3239b4cc252 100644
--- a/packages/core/src/render3/interfaces/type_checks.ts
+++ b/packages/core/src/render3/interfaces/type_checks.ts
@@ -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;
+}
diff --git a/packages/core/src/render3/interfaces/view.ts b/packages/core/src/render3/interfaces/view.ts
index 02a35f79550..93d67c94775 100644
--- a/packages/core/src/render3/interfaces/view.ts
+++ b/packages/core/src/render3/interfaces/view.ts
@@ -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|DirectiveDef|ComponentDef|number|TStylingRange|
- TStylingKey|ProviderToken|TI18n|I18nUpdateOpCodes|TIcu|null|string)[];
+export type TData =
+ (TNode|PipeDef|DirectiveDef|ComponentDef|number|TStylingRange|TStylingKey|
+ ProviderToken|TI18n|I18nUpdateOpCodes|TIcu|null|string|TDeferBlockDetails)[];
// Note: This hack is necessary so we don't erroneously get a circular dependency
// failure based on types.
diff --git a/packages/core/test/acceptance/defer_spec.ts b/packages/core/test/acceptance/defer_spec.ts
new file mode 100644
index 00000000000..a60479352e8
--- /dev/null
+++ b/packages/core/test/acceptance/defer_spec.ts
@@ -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}
+
+ {: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('Hi!');
+ });
+
+ 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: `
+ Text outside of a defer block
+ {#defer when isVisible}
+
+ {/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('Hi!');
+ });
+
+
+ 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}
+
+ {: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(
+ 'Rendering placeholder block.');
+
+ fixture.componentInstance.isVisible = true;
+ fixture.detectChanges();
+
+ expect(fixture.nativeElement.outerHTML)
+ .toContain(
+ 'Rendering loading block.');
+
+ // Wait for dependencies to load.
+ await fixture.whenStable();
+ fixture.detectChanges();
+
+ expect(fixture.nativeElement.outerHTML)
+ .toContain(
+ 'Rendering primary block.');
+ });
+ });
+
+ 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}
+
+ {:loading}
+ Loading...
+ {:placeholder}
+ Placeholder!
+ {:error}
+ Failed to load dependencies :(
+
+ {/defer}
+ `
+ })
+ class MyCmp {
+ isVisible = false;
+ @ViewChildren(NestedCmp) cmps!: QueryList;
+ }
+
+ 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('Rendering error block.');
+
+ // 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}
+
+ {:loading}
+ Loading...
+
+ {:placeholder}
+ Placeholder!
+
+ {:error}
+ Failed to load dependencies :(
+
+ {/defer}
+ `
+ })
+ class MyCmp {
+ isVisible = false;
+
+ @ViewChildren(NestedCmp) cmps!: QueryList;
+ }
+
+ 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(
+ 'Rendering placeholder block.');
+
+ 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(
+ 'Rendering loading block.');
+
+ // 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(
+ 'Rendering primary block.');
+ });
+ });
+
+ 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}
+
+
+ {:loading}
+ Loading...
+
+ {:placeholder}
+ Placeholder!
+
+ {:error}
+ Failed to load dependencies :(
+
+ {/defer}
+ `
+ })
+ class MyCmp {
+ @Input() isVisible = false;
+ }
+
+ @Component({
+ standalone: true,
+ selector: 'root-app',
+ imports: [MyCmp, CmpA, CmpB],
+ template: `
+
+ Projected content.
+ Including tags
+
+ {#defer when isInViewport}
+
+ {:placeholder}
+ Projected defer block placeholder.
+ {/defer}
+
+ `
+ })
+ class RootCmp {
+ isVisible = false;
+ isInViewport = false;
+ }
+
+ const fixture = TestBed.createComponent(RootCmp);
+ fixture.detectChanges();
+
+ expect(fixture.nativeElement.outerHTML)
+ .toContain(
+ 'Rendering placeholder block.');
+
+ fixture.componentInstance.isVisible = true;
+ fixture.detectChanges();
+
+ expect(fixture.nativeElement.outerHTML)
+ .toContain(
+ 'Rendering loading block.');
+
+ // Wait for dependencies to load.
+ await fixture.whenStable();
+ fixture.detectChanges();
+
+ // Verify primary block content.
+ const primaryBlockHTML = fixture.nativeElement.outerHTML;
+ expect(primaryBlockHTML)
+ .toContain(
+ 'Rendering primary block.');
+ expect(primaryBlockHTML).toContain('Projected content.');
+ expect(primaryBlockHTML).toContain('Including tags');
+ expect(primaryBlockHTML).toContain('CmpA');
+ 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('CmpB');
+ });
+ });
+
+ 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}
+
+ {#defer when isInViewport}
+
+ {:placeholder}
+ Nested defer block placeholder.
+ {/defer}
+ {:placeholder}
+
+ {/defer}
+ `
+ })
+ class RootCmp {
+ isVisible = false;
+ isInViewport = false;
+ }
+
+ const fixture = TestBed.createComponent(RootCmp);
+ fixture.detectChanges();
+
+ expect(fixture.nativeElement.outerHTML)
+ .toContain(
+ 'Rendering placeholder block.');
+
+ 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(
+ 'Rendering primary block.');
+
+ // 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('CmpA');
+ });
+ });
+});
diff --git a/packages/core/test/bundling/router/bundle.golden_symbols.json b/packages/core/test/bundling/router/bundle.golden_symbols.json
index c1a7bd5b965..5cc1130e5be 100644
--- a/packages/core/test/bundling/router/bundle.golden_symbols.json
+++ b/packages/core/test/bundling/router/bundle.golden_symbols.json
@@ -1977,7 +1977,7 @@
"name": "tap"
},
{
- "name": "throwError5"
+ "name": "throwError6"
},
{
"name": "throwIfEmpty"