I don’t really know much about React. And, I certainly know nothing about the upcoming “Suspense” feature. But, from what I’ve heard various people say on various podcasts, it seems that one of the features provided by Suspense is the ability to delay the rendering of “Loading Indicators” in an attempt to create a better user experience (UX). I am sure there is a lot more to it than what I’ve picked up; but, based on this superficial understanding, I wanted to see if I could create a similar type of delay using CSS animations in Angular 9.0.0-next.14.

So, from what I think I’ve heard people discuss, showing a loading indicator can sometimes increase the perception of latency if the latency is relatively low. And, apparently, in low-latency contexts, the user interface will seem faster if you omit the loading indicator altogether and just show a “blank screen” before the content loads.

Honestly, I am not sure how I feel about this. But, I’m not opposed to trying it out for myself. And, it seems that this type of behavior can be easily accomplished with a simple CSS Animation delay. The idea here being that the calling context won’t manage the delay - the calling context simply shows and hides the loading indicator the same it would traditionally. The loading indicator would then manage the delay internally using the animatable CSS property, opacity.

Before we look at the loader, though, let’s look at the App component. To experiment with this idea, the App component will toggle the display of a “content area” that is delayed by a simulated network request. While the simulated network request is pending, the App component will show a loading indicator, passing-in a [delay] property.

There are several toggle() calls, each of which uses a different [delay] value for the loading indicator. This will help us get a sense of how different delays affect the perception of latency:

// Import the core angular services.
import { Component } from "@angular/core";

// ----------------------------------------------------------------------------------- //
// ----------------------------------------------------------------------------------- //

@Component({
    selector: "app-root",
    styleUrls: [ "./app.component.less" ],
    template:
    `
        <p>
            Toggle Container with delay:
            <a (click)="toggle( 0 )" class="toggle">0ms</a>,
            <a (click)="toggle( 300 )" class="toggle">300ms</a>,
            <a (click)="toggle( 500 )" class="toggle">500ms</a>,
            <a (click)="toggle( 1000 )" class="toggle">1,000ms</a>
        </p>
        <section *ngIf="isShowingContainer">
            <!--
                The [delay] property determines the number of MS to wait before the
                loading indicator is renderered. By pushing this logic into the loader,
                it keeps the calling logic simple and binary (ie, show / don't show). The
                underlying THEORY here is that the precense of the loading indicator can
                increase the perceived delay when the latency is relatively low.
            -->
            <app-loader
                *ngIf="isLoading"
                [delay]="delay"
                class="loader">
            </app-loader>
            <div *ngIf="( ! isLoading )">
                From the corner of the gym where the BIG men train,<br />
                Through a cloud of chalk and the midst of pain<br />
                Where the big iron rides high and threatens lives,<br />
                Where the noise is made with big forty-fives,<br />
                A deep voice bellowed as he wrapped his knees,<br />
                A very big man with legs like trees.<br />
                Laughing as he snatched another plate from the stack<br />
                Chalking his hands and monstrous back,<br />
                said, "Boy, stop lying and don't say you've forgotten,<br />
                The trouble with you is you ain't been SQUATTIN'."<br />
                &mdash;DALE CLARK, 1983
            </div>
        </section>
    `
})
export class AppComponent {

    public delay: number;
    public isLoading: boolean;
    public isShowingContainer: boolean;

    // I initialize the app component.
    constructor() {

        this.delay = 0;
        this.isLoading = false;
        this.isShowingContainer = false;

    }

    // ---
    // PUBLIC METHODS.
    // ---

    // I toggle the visibility of the data container, which triggers a "network" request
    // using the given delay for the loading indicator.
    public toggle( newDelay: number ) : void {

        this.delay = newDelay;

        // Toggle the container closed.
        if ( this.isShowingContainer ) {

            this.isShowingContainer = false;
            this.isLoading = false;

        // Toggle the container opened.
        } else {

            this.isShowingContainer = true;
            this.isLoading = true;

            this.getData().then(
                () => {

                    this.isLoading = false;

                }
            );

        }

    }

    // ---
    // PRIVATE METHODS.
    // ---

    // I simulate a network request with a random amount of latency.
    private getData( maxLatency: number = 1000 ) : Promise<void> {

        var promise = new Promise<void>(
            ( resolve ) => {

                var latency = Math.floor( Math.random() * maxLatency );

                console.log( "Data Fetch Latency:", latency, "ms" );
                setTimeout( resolve, latency );
                
            }
        );

        return( promise );

    }

}

As you can see, the presence of the loading indicator is controlled by a simple *ngIf=“isLoading” directive. The App component only cares about whether or not the loading indicator is present. It defers all of the “delay” implementation to the loading indicator itself.

So, let’s look at the loading indicator component. The component logic is minimal, just showing the text, Loading…:

// Import the core angular services.
import { Component } from "@angular/core";

// ----------------------------------------------------------------------------------- //
// ----------------------------------------------------------------------------------- //

@Component({
    selector: "app-loader",
    inputs: [ "delay" ],
    host: {
        "[style.animation-delay.ms]": "delay"
    },
    styleUrls: [ "./loader.component.less" ],
    template:
    `
        <div class="indicator">
            Loading....
        </div>
    `
})
export class LoaderComponent {

    public delay: number = 0;

}

For the implementation of this loader, we’re going to use CSS animation to delay the rendering of the loader on the screen. Well, to be clear, the loader will be there immediately; however, it will be transparent with an opacity of 0.

Notice that we have the following host binding:

“[style.animation-delay.ms]”: “delay”

In this implementation, the [delay] input is being parled into a Style property, animation-delay. This is then used in the LESS file to determine when the opacity property gets flipped from 0 to 1:

:host {
    animation-delay: 0ms ; // This will be overridden in the HTML template.
    animation-duration: 250ms ;
    animation-fill-mode: both ;
    animation-name: loader-component-keyframes ;
    display: flex ;
}

// The opacity property is going to be used to drive the visibility of the loader. It
// will start out as transparent (ie, not visible); and then, the animation-delay
// property defined in the component template (in conjunction with the keyframes) will
// determine how long to wait before the indicator is rendered for the user.
// --
// NOTE: Using animation-fill-mode of "both" causes the 0% state to be applied to the
// component when it is first rendered.
@keyframes loader-component-keyframes {
    0% {
        opacity: 0.0 ;
    }

    100% {
        opacity: 1.0 ;
    }
}

.indicator {
    flex: 0 0 auto ;
    margin: auto auto auto auto ; // Center vertically and horizontally.
}

In this case, we’re using the animation-fill-mode value of both to indicate that the 0% keyframe should style the component prior to the animation; and, that the 100% keyframe should style the component following the animation. Which means, from the initial rendering though to the animation-delay, the loading indicator will be transparent. And then, once the animation kicks off, the loading indicator will become opaque.

Now, when we run this Angular application in the browser and trying toggling the content area using different delay values, we get the following output:

Angular 9 Tutorials: Delaying Loading Indicators Using CSS Animations

This is not an easy thing to see in an animated GIF. I suggest trying it out for yourself. That said, there may be a bit of sweet-spot, maybe somewhere in the sub-300ms delay range? But, again, I’m not sure how I feel about it. And, I would likely have to see this in action in a robust Angular application before I could really get a sense of whether or not it is worth it. Of course, by encapsulating the delay implementation within the loading component itself, this is something that could be added, removed, or modified in a cross-cutting way once the application has been built.

As a reminder, I know nothing about React Suspense. So, please forgive me if any of understandings here are, in fact, misunderstandings. That said, I am intrigued by the idea of delaying the rendering of a loading indicator in an attempt to decrease the perceived latency by the user. And, I love how easy it is to implement this kind of delay using CSS animations, host properties, and input bindings in Angular 9.0.0-next.14.

View this code in my JavaScript Demos project on GitHub.

via BenNadel

#angular #angularjs

Angular 9 Tutorials: Delaying Loading Indicators Using CSS Animations
3 Likes51.15 GEEK