typescript
70 lines · 8 steps
Building a signal-based toast service in Angular
An injectable Angular service manages transient notifications with signals and auto-dismiss timers.
Explained by
highlit
1import { Injectable, signal, computed } from '@angular/core';
2
3export type ToastKind = 'success' | 'error' | 'info' | 'warning';
4
5export interface Toast {
6 id: number;
7 kind: ToastKind;
8 message: string;
9 title?: string;
10 duration: number;
11}
12
13interface ToastOptions {
14 title?: string;
15 duration?: number;
16}
17
18@Injectable({ providedIn: 'root' })
19export class ToastService {
20 private readonly _toasts = signal<Toast[]>([]);
21 private readonly timers = new Map<number, ReturnType<typeof setTimeout>>();
22 private nextId = 0;
23
24 readonly toasts = this._toasts.asReadonly();
25 readonly hasToasts = computed(() => this._toasts().length > 0);
26
27 success(message: string, options?: ToastOptions) {
28 return this.show('success', message, options);
29 }
30
31 error(message: string, options?: ToastOptions) {
32 return this.show('error', message, { duration: 8000, ...options });
33 }
34
35 info(message: string, options?: ToastOptions) {
36 return this.show('info', message, options);
37 }
38
39 warning(message: string, options?: ToastOptions) {
40 return this.show('warning', message, options);
41 }
42
43 dismiss(id: number) {
44 const timer = this.timers.get(id);
45 if (timer) {
46 clearTimeout(timer);
47 this.timers.delete(id);
48 }
49 this._toasts.update((list) => list.filter((t) => t.id !== id));
50 }
51
52 clear() {
53 this.timers.forEach(clearTimeout);
54 this.timers.clear();
55 this._toasts.set([]);
56 }
57
58 private show(kind: ToastKind, message: string, options?: ToastOptions): number {
59 const id = ++this.nextId;
60 const duration = options?.duration ?? 4000;
61 const toast: Toast = { id, kind, message, title: options?.title, duration };
62
63 this._toasts.update((list) => [...list, toast]);
64
65 if (duration > 0) {
66 this.timers.set(id, setTimeout(() => this.dismiss(id), duration));
67 }
68 return id;
69 }
70}
01 / 01
STEP 01
‹ swipe to step through ›
Walkthrough
Space play
←→ step
click any line
Three takeaways
- 1A private writable signal paired with a public read-only view keeps mutation controlled inside the service.
- 2Convenience methods that delegate to one private core keep behavior consistent while defaults vary per kind.
- 3Tracking timers in a Map lets you cancel scheduled dismissals precisely when a toast is removed early.
Related explainers
typescript
import { Component } from '@angular/core'; import { NgForm } from '@angular/forms'; interface SignupModel {
How template-driven forms validate in Angular
forms
two-way-binding
validation
Intermediate
9 steps
javascript
import { useState, useEffect, useCallback } from 'react'; export function useCountdown(initialSeconds) { const [secondsLeft, setSecondsLeft] = useState(initialSeconds);
Building a useCountdown hook in React
custom-hooks
state-management
side-effects
Intermediate
8 steps
typescript
import { Controller } from '@nestjs/common'; import { MessagePattern, Payload,
Manual RabbitMQ acks in a NestJS controller
microservices
message-queue
acknowledgement
Intermediate
8 steps
typescript
type LazyImageOptions = { rootMargin?: string; loadedClass?: string; };
Lazy-loading images with IntersectionObserver
intersectionobserver
lazy-loading
performance
Intermediate
7 steps
javascript
import { useState, useRef } from "react"; export function TagInput({ initialTags = [], onChange }) { const [tags, setTags] = useState(initialTags);
Building a tag input in React
controlled-inputs
state-management
keyboard-handling
Intermediate
8 steps
typescript
type DeepPartial<T> = T extends object ? { [K in keyof T]?: DeepPartial<T[K]> } : T;
Type-safe deep merge in TypeScript
recursion
conditional-types
mapped-types
Advanced
7 steps
Share this explainer
Here's the card — post it anywhere.
Made with highlit — turn any snippet into a walkthrough like this in about a minute.
Explain your code
Embed this explainer
Drop the interactive walkthrough into a blog or docs. Views never cost a credit.
<iframe src="https://highlit.co/explainers/building-a-signal-based-toast-service-in-angular-explained-typescript-d785/embed?autoplay=1" width="100%" height="520" loading="lazy" style="border:0"></iframe>
Autoplay is on by default — add ?autoplay=0 to start paused.