Sometimes your users need to know that stuff is happening behind the scenes. That's when a progress spinner comes in handy.

Fortunately, Angular Material offers a module that makes adding progress spinners to your UI way too easy. I'll show you how to do it in this guide.

As always, you can just jump straight to the source code on GitHub. Alternatively, hang around here for a while and learn about the whys and wherefores.

Remember: if you're having problems running the Angular app locally, be sure to check the README.

Please note: this guide is a part of an ongoing series of tutorials on how to create a customer relationship management (CRM) application from scratch. The GitHub code for each guide belongs to a specific branch. The master branch holds the entire application up to the current date. If you want to follow the whole series, just view the careydevelopmentcrm tag. Note that the website displays the guides in reverse chronological order so if you want to start from the beginning, go to the last page.

The Business Requirements

Your boss Smithers walks into your office to talk about the CRM app you're working on.

"Look, you got the profile image upload thing working just fine," he says. "But users don't see anything while the upload is happening. They're left just wondering if anything is going on in the back end!"

You nod in agreement.

"So why not add a spinny thing that will make it clear to them that some activity is happening? You can do that, right?"

You nod again.

"Okay then. It's settled."

He jumps out of his chair and leaves your office whistling that tune from Kill Bill.

Recent Updates

Before getting to the spinner, you need to make some updates to ProfileImageComponent. Here's what it needs to look like:

import { Component, OnInit, ViewEncapsulation } from '@angular/core';
import { HttpResponse, HttpEvent } from '@angular/common/http';
import { UploadFileService } from '../../service/file-upload.service';
import { UserService } from '../../service/user.service';
import { UploadedImage } from '../../ui/model/uploaded-image';
import { ImageService } from '../../ui/service/image-service';

const profileImageUploadUrl: string = 'http://localhost:8080/user/profileImage';

@Component({
    selector: 'app-profile-image',
    templateUrl: './profile-image.component.html',
    styleUrls: ['./profile-image.component.css'],
    encapsulation: ViewEncapsulation.None
})
export class ProfileImageComponent implements OnInit {

  currentFileUpload: UploadedImage;
  changeImage: boolean = false;
  imageError: string = null;
  uploading: boolean = false;

  constructor(private uploadService: UploadFileService, private userService: UserService,
    private imageService: ImageService) { }

  ngOnInit() { }

  change($event) {
    this.changeImage = true;
  }

  upload() {
    this.uploading = true;

    this.uploadService.pushFileToStorage(this.currentFileUpload.file, profileImageUploadUrl)
        .subscribe(event => this.handleEvent(event),
            err => this.handleError(err));
  }

  handleEvent(event: HttpEvent<any>) {
    if (event instanceof HttpResponse) {
      let response: HttpResponse<any> = <HttpResponse<any>>event;
      if (response.status == 200) {
        this.handleGoodResponse();
      }
    }
  }

  handleGoodResponse() {
    this.currentFileUpload = undefined;
    this.uploading = false;
  }

  handleError(err: Error) {
    console.error("Error is", err);
    this.imageError = err.message;
    this.uploading = false;
  }

  onUploadedImage(image: UploadedImage) {
    this.imageError = this.imageService.validateImage(image);

    if (!this.imageError) {
        this.currentFileUpload = image;
    }
  }
}

The biggest change there from previous guides is that the clicked boolean has been removed in favor of uploading.

Why? Because it's more descriptive.

The uploading boolean will get set to true when the user formally uploads the profile image to the server via the Spring Boot user service. When the upload is complete, the boolean will go back to false.

As you've probably guessed, while that uploading boolean is true, the user will see a spinner indicating that work is going on under the covers.

Modifying the Module

You also need to update UserModule so it looks like this:

import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { RouterModule } from '@angular/router';
import { FlexLayoutModule } from '@angular/flex-layout';
import { MatIconModule } from '@angular/material/icon';
import { MatInputModule } from '@angular/material/input';
import { MatButtonModule } from '@angular/material/button';
import { MatProgressSpinnerModule } from '@angular/material/progress-spinner';
import { ReactiveFormsModule } from '@angular/forms';
import { AccountInfoComponent } from './account-info/account-info.component';
import { ProfileImageComponent } from './profile-image/profile-image.component';
import { ImageUploaderComponent } from '../ui/image-uploader/image-uploader.component';


export const routes = [
  { path: '', pathMatch: 'full', redirectTo: 'account-info' },
  { path: 'account-info', component: AccountInfoComponent },
  { path: 'profile-image', component: ProfileImageComponent }
];

@NgModule({
  declarations: [
    AccountInfoComponent,
    ProfileImageComponent,
    ImageUploaderComponent
  ],
  imports: [
    CommonModule,
    FlexLayoutModule,
    MatIconModule,
    MatInputModule,
    MatButtonModule,
    MatProgressSpinnerModule,
    ReactiveFormsModule,
    RouterModule.forChild(routes)
  ]
})
export class UserModule { }

The small change there is the addition of MatProgressSpinnerModule to the imports.

As you've probably guessed again, that's the Angular Material module that gives you the spinner.

And why are you importing it in UserModule? Because the component that handles profile image uploads happens to be a part of that same module.

Spin City

Finally, it's time to update the HTML in profile-image.component.html so it shows a spinner.

<div>
  <div>
    <h5>Profile Image</h5>
    <p>Upload a square image no smaller than 200x200.</p>
    <div *ngIf="imageError" class="errorMessage">
      <strong class="mat-error">{{imageError}}</strong>
    </div>
  </div>
  <div>
    <div>
      <app-image-uploader (uploadedImage)="onUploadedImage($event)"></app-image-uploader>
    </div>
    <div *ngIf="!uploading">
      <button mat-raised-button color="primary" [disabled]="!currentFileUpload" (click)="upload()">Save Image</button>
    </div>
    <div *ngIf="uploading" style="margin-left:10px">
      <mat-spinner [diameter]="50"></mat-spinner>
    </div>
  </div>
</div>

Take a gander at those two <div> elements towards the bottom. That's where all the good stuff is happening.

The first <div> only displays when the file is not uploading. That's why you see *ngIf="!uploading" inside the element.

That *ngIf expression, by the way, is called  structural directive in Angular. Structural directives handle HTML layout. 

They shape the structure of the Document Object Model (DOM). Hence the name.

In this case, *ngIf tells the framework to only display the <div> and its child elements if the uploading boolean is false.

Yes, that's the same uploading boolean you added to ProfileImageComponent earlier.

That <div> element displays the button that the user will press to initiate the upload. So it makes sense that it would only display when the user is not already uploading something.

Now look at that  last <div> element. It displays only when the uploading boolean is set to true. Users will see that element when they're uploading the profile image.

And what's in that <div>? The spinner, of course.

The element that displays the spinner is <mat-spinner> . The [diameter] property in that element sets the size of the spinner.

That's it. Really, it's that simple.

Entering the Spin Cycle

Now it's time to test out your spinner. Begin by firing up your Spring Boot user service. Then, launch your Angular app with all the new code in place.

Login using the credentials you've used all along (darth/thedarkside). Then, click on User and Profile Image on the left-hand nav bar.

You'll see the familiar page to upload a profile pic.

But before you do anything, stop your Spring Boot user service.

Why? Because you want to see your spinner in action. With the Spring Boot service off, the Angular app will take a few seconds to figure out that the server is down and give you a chance to check out your spinner in all its glory.

So seriously, stop the Spring Boot user service.

Now, upload a photo as usual. Click Save Image. You should see a spinner appear where the button used to be:

Once the upload is finished, you'll see the button again. You'll also see an error message because the Spring Boot server is down. That's okay.

You can click Save Image again if you want to see the spinner in action a second time.

Wrapping It Up

Well that was pretty easy, wasn't it? 

Now it's up to you to make changes as you see fit. You might, for example, want to change the size of the spinner.

You can also alter its color. See the docs for more details.

And if you want to borrow the code from GitHub, you can do that as well.

Have fun!

Photo by Tanino from Pexels