Skip to content

Commit caa8eb2

Browse files
AndrewKushniratscott
authored andcommitted
refactor(core): add after and minimum parameter support to @defer blocks (angular#52009)
This commit adds runtime code to support `after` and `minimum` parameters in the `@placeholder` and `@loading` blocks. The code uses the `TimerScheduler` service added earlier for `on timer` triggers. PR Close angular#52009
1 parent 40c5357 commit caa8eb2

File tree

3 files changed

+389
-21
lines changed

3 files changed

+389
-21
lines changed

packages/core/src/render3/instructions/defer.ts

Lines changed: 118 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ import {assertIndexInDeclRange, assertLContainer, assertLView, assertTNodeForLVi
1717
import {bindingUpdated} from '../bindings';
1818
import {getComponentDef, getDirectiveDef, getPipeDef} from '../definition';
1919
import {CONTAINER_HEADER_OFFSET, LContainer} from '../interfaces/container';
20-
import {DEFER_BLOCK_STATE, DeferBlockBehavior, DeferBlockConfig, DeferBlockInternalState, DeferBlockState, DeferBlockTriggers, DeferDependenciesLoadingState, DeferredLoadingBlockConfig, DeferredPlaceholderBlockConfig, DependencyResolverFn, LDeferBlockDetails, TDeferBlockDetails} from '../interfaces/defer';
20+
import {DEFER_BLOCK_STATE, DeferBlockBehavior, DeferBlockConfig, DeferBlockInternalState, DeferBlockState, DeferBlockTriggers, DeferDependenciesLoadingState, DeferredLoadingBlockConfig, DeferredPlaceholderBlockConfig, DependencyResolverFn, LDeferBlockDetails, LOADING_AFTER_CLEANUP_FN, LOADING_AFTER_SLOT, MINIMUM_SLOT, NEXT_DEFER_BLOCK_STATE, STATE_IS_FROZEN_UNTIL, TDeferBlockDetails} from '../interfaces/defer';
2121
import {DependencyDef, DirectiveDefList, PipeDefList} from '../interfaces/definition';
2222
import {TContainerNode, TNode} from '../interfaces/node';
2323
import {isDestroyed, isLContainer, isLView} from '../interfaces/type_checks';
@@ -101,9 +101,13 @@ export function ɵɵdefer(
101101
populateDehydratedViewsInLContainer(lContainer, tNode, lView);
102102

103103
// Init instance-specific defer details and store it.
104-
const lDetails = [];
105-
lDetails[DEFER_BLOCK_STATE] = DeferBlockInternalState.Initial;
106-
setLDeferBlockDetails(lView, adjustedIndex, lDetails as LDeferBlockDetails);
104+
const lDetails: LDeferBlockDetails = [
105+
null, // NEXT_DEFER_BLOCK_STATE
106+
DeferBlockInternalState.Initial, // DEFER_BLOCK_STATE
107+
null, // STATE_IS_FROZEN_UNTIL
108+
null // LOADING_AFTER_CLEANUP_FN
109+
];
110+
setLDeferBlockDetails(lView, adjustedIndex, lDetails);
107111
}
108112

109113
/**
@@ -611,6 +615,26 @@ function getTemplateIndexForState(
611615
}
612616
}
613617

618+
/**
619+
* Returns a minimum amount of time that a given state should be rendered for,
620+
* taking into account `minimum` parameter value. If the `minimum` value is
621+
* not specified - returns `null`.
622+
*/
623+
function getMinimumDurationForState(
624+
tDetails: TDeferBlockDetails, currentState: DeferBlockState): number|null {
625+
if (currentState === DeferBlockState.Placeholder) {
626+
return tDetails.placeholderBlockConfig?.[MINIMUM_SLOT] ?? null;
627+
} else if (currentState === DeferBlockState.Loading) {
628+
return tDetails.loadingBlockConfig?.[MINIMUM_SLOT] ?? null;
629+
}
630+
return null;
631+
}
632+
633+
/** Retrieves the value of the `after` parameter on the @loading block. */
634+
function getLoadingBlockAfter(tDetails: TDeferBlockDetails): number|null {
635+
return tDetails.loadingBlockConfig?.[LOADING_AFTER_SLOT] ?? null;
636+
}
637+
614638
/**
615639
* Transitions a defer block to the new state. Updates the necessary
616640
* data structures and renders corresponding block.
@@ -622,6 +646,7 @@ function getTemplateIndexForState(
622646
export function renderDeferBlockState(
623647
newState: DeferBlockState, tNode: TNode, lContainer: LContainer): void {
624648
const hostLView = lContainer[PARENT];
649+
const hostTView = hostLView[TVIEW];
625650

626651
// Check if this view is not destroyed. Since the loading process was async,
627652
// the view might end up being destroyed by the time rendering happens.
@@ -631,15 +656,97 @@ export function renderDeferBlockState(
631656
ngDevMode && assertTNodeForLView(tNode, hostLView);
632657

633658
const lDetails = getLDeferBlockDetails(hostLView, tNode);
659+
const tDetails = getTDeferBlockDetails(hostTView, tNode);
634660

635661
ngDevMode && assertDefined(lDetails, 'Expected a defer block state defined');
636662

663+
const now = Date.now();
664+
const currentState = lDetails[DEFER_BLOCK_STATE];
665+
666+
if (!isValidStateChange(currentState, newState) ||
667+
!isValidStateChange(lDetails[NEXT_DEFER_BLOCK_STATE] ?? -1, newState))
668+
return;
669+
670+
if (lDetails[STATE_IS_FROZEN_UNTIL] === null || lDetails[STATE_IS_FROZEN_UNTIL] <= now) {
671+
lDetails[STATE_IS_FROZEN_UNTIL] = null;
672+
673+
const loadingAfter = getLoadingBlockAfter(tDetails);
674+
const inLoadingAfterPhase = lDetails[LOADING_AFTER_CLEANUP_FN] !== null;
675+
if (newState === DeferBlockState.Loading && loadingAfter !== null && !inLoadingAfterPhase) {
676+
// Trying to render loading, but it has an `after` config,
677+
// so schedule an update action after a timeout.
678+
lDetails[NEXT_DEFER_BLOCK_STATE] = newState;
679+
const cleanupFn =
680+
scheduleDeferBlockUpdate(loadingAfter, lDetails, tNode, lContainer, hostLView);
681+
lDetails[LOADING_AFTER_CLEANUP_FN] = cleanupFn;
682+
} else {
683+
// If we transition to a complete or an error state and there is a pending
684+
// operation to render loading after a timeout - invoke a cleanup operation,
685+
// which stops the timer.
686+
if (newState > DeferBlockState.Loading && inLoadingAfterPhase) {
687+
lDetails[LOADING_AFTER_CLEANUP_FN]!();
688+
lDetails[LOADING_AFTER_CLEANUP_FN] = null;
689+
lDetails[NEXT_DEFER_BLOCK_STATE] = null;
690+
}
691+
692+
applyDeferBlockStateToDom(newState, lDetails, lContainer, hostLView, tNode);
693+
694+
const duration = getMinimumDurationForState(tDetails, newState);
695+
if (duration !== null) {
696+
lDetails[STATE_IS_FROZEN_UNTIL] = now + duration;
697+
scheduleDeferBlockUpdate(duration, lDetails, tNode, lContainer, hostLView);
698+
}
699+
}
700+
} else {
701+
// We are still rendering the previous state.
702+
// Update the `NEXT_DEFER_BLOCK_STATE`, which would be
703+
// picked up once it's time to transition to the next state.
704+
lDetails[NEXT_DEFER_BLOCK_STATE] = newState;
705+
}
706+
}
707+
708+
/**
709+
* Schedules an update operation after a specified timeout.
710+
*/
711+
function scheduleDeferBlockUpdate(
712+
timeout: number, lDetails: LDeferBlockDetails, tNode: TNode, lContainer: LContainer,
713+
hostLView: LView<unknown>): VoidFunction {
714+
const callback = () => {
715+
const nextState = lDetails[NEXT_DEFER_BLOCK_STATE];
716+
lDetails[STATE_IS_FROZEN_UNTIL] = null;
717+
lDetails[NEXT_DEFER_BLOCK_STATE] = null;
718+
if (nextState !== null) {
719+
renderDeferBlockState(nextState, tNode, lContainer);
720+
}
721+
};
722+
// TODO: this needs refactoring to make `TimerScheduler` that is used inside
723+
// of the `scheduleTimerTrigger` function tree-shakable.
724+
return scheduleTimerTrigger(timeout, callback, hostLView, true);
725+
}
726+
727+
/**
728+
* Checks whether we can transition to the next state.
729+
*
730+
* We transition to the next state if the previous state was represented
731+
* with a number that is less than the next state. For example, if the current
732+
* state is "loading" (represented as `1`), we should not show a placeholder
733+
* (represented as `0`), but we can show a completed state (represented as `2`)
734+
* or an error state (represented as `3`).
735+
*/
736+
function isValidStateChange(
737+
currentState: DeferBlockState|DeferBlockInternalState, newState: DeferBlockState): boolean {
738+
return currentState < newState;
739+
}
740+
741+
/**
742+
* Applies changes to the DOM to reflect a given state.
743+
*/
744+
function applyDeferBlockStateToDom(
745+
newState: DeferBlockState, lDetails: LDeferBlockDetails, lContainer: LContainer,
746+
hostLView: LView<unknown>, tNode: TNode) {
637747
const stateTmplIndex = getTemplateIndexForState(newState, hostLView, tNode);
638-
// Note: we transition to the next state if the previous state was represented
639-
// with a number that is less than the next state. For example, if the current
640-
// state is "loading" (represented as `2`), we should not show a placeholder
641-
// (represented as `1`).
642-
if (lDetails[DEFER_BLOCK_STATE] < newState && stateTmplIndex !== null) {
748+
749+
if (stateTmplIndex !== null) {
643750
lDetails[DEFER_BLOCK_STATE] = newState;
644751
const hostTView = hostLView[TVIEW];
645752
const adjustedIndex = stateTmplIndex + HEADER_OFFSET;
@@ -832,13 +939,9 @@ function triggerDeferBlock(lView: LView, tNode: TNode) {
832939
if (!shouldTriggerDeferBlock(injector)) return;
833940

834941
const tDetails = getTDeferBlockDetails(tView, tNode);
835-
836-
// Condition is triggered, try to render loading state and start downloading.
837-
// Note: if a block is in a loading, completed or an error state, this call would be a noop.
838-
renderDeferBlockState(DeferBlockState.Loading, tNode, lContainer);
839-
840942
switch (tDetails.loadingState) {
841943
case DeferDependenciesLoadingState.NOT_STARTED:
944+
renderDeferBlockState(DeferBlockState.Loading, tNode, lContainer);
842945
triggerResourceLoading(tDetails, lView);
843946

844947
// The `loadingState` might have changed to "loading".
@@ -848,6 +951,7 @@ function triggerDeferBlock(lView: LView, tNode: TNode) {
848951
}
849952
break;
850953
case DeferDependenciesLoadingState.IN_PROGRESS:
954+
renderDeferBlockState(DeferBlockState.Loading, tNode, lContainer);
851955
renderDeferStateAfterResourceLoading(tDetails, tNode, lContainer);
852956
break;
853957
case DeferDependenciesLoadingState.COMPLETE:

packages/core/src/render3/interfaces/defer.ts

Lines changed: 36 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -43,11 +43,17 @@ export enum DeferDependenciesLoadingState {
4343
FAILED,
4444
}
4545

46+
/** Slot index where `minimum` parameter value is stored. */
47+
export const MINIMUM_SLOT = 0;
48+
49+
/** Slot index where `after` parameter value is stored. */
50+
export const LOADING_AFTER_SLOT = 1;
51+
4652
/** Configuration object for a loading block as it is stored in the component constants. */
4753
export type DeferredLoadingBlockConfig = [minimumTime: number|null, afterTime: number|null];
4854

4955
/** Configuration object for a placeholder block as it is stored in the component constants. */
50-
export type DeferredPlaceholderBlockConfig = [afterTime: number|null];
56+
export type DeferredPlaceholderBlockConfig = [minimumTime: number|null];
5157

5258
/**
5359
* Describes the data shared across all instances of a defer block.
@@ -133,11 +139,14 @@ export enum DeferBlockInternalState {
133139
Initial = -1,
134140
}
135141

136-
/**
137-
* A slot in the `LDeferBlockDetails` array that contains a number
138-
* that represent a current block state that is being rendered.
139-
*/
140-
export const DEFER_BLOCK_STATE = 0;
142+
export const NEXT_DEFER_BLOCK_STATE = 0;
143+
// Note: it's *important* to keep the state in this slot, because this slot
144+
// is used by runtime logic to differentiate between LViews, LContainers and
145+
// other types (see `isLView` and `isLContainer` functions). In case of defer
146+
// blocks, this slot would always be a number.
147+
export const DEFER_BLOCK_STATE = 1;
148+
export const STATE_IS_FROZEN_UNTIL = 2;
149+
export const LOADING_AFTER_CLEANUP_FN = 3;
141150

142151
/**
143152
* Describes instance-specific defer block data.
@@ -147,7 +156,28 @@ export const DEFER_BLOCK_STATE = 0;
147156
* (which would require per-instance state).
148157
*/
149158
export interface LDeferBlockDetails extends Array<unknown> {
159+
/**
160+
* Currently rendered block state.
161+
*/
150162
[DEFER_BLOCK_STATE]: DeferBlockState|DeferBlockInternalState;
163+
164+
/**
165+
* Block state that was requested when another state was rendered.
166+
*/
167+
[NEXT_DEFER_BLOCK_STATE]: DeferBlockState|null;
168+
169+
/**
170+
* Timestamp indicating when the current state can be switched to
171+
* the next one, in case teh current state has `minimum` parameter.
172+
*/
173+
[STATE_IS_FROZEN_UNTIL]: number|null;
174+
175+
/**
176+
* Contains a reference to a cleanup function which cancels a timeout
177+
* when Angular waits before rendering loading state. This is used when
178+
* the loading block has the `after` parameter configured.
179+
*/
180+
[LOADING_AFTER_CLEANUP_FN]: VoidFunction|null;
151181
}
152182

153183
/**

0 commit comments

Comments
 (0)