Custom & Reusable Toast Component with Angular Animations, Async Pipe, and RxJS' BehaviorSubject
Tags: angular, javascript, typescript, webdev
Published: 2022-05-15 04:46:08

<small>Available in [my GitHub repo](https://github.com/riapacheco/toast-demo)</small>

---

### Objectives
* Create a toast component with a conditional styling mechanism
* Define states with stored `strings` to feed it
* Use cool animations from Angular's core packages
* Trigger the toast from anywhere in the app (by any service or from any parent component that calls it)

### Execution Workflow
1. **Style the Component** - we'll style the component itself and in doing so, revisit concepts for conditional styling (utilizing Angular's built-in directives)
2. **Create the Toast Service** - We'll create the actual service that will be called on by _other_ services or components when needing to trigger and populate the toast
3. **Two-Way Bind the Component** - We'll then make adjustments to the component so that its conditional variables / properties can be accessed by the toast service

--- 

#### Skip Ahead
[Creating and Styling the Component](#creating-and-styling-the-component)
[Toast Setup and Initial Data Binding](#toast-setup-and-initial-data-binding)
[Adding SCSS Styles](#adding-scss-styles)
[Binding an Array as a Class](#binding-an-array-as-a-class)
[Adding Angular Animations to the Template](#adding-angular-animations-to-the-template)
[Adding Animation Syntax to the Component File](#adding-animation-syntax-to-the-component-file)
[Creating the Toast Service](#creating-the-toast-service)
[Accessing Service's BehaviorSubjects with the Async Pipe](#accessing-services-behaviorsubjects-with-the-async-pipe)
[Two-Way Bind the Component](#twoway-bind-the-component)
[Using the Async Pipe in the Template](#using-the-async-pipe-in-the-template)

---

# Creating and Styling the Component

First, create new component in the terminal by running:

```bash
  $ ng g c components/ui/toast
```

### Global Access
To access this component from anywhere in the app, add it to the `app.component.html` file. The service we create will ensure that it doesn't show up until we call it.

```html
<!--in the app.component.html file-->
﻿<app-toast></app-toast> <!--This is the selector for the toast component-->

﻿<router-outlet></router-outlet>
```

### Quick tip for Z-Index
We want the toast to appear in front of every item in the app. But sometimes, the app gets confused. To fix this, you can simply add (and keep track) of your z-indexed items in the `styles.scss` file. Note: to ensure this consistently works, don't forget to add the parent class you end up creating for the element too!

```scss
// style.scss

app-root {   z-index: 1; }  
app-toast, .toast-class {   z-index: 2; }

```

---

# Toast Setup and Initial Data Binding
* We'll create a div with an *ngIf directive. This makes it so the toast doesn't appear unless the property `showsToast` is `true`. We'll add this property to the component file next. 
* The `{{ toastMessage }}` string is binding a message string assigned to the `toastMessage` property that we'll also find in the component file next!

```html
<!--toast.component.html-->
﻿<div *ngIf="showsToast" class="toast-class">  
  <div style="max-width: 160px;">    
    {{ toastMessage }}  
  </div> 
  
  <!--button-->
  <a
    class="close-btn" 
    (click)="showsToast = !showsToast">    
    <small>      
      Dismiss    
    </small>
  </a>
</div>
```

```typescript
// toast.component.ts
import { Component, OnInit } from '@angular/core';

@Component({  
  selector: 'app-toast',  
  templateUrl: './toast.component.html',  
  styleUrls: ['./toast.component.scss']
})

export class ToastComponent implements OnInit {  
  toastMessage = 'This is a toast'; // This is the string the template is already bound to  
  showsToast = true; // This is what toggles the component to show or hide  ﻿

  constructor() { }  

  ngOnInit(): void {  }
}

```

---

# Adding SCSS Styles
Conditional styles usually revolves around adding or removing a class (in my experience). So, an element might be styled a certain way, and when given a different condition will add new styles (that might cancel out former ones).

An awesome feature that I don't think many people know about are placeholder selectors! If you don't know about them, you can read about them [here](https://dev.to/riapacheco/using-sassscss-placeholder-selectors-for-same-sheet-repeatable-styling-10k0).

Feel free to use my SCSS package on npm for this example, as this is what I'm doing to keep things high-level! I'm only going to use it for this example here for colors. 

Run `$ npm install @riapacheco/yutes` (more instruction is on the npm page)

Then, add this to the toast component's SCSS file:

```scss
// toast.component.scss
@import '~@riapacheco/yutes/combos.scss';

%default-toast { 
  // You indicate a placeholder selected with a preceding '%'
  position: absolute;
  top: 0;
  right: 0rem;  
  margin: 2rem;  
  display: inline-flex;  
  min-width: 260px;    
  min-height: 70px;  
  max-height: 70px;  
  box-shadow: 6px 6px 12px #00000040;  

  flex-flow: row nowrap;  
  align-items: center;  
  justify-content: space-between;  
  border-left: 6px solid black;  
  padding: 1.5rem;  
  border-radius: 4px;  
  font-size: 0.9rem;
}

// Default toast
.toast-class {  
  @extend %default-toast; // You then add the styles to another selector with the @extend decorator
}

// Now we can make our state-specific classes

// Success
.success-toast {  
  @extend %default-toast;  
  border-left: 6px solid $success;
}

// Warning
.warning-toast {  
  @extend %default-toast;  
  border-left: 6px solid $warning;
}

// Danger
.danger-toast {  
  @extend %default-toast;  
  border-left: 6px solid $danger;
}
```

Your default toast looks like this:
![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/6lfozoalzcn39pakhd31.png)

And if you change the class on the element from `toast-class` to `warning-toast`, it will look like this:

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/9fh95ygbpiaq46kmf9ke.png)

---

# Binding an Array as a Class

Now that you see how the component changes with a different class, we can use binding `[]` to bind a class property to the template (that can change from inside the component).

```typescript
// toast.component.ts
import { Component, OnInit } from '@angular/core';

@Component({  
  selector: 'app-toast',  
  templateUrl: './toast.component.html',  
  styleUrls: ['./toast.component.scss']
})

export class ToastComponent implements OnInit {  
  toastClass = ['toast-class']; // Class lists can be added as an array  
  toastMessage = 'This is a toast';  
  showsToast = true;
  
  constructor() { }  

  ngOnInit(): void {  }
}
```

```html
<!--toast.component.html-->
﻿<div
  *ngIf="showsToast"
  [class]="toastClass"> 
  <!--We bind it by surrounding 'class' with square-brackets and referencing the property from the template-->  ﻿
  <div style="max-width: 160px;">    
    {{ toastMessage }}  
  </div>  

  <a
    class="close-btn" 
    (click)="showsToast = !showsToast">    
      <small>      
        Dismiss    
      </small>  
  </a>
</div>

```

---

# Adding Angular Animations to the Template
To add animations from scratch, you add the BrowserAnimationsModule from angular's core package to your app.module.ts file like so:
```typescript
@import { BrowserAnimationsModule } from '@angular/platform-browser/animations';

@NgModule({  
  declarations: [    
    AppComponent,    
    ToastComponent  
  ],  
  imports: [    
    BrowserAnimationsModule,
    CommonModule  ],  

  providers: [],  
  bootstrap: [
    AppComponent
  ]
})

export class AppModule { }
```

Then in the `toast.component.html` file, we can add the **animation trigger** that tells the app which element we'll define with animation specs. Since it has a conditional trigger baked in, we no longer need the *ngIf directive that we added earlier. (cool, eh?)
```html
<!--toast.component.html-->
<div
  [@toastTrigger]="showsToast ? 'open' : 'close'" 
  [class]="toastClass">  
  <div style="max-width: 160px;">    
    {{ toastMessage }}  
  </div>  

  <a
    class="close-btn"
    (click)="showsToast = !showsToast">    
    <small>      
      Dismiss    
    </small>  
  </a>
</div>
```
Animations require a trigger that you name with `[@<name>]`. This is then bound to the conditional property we created earlier, but now followed by a ternary operator. So `showsToast ? 'open' : 'close'` really means: if the property `showsToast` is `true`, then use `open` as the animation state... else use `close`. We'll now define the animations states and styles in the component itself.

## Adding Animation Syntax to the Component File
First, we have to import, into the component, the different elements of the animation we'll be using. You can learn more about these triggers from [angular's documentation](https://angular.io/guide/animations).
```typescript
import { Component, OnInit } from '@angular/core';
// import this ⤵
import { animate, state, style, transition, trigger } from '@angular/animations';

@Component({  
  selector: 'app-toast',  
  templateUrl: './toast.component.html',  
  styleUrls: ['./toast.component.scss'],  
  // And then these ⤵
  animations: [    
    trigger('toastTrigger', [ // This refers to the @trigger we created in the template      
      state('open', style({ transform: 'translateY(0%)' })), // This is how the 'open' state is styled      
      state('close', style({ transform: 'translateY(-200%)' })), // This is how the 'close' state is styled      
      transition('open <=> close', [ // This is how they're expected to transition from one to the other         
        animate('300ms ease-in-out')
      ])    
    ])  
  ]
})

export class ToastComponent implements OnInit {  
  toastClass = ['toast-class'];  
  toastMessage = 'This is a toast';  
  showsToast = true;  

  constructor() { }  

  ngOnInit(): void {  }
}
```

You can test to see if the animation works by changing the showsToast property to false, and adding a `setTimeout()` function to change the property to `true` after 1000ms (1 second):
```typescript
export class ToastComponent implements OnInit {  
  toastClass = ['toast-class'];  
  toastMessage = 'This is a toast';  
  showsToast = false;  

  constructor() { }  

  ngOnInit(): void {    
    setTimeout(() => {      
      this.showsToast = true;    
    }, 1000);  
  }
}
```

Run `$ ng serve` in your terminal and wait a second for the toast to appear!

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ig9pyov9jjpopt026qui.gif)


So now we need to have something trigger the service!

## 3 Things to Remember
The toast component needs three inputs:
1. A class (`string`) that's either `success-toast`, `warning-toast`, or `danger-toast`.
2. A boolean result for `showsToast` to either or show (`true`) or hide (`false`) which impacts the animation
3. A message (`string`) that binds to the `toastMessage` so that the message will display



---

# Creating the Toast Service

Add the service with the following command in your terminal
```bash
$ ng g s services/toast
```
(Don't forget to add the service to the `providers: []` array in your `app.module.ts` file!

## Adding State Variables

In the toast.service file, we'll first add a `constant` that defines our toast states as strings. These strings will be what's passed to the component and thus match the classes (e.g. success-toast) that we made earlier. This will help keep things in check and make it easier to refer to the states from anywhere in the app (without having to go revisit what they're called or type out a specific string
```typescript
import { Injectable } from '@angular/core';

// Add this constant ⤵
export const TOAST_STATE = {  
  success: 'success-toast',  
  warning: 'warning-toast',  
  danger: 'danger-toast'
};

@Injectable({  
  providedIn: 'root'
})

export class ToastService {  
  constructor() { }
}
```

# Accessing Service's BehaviorSubjects with the Async Pipe
A super handy feature that comes with Angular is its async pipe. Instead of having to subscribe manually to an observable from inside a component, if the service/provider's observable is public, you can get access to what's in the observable from directly inside the template. 

Think of an observable as a stream of data that you can then watch and make appear in different places.

Angular often uses the `RxJS` library (comes with react) for handling state management. So we'll be using `BehaviorSubjects` since you can store default properties initially.

```typescript
// toast.service.ts﻿

export class ToastService {  
  // The boolean that drives the toast's 'open' vs. 'close' behavior  
  ﻿public showsToast$: BehaviorSubject<boolean> = new BehaviorSubject<boolean>(false);  

  // The message string that'll bind and display on the toast  ﻿. 
  public toastMessage$: BehaviorSubject<string> = new BehaviorSubject<string>('Default Toast Message');  

  // The state that will add a style class to the component  ﻿. 
  public toastState$: BehaviorSubject<string> = new BehaviorSubject<string>(TOAST_STATE.success);   

  constructor() { }
}
```

Now we'll add two methods to trigger the component and populate the BehaviorSubjects with different data.

1. A `showToast()` method will trigger the toast and pass through it the toast state and toast message string (it will be assumed that if the function is called, the toast will want to be opened)
2. A `dismissToast()` to indicate that the toast is back to false
```typescript
// toast.service.ts﻿

export class ToastService {  
  public showsToast$: BehaviorSubject<boolean> = new BehaviorSubject<boolean>(false);  
  public toastMessage$: BehaviorSubject<string> = new BehaviorSubject<string>('Default Toast Message');  
  public toastState$: BehaviorSubject<string> = new BehaviorSubject<string>(TOAST_STATE.success);  

  constructor() { }  

  showToast(toastState: string, toastMsg: string): void {  
    // Observables use '.next()' to indicate what they want done with observable    
    // This will update the toastState to the toastState passed into the function    ﻿
    this.toastState$.next(toastState);    

    // This updates the toastMessage to the toastMsg passed into the function    ﻿
    this.toastMessage$.next(toastMsg);    

    // This will update the showsToast trigger to 'true'
    this.showsToast$.next(true);   
  }  

  // This updates the showsToast behavioursubject to 'false'  
  dismissToast(): void {    
    this.showsToast$.next(false);  
  }
}
```


---

# Two-Way Bind the Component
Now we can go back to the toast component and replace the variables in the template with the observables we have in the component.
1. In the component itself, since we define and store our values in the service, we can remove the values from the component properties and replace them with their types.
2. We want to inject the toast.service as a 'public' injectable within the constructor
3. We'll want to add a `dismiss()` method that can call on the toast.service within the component and access its `dismissToast()` method
```typescript
import { ToastService } from 'src/app/services/toast.service';

export class ToastComponent implements OnInit {  
  // Change the default values to types  
  toastClass: string[];  
  toastMessage: string;  
  showsToast: boolean;  

  constructor(public toast: ToastService ) { } // We inject the toast.service here as 'public'  
  
  ngOnInit(): void {  }  

  // We'll add this to the dismiss button in the template  
  dismiss(): void {    
    this.toast.dismissToast();  
  }
}
```

# Using the Async Pipe in the Template
To use the async pipe, you just have to add the service as an object followed by the observable name you're accessing; and follow those with `| async`.
```html
<!--We canged the toastTrigger and class-->
<div
  [@toastTrigger]="(toast.showsToast$ | async) ? 'open' : 'close'" 
  [class]="toast.toastState$ | async">  ﻿
    <div style="max-width: 160px;">
      <!--We access the toastMessage$ observable in the service-->    
      {{ toast.toastMessage$ | async }}  
    </div>  

  <a
    class="close-btn" 
    (click)="dismiss()">    
    <small>      
      Dismiss    
    </small>  
  </a>
</div>
```

### Test it!
Since behavior subjects can hold initial values, we can test the toast by changing the default `showsToast$` value to true and adding whatever else you want to the other values!

```typescript
export class ToastService {  
  public showsToast$: BehaviorSubject<boolean> = new BehaviorSubject<boolean>(true);  
  public toastMessage$: BehaviorSubject<string> = new BehaviorSubject<string>('This is a test message!');  
  public toastState$: BehaviorSubject<string> = new BehaviorSubject<string>(TOAST_STATE.danger);  

  constructor() { }  
  
  // .. more code
}
```

# Conclusion
This was a lot -- but it's also the article I wish I read before I knew all this. We walked through the idea of services being separated, animations, scss styling, and more.

Now, when you're completing a process that needs a toast triggered, you can call on the toast to appear (and maybe even add a `setTimeout` to dismiss it!). Here's an example in a standard firebase user auth service:
```typescript
export class AuthService {  

  constructor(
    private fireAuth: AngularFireAuth,    
    private toast: ToastService  
  ) {}  

  registerUser(email: string, password: string): void {    
    this.fireAuth.createUserWithEmailAndPassword(email, password)      
      .then(res => {        
        this.toast.showToast(          
          TOAST_STATE.success,          
          'You have successfully registered!');        
        this.dismissError();        
        console.log('Registered', res);
      })
      .catch(error => {        
        this.toast.showToast(          
          TOAST_STATE.danger,          
          'Something went wrong, could not register'        
        );        
        this.dismissError();        
        console.log('Something went wrong', error.message);      
      });  
  }  

  private dismissError(): void {    
    setTimeout(() => {      
      this.toast.dismissToast();    
    }, 2000);  
  }
}
```

That's it. That's all.

Ria

<small>Ps. Check out this code in my [my GitHub repo](https://github.com/riapacheco/toast-demo)</small>