-
Notifications
You must be signed in to change notification settings - Fork 0
task#26 create an alert service
The goal of this task is to create a simple reusable alert service, which handles displaying status messages e.g. error, success, warning and info alerts. Alert notifications are an extremely common requirement in web applications for displaying status messages to the user.
- Create alert component template that contains the html for displaying alerts alert.component.html.
- Create the alert component with the logic for displaying alerts alert.component.ts.
- Create the alert model class that defines the properties of an alert, it also includes the AlertType enum that defines the different types of alerts alert.model.ts.
- Create the alert module that encapsulates the alert component so it can be imported by the app module alert.module.ts
- Create the alert service that can be used by any angular component or service to send alerts to alert components alert.service.ts.
- Create the barrel file that re-exports the alert module, service and model so they can be imported using only the folder path instead of the full path to each file, and also enables importing from multiple files with a single import index.ts.
- Add the alert component before the main router outlet.
- Add a test component to test the different aspects of the alert component
Verify the result by running the unit tests or by using the test form:

Generate the Component:
ng generate component shared/component/alert
The alert component (/src/app/shared/component/alert/alert.component.ts) controls the adding & removing of alerts in the UI,
it maintains an array of alerts that are rendered by the component template.
The ngOnInit method subscribes to the observable returned from the alertService.onAlert() method,
this enables the alert component to be notified whenever an alert message is sent to
the alert service and add it to the alerts array for display. Sending an alert with an empty
message to the alert service tells the alert component to clear the alerts array.
This method also calls router.events.subscribe() to subscribe to route change events so it can automatically clear alerts on route changes.
The ngOnDestroy() method unsubscribes from the alert service and router when the component
is destroyed to prevent memory leaks from orphaned subscriptions.
The removeAlert() method removes the specified alert object from the array,
it allows individual alerts to be closed in the UI.
The cssClass() method returns a corresponding bootstrap alert class for each of the alert types,
if you're using something other than bootstrap you can change the CSS classes returned to suit your application.
The HTML Template
The content of alert.component.html:
<div *ngFor="let alert of alerts" class="{{cssClass(alert)}}">
<a class="close" (click)="removeAlert(alert)">×</a>
<span [innerHTML]="alert.message"></span>
</div>The Alert Component
import {Component, OnInit, OnDestroy, Input} from '@angular/core';
import {Router, NavigationStart} from '@angular/router';
import {Subscription} from 'rxjs';
import {Alert, AlertType} from './alert.model';
import {AlertService} from './alert.service';
@Component(
{
selector: 'app-alert',
templateUrl: 'alert.component.html',
styleUrls: ['./alert.component.scss']
}
)
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(): void {
// subscribe to new alert notifications
this.alertSubscription = this.alertService.onAlert(this.id)
.subscribe(alert => {
// clear alerts when an empty alert is received
if (!alert.message) {
// filter out alerts without 'keepAfterRouteChange' flag
this.alerts = this.alerts.filter(x => x.keepAfterRouteChange);
// remove 'keepAfterRouteChange' flag on the rest
this.alerts.forEach(x => delete x.keepAfterRouteChange);
return;
}
// add alert to array
this.alerts.push(alert);
// auto close alert if required
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(): void {
// unsubscribe to avoid memory leaks
this.alertSubscription.unsubscribe();
this.routeSubscription.unsubscribe();
}
removeAlert(alert: Alert): void {
// check if already removed to prevent error on auto close
if (!this.alerts.includes(alert)) {
return;
}
if (this.fade) {
// fade out alert
this.alerts.find(x => x === alert).fade = true;
// remove alert after faded out
setTimeout(() => {
this.alerts = this.alerts.filter(x => x !== alert);
}, 250);
} else {
// remove alert
this.alerts = this.alerts.filter(x => x !== alert);
}
}
cssClass(alert: Alert): string {
if (!alert) {
return;
}
const classes = ['alert', 'alert-dismissable'];
const alertTypeClass = {
[AlertType.Success]: 'alert alert-success',
[AlertType.Error]: 'alert alert-danger',
[AlertType.Info]: 'alert alert-info',
[AlertType.Warning]: 'alert alert-warning'
};
classes.push(alertTypeClass[alert.type]);
if (alert.fade) {
classes.push('fade');
}
return classes.join(' ');
}
}Fix the Unit Test
We need to add the RouterTestingModule and the AlertService to create the test service. We also want to check the existence of the correct alert type in the HTML.
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { AlertComponent } from './alert.component';
import {AlertService} from './alert.service';
import {RouterTestingModule} from '@angular/router/testing';
import {By} from '@angular/platform-browser';
describe('AlertComponent', () => {
let component: AlertComponent;
let fixture: ComponentFixture<AlertComponent>;
beforeEach(async(() => {
TestBed.configureTestingModule({
imports: [ RouterTestingModule, ],
declarations: [ AlertComponent ],
providers: [
AlertService,
]
})
.compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(AlertComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
it('should write an INFO alert to the component', () => {
const alertService: AlertService = TestBed.inject(AlertService);
alertService.info('INFO');
fixture.detectChanges();
expect(fixture.debugElement.query(By.css('span')).nativeNode.innerHTML).toEqual('INFO');
expect(fixture.debugElement.query(By.css('.alert-info'))).toBeTruthy();
expect(fixture.debugElement.query(By.css('.alert'))).toBeTruthy();
expect(fixture.debugElement.query(By.css('.close'))).toBeTruthy();
});
it('should write an ERROR alert to the component', () => {
const alertService: AlertService = TestBed.inject(AlertService);
alertService.error('ERROR');
fixture.detectChanges();
expect(fixture.debugElement.query(By.css('span')).nativeNode.innerHTML).toEqual('ERROR');
expect(fixture.debugElement.query(By.css('.alert-danger'))).toBeTruthy();
expect(fixture.debugElement.query(By.css('.alert'))).toBeTruthy();
expect(fixture.debugElement.query(By.css('.close'))).toBeTruthy();
});
it('should write an SUCCESS alert to the component', () => {
const alertService: AlertService = TestBed.inject(AlertService);
alertService.success('SUCCESS');
fixture.detectChanges();
expect(fixture.debugElement.query(By.css('span')).nativeNode.innerHTML).toEqual('SUCCESS');
expect(fixture.debugElement.query(By.css('.alert-success'))).toBeTruthy();
expect(fixture.debugElement.query(By.css('.alert'))).toBeTruthy();
expect(fixture.debugElement.query(By.css('.close'))).toBeTruthy();
alertService.clear();
fixture.detectChanges();
expect(fixture.debugElement.query(By.css('.alert'))).toBeFalsy();
});
});The scss File
a {
cursor: pointer;
}The Alert model (/src/app/shared/component/alert/alert.model.ts) defines the properties of each alert object, and the AlertType enum defines the types of alerts allowed in the application.
export class Alert {
id: string;
type: AlertType;
message: string;
autoClose: boolean;
keepAfterRouteChange: boolean;
fade: boolean;
constructor(init?:Partial<Alert>) {
Object.assign(this, init);
}
}
export enum AlertType {
Success,
Error,
Info,
Warning
}The AlertModule (/src/app/shared/component/alert/alert.module.ts) encapsulates the alert component so it can be imported and used by other Angular modules.
ng generate module shared/component/alert
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 { }The alert service (/src/app/shared/component/alert/alert.service.ts) acts as the bridge between any component in an Angular application and the alert component that actually displays the alert/toaster messages. It contains methods for sending, clearing and subscribing to alert messages.
ng generate service shared/component/alert/alert
The service uses the RxJS Observable and Subject classes to enable communication with other components.
import { Injectable } from '@angular/core';
import { Observable, Subject } from 'rxjs';
import { filter } from 'rxjs/operators';
import { Alert, AlertType } from './alert.model';
@Injectable({ providedIn: 'root' })
export class AlertService {
private subject = new Subject<Alert>();
private defaultId = 'default-alert';
// enable subscribing to alerts observable
onAlert(id = this.defaultId): Observable<Alert> {
return this.subject.asObservable().pipe(filter(x => x && x.id === id));
}
// convenience methods
success(message: string, options?: any) {
this.alert(new Alert({ ...options, type: AlertType.Success, message }));
}
error(message: string, options?: any) {
this.alert(new Alert({ ...options, type: AlertType.Error, message }));
}
info(message: string, options?: any) {
this.alert(new Alert({ ...options, type: AlertType.Info, message }));
}
warn(message: string, options?: any) {
this.alert(new Alert({ ...options, type: AlertType.Warning, message }));
}
// main alert method
alert(alert: Alert) {
alert.id = alert.id || this.defaultId;
this.subject.next(alert);
}
// clear alerts
clear(id = this.defaultId) {
this.subject.next(new Alert({ id }));
}
}The Unit Test for the Alert Service
The unit test verifies the Alerts by subscribing to the Observables. It also verifies the clear method.
import { TestBed } from '@angular/core/testing';
import { AlertService } from './alert.service';
import {AlertType} from './alert.model';
describe('AlertService', () => {
let service: AlertService;
const options = {
autoClose: false,
keepAfterRouteChange: false
};
beforeEach(() => {
TestBed.configureTestingModule({});
service = TestBed.inject(AlertService);
});
it('should be created', () => {
expect(service).toBeTruthy();
});
it('create an info message', () => {
service.onAlert()
.subscribe(alert => {
expect(alert.type).toBe(AlertType.Info);
expect(alert.message).toEqual('Info Message');
expect(alert.id).toEqual('default-alert');
expect(alert.autoClose).toBe(options.autoClose);
expect(alert.keepAfterRouteChange).toBe(options.keepAfterRouteChange);
});
service.info('Info Message', options);
});
it('clears all messages', () => {
service.onAlert()
.subscribe(alert => {
// clear alerts when an empty alert is received
if (!alert.message) {
// filter out alerts without 'keepAfterRouteChange' flag
expect(alert.message).toBeFalsy();
} else {
expect(alert.message).toEqual('Success Message');
}
});
service.success('Success Message', options);
service.clear();
});
});
To make the alert component available to the auction application you need to add the AlertModule to the imports array of your the Module (app.module.ts). See the app module from the example app below, the alert module is imported on line 13 and added to the imports array of the app module on line 25.
import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';
import { AppComponent } from './app.component';
import { FormsModule } from '@angular/forms';
import { HTTP_INTERCEPTORS, HttpClientModule } from '@angular/common/http';
import { AngularDateHttpInterceptorService, MockModule } from './shared/helper';
import { NavBarComponent } from './nav-bar/nav-bar.component';
import { HomeComponent } from './home/home.component';
import { AppRoutingModule } from './app-routing.module';
import { environment } from '../environments/environment';
import { AlertComponent } from './shared/component/alert/alert.component';
import {AlertModule} from './shared/component/alert/alert.module';
// import/use mock module only if configured (mock module will not be included in prod build!):
const mockModule = environment.useMockBackend ? [MockModule] : [];
@NgModule({
declarations: [AppComponent, NavBarComponent, HomeComponent, AlertComponent],
imports: [
BrowserModule,
FormsModule,
HttpClientModule,
AppRoutingModule,
AlertModule,
...mockModule
],
providers: [
{
provide: HTTP_INTERCEPTORS,
useClass: AngularDateHttpInterceptorService,
multi: true
},
],
bootstrap: [AppComponent]
})
export class AppModule {}Generate the Component:
ng generate component shared/component/alert-test
Alert Service Options
- The first parameter to the alert methods is a string for the alert message which can be a plain text string or HTML
- The second parameter is an optional options object that supports an autoClose boolean property and keepAfterRouteChange boolean property:
- autoClose - if true tells the alert component to automatically close the alert after three seconds. Default is false.
- keepAfterRouteChange - if true prevents the alert from being closed after one route change, this is handy for displaying messages after a redirect such as a successful registration message. Default is false.
Here is the test component fhat injects the alertService into its constructor() so it can be called from the home component template when each of the buttons is clicked. In a real world application alert notifications can be triggered by any type of event, for example an error from an http request or a success message after a user profile is saved.
The alert-test.component.ts
import { Component, OnInit } from '@angular/core';
import {AlertService} from '../alert/alert.service';
@Component({
selector: 'app-alert-test',
templateUrl: './alert-test.component.html',
styleUrls: ['./alert-test.component.scss']
})
export class AlertTestComponent implements OnInit {
options = {
autoClose: false,
keepAfterRouteChange: false
};
constructor(public alertService: AlertService) { }
ngOnInit(): void {
}
}The alert-test.component.html
<h1>Alert Test</h1>
<button class="btn btn-success m-1" (click)="alertService.success('Success!!', options)">Success</button>
<button class="btn btn-danger m-1" (click)="alertService.error('Error :(', options)">Error</button>
<button class="btn btn-info m-1" (click)="alertService.info('Some info....', options)">Info</button>
<button class="btn btn-warning m-1" (click)="alertService.warn('Warning: ...', options)">Warn</button>
<button class="btn btn-outline-dark m-1" (click)="alertService.clear()">Clear</button>
<div class="form-group mt-2">
<div class="form-check">
<input type="checkbox" name="autoClose" id="autoClose" class="form-check-input" [(ngModel)]="options.autoClose">
<label for="autoClose">Auto close alert after three seconds</label>
</div>
<div class="form-check">
<input type="checkbox" name="keepAfterRouteChange" id="keepAfterRouteChange" class="form-check-input" [(ngModel)]="options.keepAfterRouteChange">
<label for="keepAfterRouteChange">Keep displaying after one route change</label>
</div>
</div>Add the alert component tag before the router outlet:
<div class="container">
<app-nav-bar></app-nav-bar>
<alert></alert>
<router-outlet></router-outlet>
</div>Add the selector at the of home.component.html.
The complete source code has been refactored to meet the lint requirements:
https://github.com/mbachmann/angular-tutorial-2020/tree/task%2326-create-an-alert-service
The Lint Result
The Unit Test Result