From 376e6b00f39cae691bc9507cb8deb34b8e95dbc3 Mon Sep 17 00:00:00 2001 From: Andrew Kushnir Date: Wed, 9 Aug 2023 18:40:10 -0700 Subject: [PATCH] refactor(core): initial implementation of `{#defer}` block runtime This commit adds an initial implementation of the `{#defer}` block runtime. --- .../core/src/render3/instructions/defer.ts | 341 ++++++++++++++++-- .../core/src/render3/instructions/shared.ts | 29 +- .../core/src/render3/instructions/template.ts | 28 +- .../core/src/render3/interfaces/container.ts | 40 +- .../core/src/render3/interfaces/definition.ts | 6 +- packages/core/src/render3/interfaces/node.ts | 89 +++++ .../src/render3/interfaces/type_checks.ts | 4 + packages/core/test/acceptance/defer_spec.ts | 66 ++++ 8 files changed, 554 insertions(+), 49 deletions(-) create mode 100644 packages/core/test/acceptance/defer_spec.ts diff --git a/packages/core/src/render3/instructions/defer.ts b/packages/core/src/render3/instructions/defer.ts index 3e9b3eebb23ae9..3d1a456b1f2424 100644 --- a/packages/core/src/render3/instructions/defer.ts +++ b/packages/core/src/render3/instructions/defer.ts @@ -6,26 +6,41 @@ * found in the LICENSE file at https://angular.io/license */ -import {Type} from '../../interface/type'; +import {assertDefined, assertEqual, throwError} from '../../util/assert'; +import {assertLContainer} from '../assert'; +import {bindingUpdated} from '../bindings'; +import {getComponentDef, getDirectiveDef, getPipeDef} from '../definition'; +import {DEFER_BLOCK_DETAILS, DeferBlockInstanceState, LContainer} from '../interfaces/container'; +import {DependencyResolverFn, DirectiveDefList, PipeDefList} from '../interfaces/definition'; +import {DeferDependenciesLoadingState, DeferredLoadingBlockConfig, DeferredPlaceholderBlockConfig, TContainerNode, TDeferBlockDetails, TNode} from '../interfaces/node'; +import {isDestroyed} from '../interfaces/type_checks'; +import {HEADER_OFFSET, LView, PARENT, TVIEW} from '../interfaces/view'; +import {getCurrentTNode, getLView, getSelectedTNode, getTView, nextBindingIndex} from '../state'; +import {NO_CHANGE} from '../tokens'; +import {getConstant, getTNode, storeLViewOnDestroy} from '../util/view_utils'; +import {addLViewToLContainer, createAndRenderEmbeddedLView, removeLViewFromLContainer} from '../view_manipulation'; -export type DeferredDepsFn = () => Array>|Type>; +import {templateInternal} 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 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 cancelIdleCallback !== '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 +48,65 @@ 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; + + // Defer block details are needed only once during the first creation pass, + // so we wrap an object with defer block details into a function that is only + // invoked once to avoid re-constructing the same object for each subsequent + // creation run. + 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, + loadingFailedReason: null, + }); + + templateInternal(index, null, 0, 0, deferBlockConfig); + + // Init instance-specific defer details for this LContainer. + const adjustedIndex = index + HEADER_OFFSET; + const lContainer = lView[adjustedIndex]; + lContainer[DEFER_BLOCK_DETAILS] = {state: DeferBlockInstanceState.INITIAL}; +} /** * Loads the deferred content when a value becomes truthy. * @codeGenApi */ -export function ɵɵdeferWhen(value: unknown) {} // TODO: implement runtime logic. +export function ɵɵdeferWhen(value: unknown) { + const lView = getLView(); + const bindingIndex = nextBindingIndex(); + const newValue = !!value; // handle truthy or falsy values + const oldValue = lView[bindingIndex]; + // If an old value was `true` - don't enter the path that triggers + // defer loading. + if (oldValue !== true && bindingUpdated(lView, bindingIndex, value)) { + const tNode = getSelectedTNode(); + if (oldValue === NO_CHANGE && newValue === false) { + // We set the value for the first time, render a placeholder (if defined). + renderPlaceholder(lView, tNode); + } else if (newValue === true) { + // The `when` condition has changed to `true`, trigger defer block loading. + triggerDeferBlock(lView, tNode); + } + } +} /** * Prefetches the deferred content when a value becomes truthy. @@ -56,10 +115,21 @@ 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); + + const id = _requestIdleCallback(() => { + triggerDeferBlock(lView, tNode); + cancelIdleCallback(id); + }) as number; + storeLViewOnDestroy(lView, () => _cancelIdleCallback(id)); +} /** * Creates runtime data structures for the `prefetech on idle` deferred trigger. @@ -133,3 +203,222 @@ export function ɵɵdeferOnViewport(target?: unknown) {} // TODO: implement run * @codeGenApi */ export function ɵɵdeferPrefetchOnViewport(target?: unknown) {} // TODO: implement runtime logic. + +/********** Helper functions **********/ + +/** + * 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 lContainer Represents an instance of a defer block. + * @param stateTmplIndex Index of a template that should be rendered. + */ +function renderDeferBlockState( + newState: DeferBlockInstanceState, 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; + + ngDevMode && + assertDefined( + lContainer[DEFER_BLOCK_DETAILS], + 'Expected an LContainer that represents ' + + 'a defer block, but got a regular LContainer'); + + const lDetails = lContainer[DEFER_BLOCK_DETAILS]!; + + // 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.state < newState && stateTmplIndex !== null) { + lDetails.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, {}); + 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. + */ +function triggerResourceLoading(tDetails: TDeferBlockDetails, primaryBlockTNode: TNode) { + 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; + + // 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. + const dependenciesFn = tDetails.dependencyResolverFn; + if (!dependenciesFn) { + tDetails.loadingPromise = Promise.resolve().then(() => { + tDetails.loadingState = DeferDependenciesLoadingState.COMPLETE; + }); + return; + } + + // Start downloading... + tDetails.loadingPromise = Promise.allSettled(dependenciesFn()).then(results => { + let failedReason = null; + 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 { + failedReason = result.reason; + break; + } + } + + // Loading is completed, we no longer need this Promise. + tDetails.loadingPromise = null; + + if (failedReason) { + tDetails.loadingState = DeferDependenciesLoadingState.FAILED; + tDetails.loadingFailedReason = failedReason; + } 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 lContainer = lView[tNode.index]; + ngDevMode && assertLContainer(lContainer); + + const tDetails = tNode.value as TDeferBlockDetails; + renderDeferBlockState( + DeferBlockInstanceState.PLACEHOLDER, 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(lContainer: LContainer, tNode: TNode) { + const tDetails = tNode.value as TDeferBlockDetails; + + 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, lContainer, tDetails.primaryTmplIndex); + + } else if (tDetails.loadingState === DeferDependenciesLoadingState.FAILED) { + const hostLView = lContainer[PARENT]; + renderDeferBlockState(DeferBlockInstanceState.ERROR, lContainer, tDetails.errorTmplIndex); + if (!isDestroyed(hostLView)) { + console.error(tDetails.loadingFailedReason); + } + } + }); +} + +/** + * 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 lContainer = lView[tNode.index]; + ngDevMode && assertLContainer(lContainer); + + const tDetails = tNode.value as TDeferBlockDetails; + + // 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, 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); + + // The `loadingState` might have changed to "loading". + if ((tDetails.loadingState as DeferDependenciesLoadingState) === + DeferDependenciesLoadingState.IN_PROGRESS) { + renderDeferStateAfterResourceLoading(lContainer, tNode); + } + break; + case DeferDependenciesLoadingState.IN_PROGRESS: + renderDeferStateAfterResourceLoading(lContainer, tNode); + break; + case DeferDependenciesLoadingState.COMPLETE: + ngDevMode && assertDeferredDependenciesLoaded(tDetails); + renderDeferBlockState( + DeferBlockInstanceState.COMPLETE, lContainer, tDetails.primaryTmplIndex); + break; + case DeferDependenciesLoadingState.FAILED: + renderDeferBlockState(DeferBlockInstanceState.ERROR, 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.'); +} diff --git a/packages/core/src/render3/instructions/shared.ts b/packages/core/src/render3/instructions/shared.ts index 0be6f430b4b425..429597ea8ace26 100644 --- a/packages/core/src/render3/instructions/shared.ts +++ b/packages/core/src/render3/instructions/shared.ts @@ -31,7 +31,7 @@ import {CONTAINER_HEADER_OFFSET, LContainer} from '../interfaces/container'; import {ComponentDef, ComponentTemplate, DirectiveDef, DirectiveDefListOrFactory, HostBindingsFunction, HostDirectiveBindingMap, HostDirectiveDefs, PipeDefListOrFactory, RenderFlags, ViewQueriesFunction} from '../interfaces/definition'; import {NodeInjectorFactory} from '../interfaces/injector'; import {getUniqueLViewId} from '../interfaces/lview_tracking'; -import {AttributeMarker, InitialInputData, InitialInputs, LocalRefExtractor, PropertyAliases, PropertyAliasValue, TAttributes, TConstantsOrFactory, TContainerNode, TDirectiveHostNode, TElementContainerNode, TElementNode, TIcuContainerNode, TNode, TNodeFlags, TNodeType, TProjectionNode} from '../interfaces/node'; +import {AttributeMarker, InitialInputData, InitialInputs, LocalRefExtractor, PropertyAliases, PropertyAliasValue, TAttributes, TConstantsOrFactory, TContainerNode, TDeferBlockDetails, TDirectiveHostNode, TElementContainerNode, TElementNode, TIcuContainerNode, TNode, TNodeFlags, TNodeType, TProjectionNode} from '../interfaces/node'; import {Renderer} from '../interfaces/renderer'; import {RComment, RElement, RNode, RText} from '../interfaces/renderer_dom'; import {SanitizerFn} from '../interfaces/sanitization'; @@ -140,7 +140,7 @@ export function getOrCreateTNode( tView: TView, index: number, type: TNodeType.Element|TNodeType.Text, name: string|null, attrs: TAttributes|null): TElementNode; export function getOrCreateTNode( - tView: TView, index: number, type: TNodeType.Container, name: string|null, + tView: TView, index: number, type: TNodeType.Container, value: string|TDeferBlockDetails|null, attrs: TAttributes|null): TContainerNode; export function getOrCreateTNode( tView: TView, index: number, type: TNodeType.Projection, name: null, @@ -152,8 +152,9 @@ export function getOrCreateTNode( tView: TView, index: number, type: TNodeType.Icu, name: null, attrs: TAttributes|null): TElementContainerNode; export function getOrCreateTNode( - tView: TView, index: number, type: TNodeType, name: string|null, attrs: TAttributes|null): - TElementNode&TContainerNode&TElementContainerNode&TProjectionNode&TIcuContainerNode { + tView: TView, index: number, type: TNodeType, value: string|TDeferBlockDetails|null, + attrs: TAttributes|null): TElementNode&TContainerNode&TElementContainerNode&TProjectionNode& + TIcuContainerNode { ngDevMode && index !== 0 && // 0 are bogus nodes and they are OK. See `createContainerRef` in // `view_engine_compatibility` for additional context. assertGreaterThanOrEqual(index, HEADER_OFFSET, 'TNodes can\'t be in the LView header.'); @@ -161,7 +162,7 @@ export function getOrCreateTNode( ngDevMode && assertPureTNodeType(type); let tNode = tView.data[index] as TNode; if (tNode === null) { - tNode = createTNodeAtIndex(tView, index, type, name, attrs); + tNode = createTNodeAtIndex(tView, index, type, value, attrs); if (isInI18nBlock()) { // If we are in i18n block then all elements should be pre declared through `Placeholder` // See `TNodeType.Placeholder` and `LFrame.inI18n` for more context. @@ -171,7 +172,7 @@ export function getOrCreateTNode( } } else if (tNode.type & TNodeType.Placeholder) { tNode.type = type; - tNode.value = name; + tNode.value = value; tNode.attrs = attrs; const parent = getCurrentParentTNode(); tNode.injectorIndex = parent === null ? -1 : parent.injectorIndex; @@ -184,13 +185,14 @@ export function getOrCreateTNode( } export function createTNodeAtIndex( - tView: TView, index: number, type: TNodeType, name: string|null, attrs: TAttributes|null) { + tView: TView, index: number, type: TNodeType, value: string|TDeferBlockDetails|null, + attrs: TAttributes|null) { const currentTNode = getCurrentTNodePlaceholderOk(); const isParent = isCurrentTNodeParent(); const parent = isParent ? currentTNode : currentTNode && currentTNode.parent; // Parents cannot cross component boundaries because components will be used in multiple places. const tNode = tView.data[index] = - createTNode(tView, parent as TElementNode | TContainerNode, type, index, name, attrs); + createTNode(tView, parent as TElementNode | TContainerNode, type, index, value, attrs); // Assign a pointer to the first child node of a given view. The first node is not always the one // at index 0, in case of i18n, index 0 can be the instruction `i18nStart` and the first node has // the index 1 or more, so we can't just check node index. @@ -558,7 +560,7 @@ export function storeCleanupWithContext( */ export function createTNode( tView: TView, tParent: TElementNode|TContainerNode|null, type: TNodeType.Container, - index: number, tagName: string|null, attrs: TAttributes|null): TContainerNode; + index: number, value: string|TDeferBlockDetails|null, attrs: TAttributes|null): TContainerNode; export function createTNode( tView: TView, tParent: TElementNode|TContainerNode|null, type: TNodeType.Element|TNodeType.Text, index: number, tagName: string|null, attrs: TAttributes|null): TElementNode; @@ -573,10 +575,10 @@ export function createTNode( index: number, tagName: string|null, attrs: TAttributes|null): TProjectionNode; export function createTNode( tView: TView, tParent: TElementNode|TContainerNode|null, type: TNodeType, index: number, - tagName: string|null, attrs: TAttributes|null): TNode; + value: string|TDeferBlockDetails|null, attrs: TAttributes|null): TNode; export function createTNode( tView: TView, tParent: TElementNode|TContainerNode|null, type: TNodeType, index: number, - value: string|null, attrs: TAttributes|null): TNode { + value: string|TDeferBlockDetails|null, attrs: TAttributes|null): TNode { ngDevMode && index !== 0 && // 0 are bogus nodes and they are OK. See `createContainerRef` in // `view_engine_compatibility` for additional context. assertGreaterThanOrEqual(index, HEADER_OFFSET, 'TNodes can\'t be in the LView header.'); @@ -600,8 +602,8 @@ export function createTNode( propertyBindings: null, flags, providerIndexes: 0, - value: value, - attrs: attrs, + value, + attrs, mergedAttrs: null, localNames: null, initialInputs: undefined, @@ -1411,6 +1413,7 @@ export function createLContainer( null, // view refs null, // moved views null, // dehydrated views + null, // defer block details ]; ngDevMode && assertEqual( diff --git a/packages/core/src/render3/instructions/template.ts b/packages/core/src/render3/instructions/template.ts index f747f5974ec14b..50245f0541a63f 100644 --- a/packages/core/src/render3/instructions/template.ts +++ b/packages/core/src/render3/instructions/template.ts @@ -14,7 +14,7 @@ import {assertFirstCreatePass} from '../assert'; import {attachPatchData} from '../context_discovery'; import {registerPostOrderHooks} from '../hooks'; import {ComponentTemplate} from '../interfaces/definition'; -import {LocalRefExtractor, TAttributes, TContainerNode, TNode, TNodeType} from '../interfaces/node'; +import {LocalRefExtractor, TAttributes, TContainerNode, TDeferBlockDetails, TNode, TNodeType} from '../interfaces/node'; import {RComment} from '../interfaces/renderer_dom'; import {isDirectiveHost} from '../interfaces/type_checks'; import {HEADER_OFFSET, HYDRATION, LView, RENDERER, TView, TViewType} from '../interfaces/view'; @@ -26,7 +26,7 @@ import {addToViewTree, createDirectivesInstances, createLContainer, createTView, function templateFirstCreatePass( index: number, tView: TView, lView: LView, templateFn: ComponentTemplate|null, - decls: number, vars: number, tagName?: string|null, attrsIndex?: number|null, + decls: number, vars: number, value?: string|TDeferBlockDetails|null, attrsIndex?: number|null, localRefsIndex?: number|null): TContainerNode { ngDevMode && assertFirstCreatePass(tView); ngDevMode && ngDevMode.firstCreatePass++; @@ -34,7 +34,7 @@ function templateFirstCreatePass( // TODO(pk): refactor getOrCreateTNode to have the "create" only version const tNode = getOrCreateTNode( - tView, index, TNodeType.Container, tagName || null, + tView, index, TNodeType.Container, value || null, getConstant(tViewConsts, attrsIndex)); resolveDirectives(tView, lView, tNode, getConstant(tViewConsts, localRefsIndex)); @@ -75,14 +75,26 @@ export function ɵɵtemplate( index: number, templateFn: ComponentTemplate|null, decls: number, vars: number, tagName?: string|null, attrsIndex?: number|null, localRefsIndex?: number|null, localRefExtractor?: LocalRefExtractor) { + templateInternal( + index, templateFn, decls, vars, tagName, attrsIndex, localRefsIndex, localRefExtractor); +} + +export function templateInternal( + index: number, templateFn: ComponentTemplate|null, decls: number, vars: number, + value?: string|(() => TDeferBlockDetails)|null, attrsIndex?: number|null, + localRefsIndex?: number|null, localRefExtractor?: LocalRefExtractor) { const lView = getLView(); const tView = getTView(); - const adjustedIndex = index + HEADER_OFFSET; - const tNode = tView.firstCreatePass ? templateFirstCreatePass( - adjustedIndex, tView, lView, templateFn, decls, vars, - tagName, attrsIndex, localRefsIndex) : - tView.data[adjustedIndex] as TContainerNode; + const adjustedIndex = index + HEADER_OFFSET; + const tNode = tView.firstCreatePass ? + templateFirstCreatePass( + adjustedIndex, tView, lView, templateFn, decls, vars, + // Defer block details are needed only once during the first creation pass, + // so we pass a function and invoke it here to avoid re-constructing the same + // object for each subsequent creation run. + (typeof value === 'function' ? value() : value), attrsIndex, localRefsIndex) : + tView.data[adjustedIndex] as TContainerNode; setCurrentTNode(tNode, false); const comment = _locateOrCreateContainerAnchor(tView, lView, tNode, index) as RComment; diff --git a/packages/core/src/render3/interfaces/container.ts b/packages/core/src/render3/interfaces/container.ts index 524874ea4f5735..6d296a2f1647df 100644 --- a/packages/core/src/render3/interfaces/container.ts +++ b/packages/core/src/render3/interfaces/container.ts @@ -47,6 +47,7 @@ export const NATIVE = 7; export const VIEW_REFS = 8; export const MOVED_VIEWS = 9; export const DEHYDRATED_VIEWS = 10; +export const DEFER_BLOCK_DETAILS = 11; /** @@ -55,7 +56,38 @@ export const DEHYDRATED_VIEWS = 10; * which views are already in the DOM (and don't need to be re-added) and so we can * remove views from the DOM when they are no longer required. */ -export const CONTAINER_HEADER_OFFSET = 11; +export const CONTAINER_HEADER_OFFSET = 12; + +/** + * 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 +} + +/** + * Describes instance-specific {#defer} block data. + * + * Note: currently there is only the `state` field, but more fields + * would be added later to keep track of `after` and `maximum` features + * (which would require per-instance state). + */ +export interface LDeferBlockDetails { + state: DeferBlockInstanceState; +} /** * The state associated with a container. @@ -144,6 +176,12 @@ export interface LContainer extends Array { * logic finishes. */ [DEHYDRATED_VIEWS]: DehydratedContainerView[]|null; + + /** + * If this LContainer represents an instance of a `{#defer}` block - + * this field contains instance-specific information about the block. + */ + [DEFER_BLOCK_DETAILS]: LDeferBlockDetails|null; } // Note: This hack is necessary so we don't erroneously get a circular dependency diff --git a/packages/core/src/render3/interfaces/definition.ts b/packages/core/src/render3/interfaces/definition.ts index 6d9719e916b290..675f9f291ad792 100644 --- a/packages/core/src/render3/interfaces/definition.ts +++ b/packages/core/src/render3/interfaces/definition.ts @@ -491,7 +491,11 @@ 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 DependencyResolverFn = () => Array>; export type TypeOrFactory = T|(() => T); diff --git a/packages/core/src/render3/interfaces/node.ts b/packages/core/src/render3/interfaces/node.ts index b51e350cf76158..3c8c89aea1d073 100644 --- a/packages/core/src/render3/interfaces/node.ts +++ b/packages/core/src/render3/interfaces/node.ts @@ -8,6 +8,7 @@ import {KeyValueArray} from '../../util/array_utils'; import {TStylingRange} from '../interfaces/styling'; +import {DependencyResolverFn} from './definition'; import {TIcu} from './i18n'; import {CssSelector} from './projection'; import {RNode} from './renderer_dom'; @@ -858,6 +859,94 @@ export interface TProjectionNode extends TNode { value: null; } +/** + * 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; + + /** + * If dependency loading fails, this field retains the error message, which can + * be accessed by all instances of a defer block. + */ + loadingFailedReason: string|null; +} + /** * A union type representing all TNode types that can host a directive. */ diff --git a/packages/core/src/render3/interfaces/type_checks.ts b/packages/core/src/render3/interfaces/type_checks.ts index 5d2a67434e890e..3239b4cc25221c 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/test/acceptance/defer_spec.ts b/packages/core/test/acceptance/defer_spec.ts new file mode 100644 index 00000000000000..0dcbd5fc75d8ff --- /dev/null +++ b/packages/core/test/acceptance/defer_spec.ts @@ -0,0 +1,66 @@ +/** + * @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, QueryList, ViewChildren} from '@angular/core'; +import {TestBed} from '@angular/core/testing'; + +describe('#defer', () => { + beforeEach(() => setEnabledBlockTypes(['defer'])); + afterEach(() => setEnabledBlockTypes([])); + + fit('should work with basic cases', 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} + Ooops :( + {/defer} + ` + }) + class MyCmp { + isVisible = false; + + @ViewChildren(MyLazyCmp) cmps!: QueryList; + } + + const fixture = TestBed.createComponent(MyCmp); + fixture.detectChanges(); + + expect(fixture.componentInstance.cmps.length).toBe(0); + expect(fixture.nativeElement.outerHTML).toContain('Placeholder'); + + fixture.componentInstance.isVisible = true; + fixture.detectChanges(); + + expect(fixture.componentInstance.cmps.length).toBe(0); + expect(fixture.nativeElement.outerHTML).toContain('Loading'); + + await fixture.whenStable(); // loading dependencies... + fixture.detectChanges(); + + expect(fixture.componentInstance.cmps.length).toBe(1); + expect(fixture.nativeElement.outerHTML).toContain('Hi!'); + }); +});