Sometimes you gotta let users know what's going on. For that, you should enlist the aid of an alert service.

An alert service is different from a Toast (or Snackbar in Angular Material) because it stays on the page until the user closes it or visits another URL.

By contrast a Toast or Snackbar usually looks like a popup that fades away on its own after a while.

Alerts are great to use for errors, warnings, and success messages. In this guide, I'll show you how to add a reusable alert service for those types of use cases.

You could, of course, check out the code on GitHub instead of reading this article. But where's the fun in that?

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

It's Casual Friday, so your boss Smithers walks into your office wearing his death metal t-shirt. He sits down without even saying hi and then gets straight to the point.

"Thanks for that progress spinner on the user profile photo upload," he says. "But we need more."

He cracks his knuckles.

"Specifically, we need to let the user know that the upload finished with some kind of an alert. We'd also like to let the user know if something went wrong."

You nod.

"Good. I'm glad you see things my way."

He leaves your office humming the chords from "Smoke on the Water."

Alterations

If you're following all the guides, be advised that I've made some updates since the last guide that have nothing to do with this subject.

For example, the profile image upload page now uses an Angular Material card. I was going to write a whole guide on using cards but they're so easy to implement that I don't think any guide is necessary.

I've also updated ImageUploaderComponent. The changes are mainly cosmetic, though.

Bottom line: you should probably get all the latest code from GitHub as there are new updates that I won't cover in this guide.

In the Beginning, There Was a Module

Start by Creating a new module that will handle the alert service.

Why a module? Because a module consists of one or more components and related services. 

And that's exactly what you're going to build: a service that uses a component to display alerts.

So head over to your command line. Navigate to the root of your source tree and type this:

ng g m ui/alert

And then...

ng g c ui/alert

Let the Angular command line do its magic and then go back to your source tree in Microsoft Visual Studio. Navigate to src/app/ui/alert. You should see several files in that folder.

Start by developing a model that will hold important info about each alert. For that, you'll have to create a new file called alert.model.ts.

export class Alert {
  id: string;
  message: string;
  autoClose: boolean;
  fade: boolean;
  alertType: string;

  constructor(init?:Partial<Alert>) {
    Object.assign(this, init);
  }
}

Most of that is self-explanatory. However, you might be wondering about alertType

What kind of alert "types" are there?

By the time you're done with this guide, the app will support these types of alerts:

  • Success
  • Info
  • Error
  • Warning

The important difference between all of those kinds of alerts is the color of the alert box. Error alerts, for example, show up in a red box to let the user know that something went wrong. Success alerts show up in a green box.

While I'm on the subject of alert types, I guess I should point out that you need to create alert-settings.ts.

export class AlertSettings {
  public static SUCCESS = "success";
  public static ERROR = "error";
  public static INFO = "info";
  public static WARNING = "warning";
}

That class makes it easy to identify different types of alerts.

For Distinguished Service

Okay, now it's time to create the service itself. You probably don't have that class in your folder so create alert.service.ts. Make it look like this:

import { Injectable } from '@angular/core';
import { Observable, BehaviorSubject } from 'rxjs';
import { filter } from 'rxjs/operators';
import { Alert } from './alert.model';
import { AlertSettings } from './alert-settings';

@Injectable({ providedIn: 'root' })
export class AlertService {
  private subject = new BehaviorSubject<Alert>(null);
  private defaultId = 'default-alert';

  onAlert(id = this.defaultId): Observable<Alert> {
    return this.subject.asObservable().pipe(filter(x => x && x.id === id));
  }

  success(message: string, options?: any) {
    this.alert(new Alert({ ...options, alertType: AlertSettings.SUCCESS, message }));
  }

  error(message: string, options?: any) {
    this.alert(new Alert({ ...options, alertType: AlertSettings.ERROR, message }));
  }

  info(message: string, options?: any) {
    this.alert(new Alert({ ...options, alertType: AlertSettings.INFO, message }));
  }

  warn(message: string, options?: any) {
    this.alert(new Alert({ ...options, alertType: AlertSettings.WARNING, message }));
  }

  alert(alert: Alert) {
    alert.id = alert.id || this.defaultId;
    this.subject.next(alert);
  }

  clear(id = this.defaultId) {
    this.subject.next(new Alert({ id }));
  }
}

A quick glance at that code and you'll see plenty of convenience methods that issue different types of alerts (info, error, etc.). 

But if you look towards the top you'll see that the alert isn't just stored as a simple object. It's actually stored as a BehaviorSubject and returned as an Observable.

What gives?

The code is using an Observable here because it wants to communicate state changes to the observer.

In this case, the component will "listen" for new alerts. Then, it will display them as needed.

So the component is the observer and the alert is the subject.

In the observer pattern, the subject notifies the observer about state changes. That's exactly what's happening in this module.

The alert component will store 0 to many alert messages. That's important to keep in mind. This implementation won't create a new alert component for each alert message. Instead, a single alert component will display as many alert messages as needed.

Now you might be wondering how the subject communicates with the observer, That's the next() method that you see in the alert() method. 

Simply put, the poorly named next() method simply tells the Observable to send a message to its subscribers.  Then, those subscribers can act accordingly.

In this case, the alert() method in the service tells the alert component to display an alert message.

Read that sentence a few times and it will make sense.

The clear() method sends an alert with no message. That tells the component to clear all alerts.

That's a bit of a hack, isn't it? So this might be a good time to say this ain't my code. I "borrowed" it from somewhere else and updated it to suit the current business requirements.

On to the Component

Next, update alert.component.ts. Make it look like this:

import { Component, OnInit, OnDestroy, Input, ViewEncapsulation } from '@angular/core';
import { Router, NavigationStart } from '@angular/router';
import { Subscription } from 'rxjs';
import { Alert } from './alert.model';
import { AlertService } from './alert.service';
import { AlertSettings } from './alert-settings';

@Component({
  selector: 'alert',
  templateUrl: 'alert.component.html',
  styleUrls: ['./alert.component.css'],
  encapsulation: ViewEncapsulation.None
})
export class AlertComponent implements OnInit, OnDestroy {
  @Input() id = 'default-alert';
  @Input() fade = true;

  alerts: Alert[] = [];
  alertSubscription: Subscription;
  routeSubscription: Subscription;

  constructor(private router: Router, private alertService: AlertService) { }

  ngOnInit() {
    this.alertSubscription = this.alertService.onAlert(this.id)
      .subscribe(alert => {
        if (!alert.message) {
          this.alerts = [];
          return;
        }

        this.alerts.push(alert);

        if (alert.autoClose) {
          setTimeout(() => this.removeAlert(alert), 3000);
        }
      }
    );

    // clear alerts on location change
    this.routeSubscription = this.router.events.subscribe(event => {
      if (event instanceof NavigationStart) {
        this.alertService.clear(this.id);
      }
    });
  }

  ngOnDestroy() {
    // unsubscribe to avoid memory leaks
    this.alertSubscription.unsubscribe();
    this.routeSubscription.unsubscribe();
  }

  removeAlert(alert: Alert) {
    if (!this.alerts.includes(alert)) return;

    if (this.fade) {
      this.alerts.find(x => x === alert).fade = true;

      setTimeout(() => {
          this.alerts = this.alerts.filter(x => x !== alert);
      }, 250);
    } else {
      this.alerts = this.alerts.filter(x => x !== alert);
    }
  }

  cssClass(alert: Alert) {
    if (!alert) return;

    const classes = ['alert', 'alert-dismissable'];
                
    const alertTypeClass = {
      [AlertSettings.SUCCESS]: 'alert alert-success',
      [AlertSettings.ERROR]: 'alert alert-danger',
      [AlertSettings.INFO]: 'alert alert-info',
      [AlertSettings.WARNING]: 'alert alert-warning'
    }

    classes.push(alertTypeClass[alert.alertType]);

    if (alert.fade) {
      classes.push('fade');
    }

    return classes.join(' ');
  }
}

In a nutshell, this is the class responsible for displaying alerts. That makes sense because it's a component class.

Since there's a lot going on in the class, I'll cover chunks of the code in detail.

Let's start at the beginning:

@Component({
  selector: 'alert',
  templateUrl: 'alert.component.html',
  styleUrls: ['./alert.component.css'],
  encapsulation: ViewEncapsulation.None
})

Note that the selector here is "alert". That means if you want to use the alert in your HTML (and you do), then you'll need to do so with an <alert> element.

  @Input() id = 'default-alert';
  @Input() fade = true;

Two inputs that you won't see used in this guide.

You can set the id to whatever you want. Just make sure it's unique within your array of alerts. If you don't set anything, it defaults to "default-alert."

Use the fade input if you want the alert to disappear slowly instead of all at once. I won't use it here. 

  alerts: Alert[] = [];
  alertSubscription: Subscription;
  routeSubscription: Subscription;

The alerts array, as you can imagine stores the array of alerts that the app is currently displaying.

The first subscription, alertSubscription, subscribes the Observable returned by the service. That's the same Observable that you saw in the previous section.

So why is there another Subscription here for routes?

Remember: you want to clear the alerts if the user navigates away to another place in the app. To that end, routeSubscription will listen for route changes.

  ngOnInit() {
    this.alertSubscription = this.alertService.onAlert(this.id)
      .subscribe(alert => {
        if (!alert.message) {
          this.alerts = [];
          return;
        }

        this.alerts.push(alert);

        if (alert.autoClose) {
          setTimeout(() => this.removeAlert(alert), 3000);
        }
      }
    );

That method executes when the class is instantiated. That's the point of every ngOnInit() method, after all.

The first thing that method does is subscribe to the Observable in the service. Then, it responds to messages received from the subject.

If there's no message in the incoming alert, then the code interprets that to mean that it's time to clear all messages. And it does so by setting the alerts array to an empty set.

Otherwise, the code pushes the new alert to the alerts array. The HTML will then display the new alert.

The code also handles auto-closing which I won't cover in this guide.

    // clear alerts on location change
    this.routeSubscription = this.router.events.subscribe(event => {
      if (event instanceof NavigationStart) {
        this.alertService.clear(this.id);
      }
    });

And there's the part that listens for user navigation to another place in the app. If that happens, the code invokes the service to clear all alerts.

  removeAlert(alert: Alert) {
    if (!this.alerts.includes(alert)) return;

    if (this.fade) {
      this.alerts.find(x => x === alert).fade = true;

      setTimeout(() => {
          this.alerts = this.alerts.filter(x => x !== alert);
      }, 250);
    } else {
      this.alerts = this.alerts.filter(x => x !== alert);
    }
  }

That code handles removing a single alert. As you can see, it also supports a fade-out if the alert is configured to fade instead of disappear immediately.

In this guide, alerts just disappear immediately when the user dismisses them.

  cssClass(alert: Alert) {
    if (!alert) return;

    const classes = ['alert', 'alert-dismissable'];
                
    const alertTypeClass = {
      [AlertSettings.SUCCESS]: 'alert alert-success',
      [AlertSettings.ERROR]: 'alert alert-danger',
      [AlertSettings.INFO]: 'alert alert-info',
      [AlertSettings.WARNING]: 'alert alert-warning'
    }

    classes.push(alertTypeClass[alert.alertType]);

    if (alert.fade) {
      classes.push('fade');
    }

    return classes.join(' ');
  }

The cssClass() method sets the class at the HTML level so the alert gets displayed in the appropriate color.

For example, if it's an error alert, then the code will append the alert-danger class to the element and paint the alert box red.

Aesthetics

Next, edit alert.component.css so you can add some styling to these alerts.

.alert {
  position: relative;
  padding: 0.75rem 1.25rem;
  margin-bottom: 1rem;
  border: 1px solid transparent;
  border-radius: 0.42rem;
}

.alert-primary {
  background-color: #3F51B5;
  border-color: #3F51B5;
  color: #FFFFFF;
}

.alert-success {
  background-color: #1BC5BD;
  border-color: #1BC5BD;
  color: #ffffff;
}

.alert-info {
  background-color: #8950FC;
  border-color: #8950FC;
  color: #ffffff;
}

.alert-warning {
  background-color: #FFA800;
  border-color: #FFA800;
  color: #ffffff;
}

.alert-danger {
  background-color: #F64E60;
  border-color: #F64E60;
  color: #ffffff;
}

.close {
  float: right;
  cursor: pointer;
}

.message-div {
  flex: 0 0 85%;
}

.close-div {
  flex: 1;
}

Key takeaway here: the different colors for each type of alert.

Plug those color codes into your favorite color-picking tool and you'll see they align nicely with their respective class names.

Towards the bottom, you'll see some CSS that enables you to put the close icon (an "x", as is always the case) on the alert. The user can just click or tap that "x" to dismiss the alert.

Hypertexting the Alert

The next thing I'll go over in the alert module is alert.component.html. It's the markup that actually displays the alert.

Make it look like this:

<div *ngFor="let alert of alerts" class="{{cssClass(alert)}}" style="display: flex; text-align: center; width: 50%; margin: 0 auto">
  <div class="message-div"><span [innerHTML]="alert.message"></span></div>
  <div class="close-div"><a class="close" (click)="removeAlert(alert)">&times;</a></div>
</div>

Nice and neat, isn't it?

The *ngFor structural directive in the first <div> loops through all the existing alerts and displays every one of them.

And you can see that the class property is using interpolation to grab the string returned from the cssClass() method you saw a couple of sections ago.

The additional styling in that first <div> element centers the alert box withinin the parent element.

The next two <div> elements display side-by-side. Take another look at the CSS from the previous section and you'll see they use flex to make that happen.

The first <div> is the message itself and the second <div> is the "x" that the user can click to dismiss the alert.

Mod Mon

You remembered that this is a module, right?

Now you need to update alert.module.ts

import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { AlertComponent } from './alert.component';

@NgModule({
  imports: [CommonModule],
  declarations: [AlertComponent],
  exports: [AlertComponent]
})
export class AlertModule { }

Key takeaway here is that the code exports AlertComponent so it can be used by any modules importing this module.

More Aesthetics

Although you already updated the CSS file specific to this module, you need to make other changes as well that will apply to all modules.

So edit styles.css. Add these lines.

.center-div {
  text-align:center;
  width: 50%;
  margin: 0 auto
}

.mat-form-field-flex {
  background-color: #ffffff !important;
}

.mat-card-header-text {
  margin: 0 0px !important;
}

.mat-card-header {
  font-size: 16pt;
  margin-bottom: 30px;
}

.card-content {
  font-size: 12pt;
}

Most of that deals with how the app will display Angular Material cards. It's overriding some of the defaults to give users of our software a little bit better experience.

The center-div class, though, offers a convenient way to center one <div> horizontally within another <div>. I'm sure you'll use it from time to time.

A Profile in Alerts

And now, finally, back to the profile component.

Once again, it's time to update the styling a bit. Edit profile-image.component.css.

.alert-section {
    margin-bottom: 15px;
    margin-top: 15px;
    height:70px;
}

.centered-card {
  height: 100vh;
  display: flex;
  justify-content: center;
  align-items: center;
}

The first class styles the section that will house the alert message. It's set to a height of 70 pixels to avoid layout shifting when the alert appears and disappears.

That last class is a newbie. It's used to center the Angular Material card on the screen.

Why? Because that's what the cool kids seem to be doing these days with photo uploads.

If you can't beat 'em, join 'em.

Next, edit profile-image.component.ts.

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';
import { AlertService} from '../../../ui/alert/alert.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;
  uploading: boolean = false;

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

  ngOnInit() { }

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

  upload() {
    this.alertService.clear();
    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;
    this.displaySuccess();
  }

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

  onUploadedImage(image: UploadedImage) {
    this.alertService.clear();
    let imageError: string = this.imageService.validateImage(image);

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

  private displayError(message: string) {
    this.alertService.error(message,
      { autoClose: false }
    );
  }

  private displaySuccess() {
    this.alertService.success("Profile photo successfully uploaded!",
      { autoClose: false }
    );
  }
}

Note the last two methods in the class. They both make use of AlertService.

Yes, that's the same AlertService you just created.

The first method displays an alert as an error. The second method displays an alert as a success.

In both cases, the autoClose property is set to false because you want that message to hang around until the user explicitly closes it or navigates to another route.

Now edit profile-image.component.html.

<div class="centered-card">
  <mat-card style="width: 70%">
    <mat-card-header>
      Profile Image
    </mat-card-header>
    <mat-card-content>
      <div class="alert-section">
        <alert></alert>
      </div>
      <div>
        <div style="text-align: center">
          <app-image-uploader (uploadedImage)="onUploadedImage($event)"></app-image-uploader>
        </div>
        <div *ngIf="!uploading" style="text-align:center">
          <button mat-raised-button color="primary" [disabled]="!currentFileUpload" (click)="upload()">Save Image</button>
        </div>
        <div *ngIf="uploading">
          <mat-spinner [diameter]="50" class="center-div"></mat-spinner>
        </div>
      </div>
    </mat-card-content>
    <mat-card-actions>
    </mat-card-actions>
  </mat-card>
</div>

The relevent part here is the <alert> element about a third of the way down. That's where the alerts will appear.

For the purposes of this requirement, though, there is only ever at most one alert at a time. You'll see that in action when you test it.

Speaking of Testing...

Yeah. Go ahead and test it out now. It's time.

Start by firing up your Spring Boot user service. Then, launch the Angular app.

Head over to the login page at http://localhost:4200/login and login with your usual credentials (darth/thedarkside).

When you're at the dashboard, select User and Profile Image from the left-hand navigation menu. You should see this:

 

Before you do anything else, go back and stop your Spring Boot application.

Why? Because you first want to test an error message. And if the server isn't running, you will definitely get an error message.

Once you've stopped the Spring Boot application, go back to the browser and upload your profile image as you normally would. Click Save Image.

After a few seconds of churn, you should see this:

 

Ah, good. The error message works beautifully.

Now, start up your Spring Boot application again. Once it's settled, go back to your browser and click Save Image once more.

You should see this:

 

Bingo! Your upload worked!

Wrapping It Up

Now that you've got an alert service working, it's time to tinker with the code on your own.

Why not add an alert to the login page? There's definitely some opportunity there.

Also, consider updating the look and feel of the alert to suit your own interests.

And if you want to, just go grab the code on GitHub.

Have fun!

Photo by Maria Freyenbacher on Unsplash