Creating an Angular Component Library - Progress Bar Component


Another common component you might use in an application is a progress bar. There are a few types of progress bars one being the general bootstrap style. Another one that I’m starting to see more frequently is the style positioned to the top of the page with an animation spanning the full width of the page similar to YouTube. This is the type we’re going to create.

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

 

Prerequisites

 

Generating Our Progress Bar Component

Our progress bar component will comprise a 3 different items. First, we will need a module just like our Alert component. We will also need a component for our progress bar. And finally we will need a service to allow us to update our root level progress bar from other components.

ng generate module components/progress-bar --project=foo
ng generate component components/progress-bar --project=foo
ng generate service components/progress-bar/progress-bar --project=foo

Now that we have our generated items, we now need to update the module to export our component.

import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { ProgressBarComponent } from './progress-bar.component';

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

One additional file we are going to create is an enum to hold our configurable fill color.

touch projects/foo/src/lib/components/progress-bar/progress-bar-fill.enum.ts

And our enum…

export enum ProgressBarFill {
  PRIMARY = 'progress-bar-fill-primary',
  SECONDARY = 'progress-bar-fill-secondary',
  SUCCESS = 'progress-bar-fill-success',
  INFO = 'progress-bar-fill-info',
  WARNING = 'progress-bar-fill-warning',
  DANGER = 'progress-bar-fill-danger'
}

And finally we will update our components directory’s public_api.ts file, exporting our module, component, service, and enum.

// Progress Bar Component
export * from './progress-bar/progress-bar.module';
export * from './progress-bar/progress-bar.component';
export * from './progress-bar/progress-bar.service';
export * from './progress-bar/progress-bar-fill.enum';

 

Creating Our Progress Bar Component

Again, like our alert component, our progress bar is a very simple HTML structure with a two nested divs. The outer div will be a container and the inner div will be the fill.

<div class="progress-bar">
  <div class="progress-bar-fill" 
    [ngClass]="fill" 
    [ngStyle]="{ width: (progressValue$ | async) + '%' }">
  </div>
</div>

Our SCSS styling will position our progress bar to the top of the page. We’re also creating fill color classes based on our enum values…

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

.progress-bar {
  position: fixed;
  background: transparent;
  height: 8px;
  top: 0;
  left: 0;
  right: 0;
  z-index: 200;

  .progress-bar-fill {
    height: 100%;
    transition: width 500ms;

    &.progress-bar-fill-primary {
      background: $primary;
    }

    &.progress-bar-fill-secondary {
      background: $secondary;
    }

    &.progress-bar-fill-success {
      background: $success;
    }

    &.progress-bar-fill-info {
      background: $info;
    }

    &.progress-bar-fill-warning {
      background: $warning;
    }

    &.progress-bar-fill-danger {
      background: $danger;
    }
  }
}

And our component class file…

import { Component, OnInit, Input, ChangeDetectionStrategy } from '@angular/core';
import { Observable } from 'rxjs';

import { ProgressBarFill } from './progress-bar-fill.enum';
import { ProgressBarService } from './progress-bar.service';

@Component({
  selector: 'foo-progress-bar',
  templateUrl: './progress-bar.component.html',
  styleUrls: ['./progress-bar.component.scss'],
  changeDetection: ChangeDetectionStrategy.OnPush
})
export class ProgressBarComponent implements OnInit {
  @Input()
  public fill: ProgressBarFill;

  public progressValue$: Observable<number>;

  constructor(private _progressBarService: ProgressBarService) {
    this.fill = ProgressBarFill.PRIMARY;
  }

  ngOnInit(): void {
    this.progressValue$ = this._progressBarService.onProgressChange()
  }
}

Our component class will have one @Input() value, our fill color. There is very minimal logic in our component. We are using our progress bar service to subscribe to progress changes. When we receive a new value, it is set to progressValue. The progressValue is used in our template with [ngStyle] to set the width of the fill div.

 

Creating Our Progress Bar Service

Our progress bar service is what drives our component’s functionality. Our service will maintain the current progress of the progress bar broadcast two events. One event is to notify subscribers of progress value changes and the other broadcasts that progress is complete.

import { Injectable } from '@angular/core';
import { BehaviorSubject, Observable, Subject } from 'rxjs';

@Injectable({
  providedIn: 'root'
})
export class ProgressBarService {
  private _progressValue: number;
  private _progressSource: BehaviorSubject<number>;

  private _progressCompleteSource: Subject<boolean>;

  constructor() {
    this._progressValue = 0;
    this._progressSource = new BehaviorSubject<number>(this._progressValue);
    this._progressCompleteSource = new Subject<boolean>();
  }

  public updateProgress(value: number): void {
    this._progressValue = value;
    this._progressSource.next(this._progressValue);

    if (value >= 100) {
      setTimeout(() => {
        this._progressCompleteSource.next(true);
        this._progressValue = 0;
        this._progressSource.next(this._progressValue);
      }, 1000);
    }
  }

  public resetProgress(): void {
    this._progressValue = 0;
    this._progressSource.next(this._progressValue);
  }

  public onProgressChange(): Observable<number> {
    return this._progressSource.asObservable();
  }

  public onProgressComplete(): Observable<boolean> {
    return this._progressCompleteSource.asObservable();
  }
}

So how are we broadcasting our events? We’re using an RxJS Subject and a BehaviorSubject. These both follow the publish subscribe ,patter, observer pattern, which allow our components to subscribe to changes. When we want to broadcast/emit a new value, we simply call next() on our subject with the new value. The subject will emit the new value to all of it’s subscribers.

The main difference between Subject and BehaviorSubject is that the later is givin an initial value. The later also maintains the last value emitted so when a new subscribion is established, it will immediately receive thew last emitted value.

We also have to public method to update and reset the progress and that’s pretty much it. We can place a single instance of our component at the root of our project and interact with it from any other component by inject our progress bar service.

 

Using Our Progress Bar

We will now create a quick demo of our progress bar. Our demo will manipulate our progress bar using our service. We will also subscribe to progress complete events and show one of our Alert components stating progress is complete.

First will update our app component html

<div class="container">
  <div>
    <foo-alert
      [type]="alertType"
      [visible]="alertIsVisible"
      [message]="alertMessage"
      [iconClass]="alertIconClass"
      [dismissible]="alertIsDismissible">
    </foo-alert>
  </div>
</div>
<foo-progress-bar [fill]="ProgressBarFill.DANGER"></foo-progress-bar>

And our app component class

import { Component, OnInit } from '@angular/core';
import { take } from 'rxjs/operators';
import { AlertType, ProgressBarFill, ProgressBarService } from 'foo';

@Component({
  selector: 'app-root',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.scss']
})
export class AppComponent implements OnInit {
  public ProgressBarFill = ProgressBarFill;

  public alertType: AlertType;
  public alertIsVisible: boolean;
  public alertMessage: string;
  public alertIconClass: string;
  public alertIsDismissible: boolean

  constructor(private _progressBarService: ProgressBarService) {
    this.alertType = AlertType.INFO;
    this.alertIsDismissible = false;
    this.alertMessage = 'Progress complete';
    this.alertIconClass = 'fas fa-info-circle';
    this.alertIsDismissible = false;
  }
  ngOnInit() {
    this._progressBarService.updateProgress(25);
    setTimeout(() => this._progressBarService.updateProgress(50), 2000);
    setTimeout(() => this._progressBarService.updateProgress(100), 3000);

    this._progressBarService.onProgressComplete()
      .pipe(take(1))
      .subscribe(complete => {
        if (complete) {
          this.alertIsVisible = true;
          setTimeout(() => this.alertIsVisible = false, 2000);
        }
      });
  }
}

Now we just need to build our library and run our demo project.

npm run build
npm start

 

Stackblitz Result

I’ve also embeded a Stackblitz showing our progress bar with our dashabord layout we create a few posts back. If you click “Click To Update” you will see our progress bar in action. After progress bar completes, our alert component will display a completion message.

A full screen demo can also be viewed here

 

 

The completed github repository can be found here

 


comments powered by Disqus