Create a Custom Toaster Component in Angular 17+ Using Angular CDK

Learn how to create a custom toaster module library in Angular 17+ using Angular CDK Portal. This step-by-step guide includes detailed instructions and examples to help you build flexible and reusable toaster notifications for your Angular applications.


In my journey as a developer working in administrative systems, I've often needed to implement a Toaster Notification System that provides non-intrusive feedback to end-users. This feedback can be anything from notifying the user that their changes were saved successfully or showing a validation/server error. Angular's Component Dev Kit (CDK) offers a powerful tool called Portal that simplifies the process of creating and displaying dynamic components. In this article, I'll walk you through how to create a Toaster Component in Angular 17+ using the @angular/cdk Portal.

Prerequisites

Before we begin, make sure you have Angular and @angular/cdk installed in your project. If not, you can install them using:

npm install @angular/core @angular/cdk

Step 1: Set Up the Toaster Component

First, let's generate our Toaster Component using Angular's CLI which will queue and display our notifications.

ng generate component toaster

This command will generate the necessary files for our Toaster Component. Let's modify the HTML and CSS for a basic toaster:

toaster.component.html

<div class="toaster-container">
  <ng-template cdkPortalOutlet></ng-template>
</div>

You will notice here we are using the cdkPortalOutlet directive which is used to add a portal outlet to our template.

toaster.component.css

Let's add some very basic CSS for our toaster to get started.

.toaster-container {
  position: fixed;
  top: 20px;
  right: 20px;
  width: 300px;
  z-index: 1000;
}

Step 2: Create the Notification Component

Next, we need a component that will represent individual notifications. This component will be injected on demand inside the Portal we created earlier.

ng generate component notification

Modify the generated component to display a notification message:

notification.component.html

<div class="notification">
  <p>{{ message }}</p>
</div>

notification.component.ts

import { Component, Input } from "@angular/core";

@Component({
  selector: "app-notification",
  templateUrl: "./notification.component.html",
  styleUrls: ["./notification.component.css"],
})
export class NotificationComponent {
  // Allow this component to accept an input called `message`
  @Input() message: string = "";
}

notification.component.css

Let's customize our notification/message with some basic styling.

.notification {
  background-color: #323232;
  color: white;
  padding: 10px;
  margin-bottom: 10px;
  border-radius: 4px;
  box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
}

Step 3: Set Up the Portal Infrastructure

Now, we will set up the infrastructure to use portals in our Toaster Component.

toaster.component.ts

import { Component, ComponentRef, OnInit } from "@angular/core";
import { ComponentPortal, PortalInjector } from "@angular/cdk/portal";
import { Overlay, OverlayRef } from "@angular/cdk/overlay";
import { NotificationComponent } from "../notification/notification.component";

@Component({
  selector: "app-toaster",
  templateUrl: "./toaster.component.html",
  styleUrls: ["./toaster.component.css"],
})
export class ToasterComponent implements OnInit {
  private overlayRef: OverlayRef;

  constructor(private overlay: Overlay) {}

  ngOnInit(): void {
    // Initialize an overlay and define a relative position
    // Change this to control where your toaster should appear
    this.overlayRef = this.overlay.create({
      hasBackdrop: false,
      positionStrategy: this.overlay
        .position()
        .global()
        .top("20px")
        .right("20px"),
    });
  }

  showNotification(message: string): void {
    // Create a Portal instance of our `NotificationComponent`
    const notificationPortal = new ComponentPortal(NotificationComponent);
    // Attach our `NotificationComponent` to the Overlay Portal
    const notificationRef: ComponentRef<NotificationComponent> =
      this.overlayRef.attach(notificationPortal);
    // Now we can access the component instance
    // and we can pass input for the `message` prop we created.
    notificationRef.instance.message = message;

    // Set a timeout to detach our component from the overlay
    // after 3000 milliseconds
    setTimeout(() => {
      this.overlayRef.detach();
    }, 3000);
  }
}

Step 4: Use the Toaster Component

To use the Toaster Component, we'll integrate it into our main application component.

app.component.html

<!-- Make our toaster available globally -->
<app-toaster></app-toaster>
<!-- Add a button to test our toaster -->
<button (click)="showToast()">Show Toast</button>

app.component.ts

import { Component, ViewChild } from "@angular/core";
import { ToasterComponent } from "./toaster/toaster.component";

@Component({
  selector: "app-root",
  templateUrl: "./app.component.html",
  styleUrls: ["./app.component.css"],
})
export class AppComponent {
  @ViewChild(ToasterComponent) toaster!: ToasterComponent;

  showToast(): void {
    this.toaster.showNotification("This is a toast message!");
  }
}

Step 5: Adjust Module Declarations

Make sure all components and CDK modules are declared and imported in your AppModule.

app.module.ts

import { NgModule } from "@angular/core";
import { BrowserModule } from "@angular/platform-browser";
import { AppComponent } from "./app.component";
import { ToasterComponent } from "./toaster/toaster.component";
import { NotificationComponent } from "./notification/notification.component";
import { OverlayModule } from "@angular/cdk/overlay";
import { PortalModule } from "@angular/cdk/portal";

@NgModule({
  declarations: [AppComponent, ToasterComponent, NotificationComponent],
  imports: [BrowserModule, OverlayModule, PortalModule],
  providers: [],
  bootstrap: [AppComponent],
})
export class AppModule {}

Conclusion

Using Angular's CDK Portal, we created a dynamic Toaster Component that can display notification messages. This method is not only flexible but also makes use of Angular's powerful CDK utilities to manage dynamic content efficiently. Now, you can expand this toaster to handle different types of notifications, styles, and durations based on your application's needs.

More posts in JavaScript