WebCake

One of the challenges of Angular 1.x template rendering was the possibility of a digest cycle attempting to process an object property when an object was not yet defined. This would inherently throw an error, and would break Angular. There were probably a lot of solutions to this, but for me a simple ng-if attribute blocking a possibly undefined object from entering the DOM was the way to go.

Now that we’re looking at Angular 2, I’m noticing that the same problem still exists, as templates are compiled in JS (I’d imagine) in a somewhat similar way. I had seen other examples built for UX purposes, but nothing that actually blocked rendering in the way that I was looking for.

#TL;DR: If you’re not interested in all of the details of how this came together, feel free to browse the final product on GitHub.

It should be noted that while this will still work, I’ve abandoned this solution in favor of a more componentized approach, which I introduce in a later post.

Waiting for Async Data

When I started including backend calls in my Ionic 2 app, I quickly noticed that I needed to provide a solution similar to the one outlined above. Each page that depends on server-side data needed to have some conditional applied in order to block certain aspects of the template from entering the DOM, attempting to compile, and subsequently throwing an error.

As a result, I ended up putting together a LoadingPage class, which all other pages in my app extend in order to intelligently display a spinner.

Introducing ngSwitch

My first attempt at a blocking spinner was done completely in the template, and wasn’t very nice to look at. I added a simple SVG spinner that used an inline style tag to define its spin, just to get something moving. Here’s a summary of how the template looked:

<div [ngSwitch]='status'>
    <div *ngSwitchCase="'loading'">
        <!-- all of the inline styles and massive SVG markup for my spinner -->
    </div>
    <div *ngSwitchCase="'active'">
        <form>
            <ion-list>
                <ion-item clearInput>
                    <ion-label stacked>Email Address</ion-label>
                    <ion-input [(ngModel)]="userEmail"
                               type="email"
                               required
                               #emailInput="ngModel"></ion-input>
                </ion-item>
                <ion-item clearInput>
                    <ion-label stacked>Password</ion-label>
                    <ion-input [(ngModel)]="password"
                               type="password"
                               required
                               #passwordInput="ngModel"></ion-input>
                </ion-item>

                <button block (click)="login()" [disabled]="emailInput.invalid || passwordInput.invalid">
                    Sign In
                </button>
            </ion-list>
        </form>
    </div>
</div>

If that looks to you like a Login page, that’s because it is. The loading indicator was first used to block the login form while I waited to see if the user was still authenticated. If so, they were redirected to another screen where even more async information was loaded, and if not they were shown the form above.

This was my first use of ngSwitch, which I certainly like as an improvement over several stacked ng-if conditionals, as we’ve all begrudgingly written/seen in plenty of Angular 1.x apps.

In my class definition, I simply apply some initial values, and some supporting logic based on the authentication status:

export class LoginPage {
    public userEmail: string;
    public password: string;
    public status: string;

    constructor(private _nav: NavController, private _user: UserService) {
        this.status = 'loading';
    }

    ionViewWillEnter() {
        const auth: AuthModel = this._user.getUser();
        if(auth) {
            this._nav.setRoot(TabsPage);
        } else {
            this.status = 'active';
        }
    }

    // some more supporting/validation methods...

}

Kind of speaks for itself – I use a UserService to check the auth status, and the return value determines the value of the status class property. The setRoot() method was how I dealt with various page stacks in Ionic 2, something I discussed in a previous blog post.

Standardizing the Template

Once I started building more pages like this, I began to notice some inconsistencies and points for improvement that I was able to solidify once I brought everything into a single component. First and foremost, the property in the ngSwitch was almost never the same whenever I created a new page. Secondly, my templates were getting hideous, with all of the SVG string dropped into each template’s switch case. Third, I had a lot of duplicate code that I really wanted to get rid of – e.g. setting a public property on every page for the ngSwitch, or explicitly setting that property’s string whenever I need to toggle the active view.

The solution I came up with was two-fold. First, I moved the spinner markup into a Component that all pages can access. That was extremely simple, as it’s just an empty class with an template set in its Component decorator:

import {Component} from 'angular2/core';

@Component({
    selector: 'loading-indicator',
    template: `
        <!-- for the sake of brevity, I'm omitting the full template -->
        <!-- you can find it by visiting the GitHub link at the top  -->
    `
})
export class LoadingIndicator {}

Crazy simple. No logic, no properties; just a template for an SVG that spins. At this point I was able to include it in place of the massive SVG block in all of my loading pages by simply dropping in the selector:

<div [ngSwitch]='status'>
    <div *ngSwitchCase="'loading'">
        <!-- YEAH BUDDY! -->
        <loading-indicator></loading-indicator>
    </div>
    <div *ngSwitchCase="'active'">
        <form>
            <!-- the login inputs from earlier -->
        </form>
    </div>
</div>

Extending a LoadingPage

In order to get rid of duplicate code, I standardized the public property in the ngSwitch, and then created a base class that all of my pages waiting for async data would extend. If you’ve clicked on the link above, you’ve probably seen it:

export class LoadingPage {
    public loading: boolean;
    constructor(val: boolean) {
        this.loading = val;
    }
    standby() {
        this.loading = true;
    }
    ready() {
        this.loading = false;
    }
}

Now all of the pages that are dependent on async data can extend this class and utilize its methods:

import {LoadingIndicator, LoadingPage} from '../../components/loading-indicator';

@Component({
    selector: 'login-page',
    templateUrl: 'build/pages/login/login.html'
})
export class LoginPage extends LoadingPage {
    public userEmail: string;
    public password: string;

    constructor(private _nav: NavController, private _user: UserService) {
        super(true);
    }

    ionViewWillEnter() {
        const auth: AuthModel = this._user.getUser();
        if(auth) {
            this._nav.setRoot(TabsPage);
        } else {
            this.ready();
        }
    }

    // more stuff...
}

Now with a little styling it’s ready to go! Here’s the scss block I included to center it:

loading-indicator {
  position: absolute;
  width: 100%;
  height: 100%;
  background: white;
  > div {
    position: absolute;
    -webkit-transform: translate(-50%,-50%) scale(0.34);
    transform: translate(-50%,-50%) scale(0.34);
    top: 190px;
    left: 50%;
  }
}

This could certainly be extended with animations and whatnot, but I’m just looking at an MVP right now, so that’s beyond my stopping point. Feel free to add your own.

You can find the finished version on my GitHub.

Screen Shot 2016-04-16 at 6.33.21 PM

Design Decisions

It could easily be argued that this isn’t a very elegant solution – that a better solution would be to use a component that takes an embedded template and blocks that based on certain conditions.

As it were, that was my first approach. I learned a lot about how Angular renders the DOM in the process of creating this simple component; most of the learning was done while trying to add complexity for a more Angular-looking component.

Take, for example, the following:

<!-- DON'T USE THIS -->
<loading-indicator>
    <div *ngFor='#item in object.items'>
        <!-- stuff... -->
    </div>
</loading-indicator>

So that’s meant to give you an idea of the original approach I took. In this case, the loading-indicator blocked the embedded template until certain conditions were met.

The problem was that the repeater over object.items would actually render before the loading-indicator. Since object was async Angular would throw an error, and break the app before the template-blocking logic could even be loaded.

This was bad. I tried a bunch of things before hitting the Googles, and ultimately finding this blog post by Minko Gechev, in which he clearly (in bold letters) says:

Since Angular’s DOM compiler will process the todo-app component before its children, during the instantiation of the todo-app component the inputComponent and todosComponen properties will not be initialized. Their values are going to be set in the ngAfterViewInit life-cycle hook.

That was a solid indication that any template markup would be processed before the loading-indicator component, meaning that the attempt to repeat over object.items would always happen before the blocking logic of the loading-indicator removes it from the DOM.

There’s a possibility that if I had removed the part of the template that threw an error and instead put it into another component I might not have had a problem. While that’s the case, it would have locked the dev using this component into always separating the internal template out to another file, which can be annoying if the template isn’t very complex.

Alternatively, the approach above works both ways – with DOM-blocked markup living inline, or in a separate component. Ultimately I figured that was the better approach.

Still, I’m happy to hear from anyone who disagrees in an attempt to improve.

10 responses to “A Loading Spinner in Angular 2 Using ngSwitch”

  1. Ofer Gal says:

    My code uses subscribe for the async call to the data source in ngOnInit() this._KeyDocumentService.getKeyDocuments(this.typeOfDocs)
    .subscribe(
    KeyDocuments => this.onDOcsRetrieved(KeyDocuments),
    error => this.errorMessage = error);
    Where do I put your this.ready();

    • Colin says:

      You’d add it at whatever point in your flow where you’re ready to display data to the user. So in this case, you might add it as a second function on your subscribe callback:

      this._KeyDocumentService.getKeyDocuments(this.typeOfDocs)
      .subscribe(
          KeyDocuments => {
              // I'm assuming this is a synchronous method,
              // so I'll put the ready call after
              this.onDocsRetrieved(KeyDocuments);
              // now that we've processed whatever data we'll get,
              // we can display it to the user
              this.ready();
          },
          error => this.errorMessage = error;
      );
      

      Also, since you already have a handler for your subscribe stream – onDocsRetrieved – you could just add it in there.

      With this approach, each time your subscribe stream gets a new update, this.ready() will fire. It’s not a bad thing, setting a true value to true again; just something to be aware of. If you wanted to limit that, you might wrap it in a conditional:

      this._KeyDocumentService.getKeyDocuments(this.typeOfDocs)
      .subscribe(
          KeyDocuments => {
              this.onDocsRetrieved(KeyDocuments);
              if(something && something.else) this.ready();
          },
          error => this.errorMessage = error;
      );
      

      Thanks for asking! Hope this helps, and please let me know if you have any other questions.

  2. Ed Appell says:

    Thanks for the helpful sample! Any chance you might update this for the release of Angular 2? Looks like some conventions have changed, and some of the code in your example doesn’t work any more. Thanks!

  3. eappell says:

    Thanks Colin, I didn’t realize it was that simple. Now investigating your other sample using the loading container, and running into the same issue where the “directives” property is set in the meta tag, but that is no longer used in ng2. What do we do with that directives property?

    • Colin says:

      That’s no longer needed. Once you declare components, pipes, and directives in the declarations array of your module, they’re available to use anywhere in that module. I’ll update the post to reflect that. Thanks for pointing it out.

  4. ng-bootstrap ( still in alpha and requires CLI ) makes pretty work of this and you don’t have to use bootstrap everywhere nor include Bootstrap JS, if you have concerns about having to format all your code.

    https://ng-bootstrap.github.io/#/components/modal

  5. I previously suggested using ng-bootstrap but it’s even easier than that. If you want a modal / loading screen to appear over the whole screen you can just do this:

    In your root index.html, where the body is:

    <body id="main-body">
    <div id="loadingModal"></div>
    <app-root>Loading...</app-root>
    </body>

    in styles.css ( styling for that root page ):

    body {
    overflow: hidden;
    }

    body.loading #loadingModal {
    display: block;
    }

    #loadingModal {
    display: none;
    position: fixed;
    z-index: 1000;
    top: 0;
    left: 0;
    height: 100%;
    width: 100%;
    background: rgba( 255, 255, 255, .8 ) url( '../../common/media/loading-squares.gif' ) 50% 50% no-repeat;
    }

    Obviously you will need your own gif.

    Then add the class ‘loading’ when you want it to start and remove it when you want it to finish. In my case, I do it on ngOnInit in a lower component and remove it in the complete portion of my Observable call, from my main component.

    // Initial call to load info from db
    private getData( choice: string, values: string[], onComplete: Function, varName: string ): void {
    this.dataService.getTableData( choice, 'mainCalls.php', values )
    // When the Observable returns data, we do this.
    .subscribe(
    data => {
    this[varName] = data;
    },
    err => {
    console.log(err);
    },
    () => {
    onComplete();
    removeClass( document.getElementById( 'main-body' ), 'loading' );
    }
    );
    }

    ....

    ngOnInit() {
    addClass( document.getElementById( 'main-body' ), 'loading' );
    this.getData('getTableData', null, this.doOnComplete, 'tableData' );
    // set initial window height. Const of 77 because it of destop bar.
    this.setTbodyHeight();
    }

    jQuery like add/remove/has class functions ( There are other ways to do this ):

    function hasClass(el, className) {
    if (el.classList) {
    return el.classList.contains(className);
    } else {
    return !!el.className.match(new RegExp('(\\s|^)' + className + '(\\s|$)'));
    }
    }

    function addClass(el, className) {
    if (el.classList) {
    el.classList.add(className);
    } else if (!hasClass(el, className)) {
    el.className += ' ' + className;
    }
    }

    function removeClass(el, className) {
    if (el.classList) {
    el.classList.remove(className);
    } else if (hasClass(el, className)) {
    let reg = new RegExp('(\\s|^)' + className + '(\\s|$)');
    el.className = el.className.replace(reg, ' ');
    }
    }

    • Colin says:

      That’s legit, and works for a loading overlay. In my case I needed to block the DOM from trying to display data that hadn’t returned, so I needed to prevent the template from completely loading until the full view model had returned. Yes, it sucks having a loading spinner embedded in every page, so I’m totally open to feedback on how this can be done from a global standpoint instead.

Leave a Reply

Your email address will not be published. Required fields are marked *