Creating an Angular Component Library - Alert Component


The first component we are going to create for our library is an alert component. Alerts are a common component used to notify the user that something has change or maybe an error occured or even a valididation constraint failed on a form.

A full screen demo of what we’re going to build

 

Prerequisites

 

Generating Our Alert Component

We will need to generate a new module along with a component in our library project. Then we will need to make them both accessible to applications using our library. Let generate a new module and a new component.

ng generate module components/alert --project=foo
ng generate component components/alert --project=foo

We will update our newly generated module to export our alert component

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

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

And last, we will update components directory’s public_api.ts file to export both the module and component…

export * from './alert/alert.module';
export * from './alert/alert.component';

 

Creating Our Alert Component

The structure of an alert is fairly straight forward. An alert has a container, a message, a possible icon, and a way to close the alert. We can make use of Angular’s @Input and @Output decorators to make our alert customizable.

Our alert component will have the following bindings for customization…

 

Binding Type Default Description
[type] AlertType PRIMARY The alert type to display (primary, info, etc)
[message] string EMPTY The message to display in the alert
[(visible)] boolean true The default visibility of the alert
[dismissible] boolean false Whether or not the alert is dismissible by the user
[iconClass] string EMPTY The icon classes to add if using icon lib

 

We will start first with our simple HTML tempalte…

<div class="alert" *ngIf="visible" [ngClass]="type" [@showHide]>
  <span class="alert-icon" *ngIf="iconClass" [ngClass]="iconClass"></span>
  <p class="alert-message">{{ message }}</p>
  <span class="alert-dismiss" *ngIf="dismissible" (click)="dismiss()">&times;</span>
</div>

Then our SCSS for our alert…

@import "../_shared/scss/variables";

.alert {
  padding: 1.5rem 1rem;
  margin: 0 0 1rem 0;
  border-radius: $border-radius;
  display: flex;
  flex-direction: row;
  font-size: 1rem;
  align-items: flex-start;

  .alert-icon,
  .alert-message {
    line-height: 22px;
    vertical-align: middle;
  }

  .alert-icon {
    flex: 0 0 18px;
  }

  .alert-message {
    flex-grow: 1;
    margin: 0;
    padding: 0 0.25rem;
    word-break: break-all;
  }

  .alert-dismiss {
    flex: 0 0 22px;
    font-weight: bold;
    font-size: 24px;
    line-height: 24px;
    cursor: pointer;
  }

  &.alert-primary {
    background: lighten($primary, 20%);
    color: darken($primary, 10%);

    .alert-dismiss:hover {
      color: darken($primary, 30%);
    }
  }

  &.alert-secondary {
    background: lighten($secondary, 20%);
    color: darken($secondary, 20%);

    .alert-dismiss:hover {
      color: darken($secondary, 30%);
    }
  }

  &.alert-success {
    background: lighten($success, 20%);
    color: darken($success, 20%);

    .alert-dismiss:hover {
      color: darken($success, 30%);
    }
  }

  &.alert-info {
    background: lighten($info, 20%);
    color: darken($info, 20%);

    .alert-dismiss:hover {
      color: darken($info, 30%);
    }
  }

  &.alert-warning {
    background: lighten($warning, 20%);
    color: darken($warning, 20%);

    .alert-dismiss:hover {
      color: darken($warning, 30%);
    }
  }

  &.alert-danger {
    background: lighten($danger, 20%);
    color: darken($danger, 20%);

    .alert-dismiss:hover {
      color: darken($danger, 30%);
    }
  }
}

And finally our component class…

import { Component, Input, Output, EventEmitter, ChangeDetectionStrategy } from '@angular/core';

import { SHOW_HIDE } from '../_shared/animations/show-hide.animation';
import { AlertType } from './alert-type.enum';

@Component({
  selector: 'foo-alert',
  templateUrl: './alert.component.html',
  styleUrls: ['./alert.component.scss'],
  changeDetection: ChangeDetectionStrategy.OnPush,
  animations: [SHOW_HIDE]
})
export class AlertComponent {
  @Input()
  public type: AlertType;

  @Input()
  public message: string;

  @Input()
  public iconClass: string | string[];

  @Input()
  public dismissible: boolean;

  @Output()
  public visibleChange: EventEmitter<boolean>;

  private isVisible: boolean;

  constructor() {
    this.type = AlertType.PRIMARY;
    this.message = "",
    this.isVisible = true;
    this.dismissible = false;
    this.visibleChange = new EventEmitter<boolean>();
  }

  @Input()
  public set visible(value: boolean) {
    this.isVisible = value;
  }

  public get visible(): boolean {
    return this.isVisible;
  }

  public dismiss(): void {
    this.isVisible = !this.isVisible;
  }
}

One thing you will notice is the use of the @Output decorator. In Angular, you can achieve two way data binding by creating an EventEmitter<T> with the @Output decorator. The catch is that the emitter must be named the same as the @Input with Change appended to it. Since our input is visible, our output must be visibleChange to achieve the two way binding ([(visible)]).

Another option would be to use an rxjs Subject that the parent can subscribe to and can be notified of the alert being closed. For now we will keep the two way binding but I wanted to make you aware of another approach if you’re looking to execute a callback on close.

Now we need to create two additional files. One is an emum holding the different alert types. This will allow us to style out alert with different styles based on the severity of the alert. The other file will hold the animation to execute when the alert is added and removed from the DOM.

Let create our alert type enum…

touch projects/foo/src/lib/components/alert/alert-type.enum.ts

And our enum…

export enum AlertType {
  PRIMARY = "alert-primary",
  SECONDARY = "alert-secondary",
  SUCCESS = "alert-success",
  INFO = "alert-info",
  WARNING = "alert-warning",
  DANGER = "alert-danger"
}

Now we can create our animation. The animation file will be placed in our _shared directory since we probably will end up reusing this animation with multiple components in our library. We will create a directory to hold all our shared animations…

mkdir -p projects/foo/src/lib/components/_shared/animations
touch projects/foo/src/lib/components/_shared/animations/show-hide.animation.ts

And our animation…

import { trigger, state, style, animate, transition } from '@angular/animations';

export const SHOW_HIDE = trigger('showHide', [
  transition(':enter', [
    style({ 
      opacity: 0, 
      transform: 'scaleX(0.98) scaleY(0)',
      position: 'relative'
    }),
    animate('150ms', style({ 
      opacity: 1,
      transform: 'scale(1)'
    }))
  ]),
  transition(':leave', [
    style({ 
      opacity: 1,
      transform: 'scale(1)'
    }),
    animate('250ms', style({ 
      opacity: 0,
      transform: 'scaleX(0.98) scaleY(0)'
    }))
  ])
])

Our animation has two transitions, one for when the component is being added to the DOM and another for when it is removed. Each transition has a starting style followed by the animation with a duration and a style to animate to. This will give us a very simple effect, similar to an old tube tv turning off/on. Feel free to change the animation to whatever you like.

 

Using Our Alert

Now that we have our alert created, we can build our library and use it in our demo application.

First we will run our build script.

npm run build

And now we can import our alert module from our library in our demo application’s app module…

import { BrowserModule } from '@angular/platform-browser';
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
import { NgModule } from '@angular/core';

import { AlertModule } from 'foo';
import { AppRoutingModule } from './app-routing.module';
import { AppComponent } from './app.component';

@NgModule({
  declarations: [
    AppComponent
  ],
  imports: [
    BrowserModule,
    BrowserAnimationsModule,
    AppRoutingModule,
    AlertModule
  ],
  providers: [],
  bootstrap: [AppComponent]
})
export class AppModule { }

Now we will add a couple alerts our demo’s app component.

Our app component template will look something like this…

<div class="container">
  <div>
    <foo-alert
      [type]="AT.DANGER"
      [visible]="true"
      [message]="'This is a danger alert'"
      [dismissible]="true">
    </foo-alert>
  </div>
  <div>
    <foo-alert
      [type]="alertType"
      [visible]="alertIsVisible"
      [message]="alertMessage"
      [iconClass]="alertIconClass"
      [dismissible]="alertIsDismissible">
    </foo-alert>
  </div>
</div>

Our app component styles

.container {
  padding: 1.5rem;
  div {
    display: block;
    margin-bottom: 1rem;
  }
}

And our app component class

import { Component, OnInit } from '@angular/core';

import { AlertType } from 'foo';

@Component({
  selector: 'app-root',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.scss']
})
export class AppComponent implements OnInit {
  public AT = AlertType;
  public alertType: AlertType;
  public alertIsVisible: boolean;
  public alertMessage: string;
  public alertIconClass: string;
  public alertIsDismissible: boolean

  constructor() {
    this.alertType = AlertType.INFO;
    this.alertIsDismissible = false;
    this.alertMessage = 'This is an example alert with our new reusable component';
    this.alertIconClass = 'fas fa-info-circle';
    this.alertIsDismissible = true;
  }
  ngOnInit() {
    setTimeout(() => this.alertIsVisible = true, 2000);
    setTimeout(() => this.alertIsVisible = false, 4000);
  }
}

If you look in our ngOnInit method you will see we added a couple setTimeout(...)’s to simulate dynamically adding and removing an alert from the DOM.

We will also add is some styling in our demo app’s root style.scss file. We are adding an import for a font, font-awesome icons, and a light css reset.

@import url('https://fonts.googleapis.com/css2?family=Source+Sans+Pro:wght@200;400;600;700&display=swap');
@import url("https://cdnjs.cloudflare.com/ajax/libs/font-awesome/5.13.0/css/all.min.css");

$background: #F6F6F8;
$font-color: #FFFF;

html, body, ul, p, h1, h2, h3, h4, h5, h6, blockquote {
  margin: 0;
  padding: 0;
  box-sizing: border-box;
  font-family: 'Source Sans Pro', Arial, Helvetica, sans-serif;
}

html {
  font-size: 16px;
}

body {
  background: $background;
}

 

Now we can run our demo application with npm start.

 

Stackblitz Result

I’ve also embeded a Stackblitz showing our different alerts with our dashabord layout we create a few posts back.

A full screen demo can also be viewed here

 

 

The completed github repository can be found here

 


comments powered by Disqus