@@ -17,7 +17,7 @@ import {assertIndexInDeclRange, assertLContainer, assertLView, assertTNodeForLVi
1717import { bindingUpdated } from '../bindings' ;
1818import { getComponentDef , getDirectiveDef , getPipeDef } from '../definition' ;
1919import { 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' ;
2121import { DependencyDef , DirectiveDefList , PipeDefList } from '../interfaces/definition' ;
2222import { TContainerNode , TNode } from '../interfaces/node' ;
2323import { 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(
622646export 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 :
0 commit comments