Available in my GitHub repo
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
- Style the Component - we'll style the component itself and in doing so, revisit concepts for conditional styling (utilizing Angular's built-in directives)
- 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
- 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
Toast Setup and Initial Data Binding
Adding SCSS Styles
Binding an Array as a Class
Adding Angular Animations to the Template
Adding Animation Syntax to the Component File
Creating the Toast Service
Accessing Service's BehaviorSubjects with the Async Pipe
Two-Way Bind the Component
Using the Async Pipe in the Template
Creating and Styling the Component
First, create new component in the terminal by running:
$ 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.
<!--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!
// 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
istrue
. We'll add this property to the component file next. - The
{{ toastMessage }}
string is binding a message string assigned to thetoastMessage
property that we'll also find in the component file next!
<!--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>
// 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.
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:
// 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:
And if you change the class on the element from toast-class
to warning-toast
, it will look like this:
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).
// 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 { }
}
<!--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:
@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?)
<!--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.
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):
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!
So now we need to have something trigger the service!
3 Things to Remember
The toast component needs three inputs:
- A class (
string
) that's eithersuccess-toast
,warning-toast
, ordanger-toast
. - A boolean result for
showsToast
to either or show (true
) or hide (false
) which impacts the animation - A message (
string
) that binds to thetoastMessage
so that the message will display
Creating the Toast Service
Add the service with the following command in your terminal
$ 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
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.
// 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.
- 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) - A
dismissToast()
to indicate that the toast is back to false ```typescript
// toast.service.ts
export class ToastService {
public showsToast$: BehaviorSubject = new BehaviorSubject(false);
public toastMessage$: BehaviorSubject = new BehaviorSubject('Default Toast Message');
public toastState$: BehaviorSubject = new BehaviorSubject(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
.
<!--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!
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:
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
Ps. Check out this code in my my GitHub repo