Notification System
A complete notification system with auto-dismiss, multiple types, animations, and queue management.
Features Demonstrated
Dependency Injection - Singleton service pattern
Event Aggregator - Global notification triggering
Animations - CSS transitions for enter/leave
Timers - Auto-dismiss with setTimeout
Array manipulation - Add/remove notifications
Dynamic CSS classes - Type-based styling
Conditional rendering - Show/hide based on array length
Code
Service (notification-service.ts)
// src/services/notification-service.ts
import { DI } from '@aurelia/kernel';
export interface Notification {
id: string;
type: 'success' | 'error' | 'warning' | 'info';
title: string;
message: string;
duration: number; // milliseconds, 0 = no auto-dismiss
dismissible: boolean;
timestamp: Date;
expiresAt?: number;
remaining?: number;
}
export const INotificationService = DI.createInterface<INotificationService>(
'INotificationService',
x => x.singleton(NotificationService)
);
export interface INotificationService {
readonly notifications: Notification[];
show(options: Partial<Notification>): string;
success(title: string, message: string, duration?: number): string;
error(title: string, message: string, duration?: number): string;
warning(title: string, message: string, duration?: number): string;
info(title: string, message: string, duration?: number): string;
dismiss(id: string): void;
clear(): void;
}
class NotificationService implements INotificationService {
notifications: Notification[] = [];
private nextId = 1;
private timers = new Map<string, number>();
private progressTimers = new Map<string, number>();
show(options: Partial<Notification>): string {
const notification: Notification = {
id: `notification-${this.nextId++}`,
type: options.type || 'info',
title: options.title || '',
message: options.message || '',
duration: options.duration !== undefined ? options.duration : 5000,
dismissible: options.dismissible !== undefined ? options.dismissible : true,
timestamp: new Date(),
remaining: options.duration ?? 5000,
expiresAt: options.duration ? Date.now() + options.duration : undefined
};
// Add to beginning of array (newest first)
this.notifications.unshift(notification);
// Auto-dismiss if duration > 0
if (notification.duration > 0) {
const timer = window.setTimeout(() => {
this.dismiss(notification.id);
}, notification.duration);
this.timers.set(notification.id, timer);
const progress = window.setInterval(() => {
if (!notification.expiresAt) return;
const remaining = Math.max(notification.expiresAt - Date.now(), 0);
notification.remaining = remaining;
if (remaining <= 0) {
window.clearInterval(progress);
this.progressTimers.delete(notification.id);
}
}, 100);
this.progressTimers.set(notification.id, progress);
}
return notification.id;
}
success(title: string, message: string, duration = 5000): string {
return this.show({ type: 'success', title, message, duration });
}
error(title: string, message: string, duration = 0): string {
// Errors don't auto-dismiss by default
return this.show({ type: 'error', title, message, duration });
}
warning(title: string, message: string, duration = 7000): string {
return this.show({ type: 'warning', title, message, duration });
}
info(title: string, message: string, duration = 5000): string {
return this.show({ type: 'info', title, message, duration });
}
dismiss(id: string): void {
// Clear timer if exists
const timer = this.timers.get(id);
if (timer) {
clearTimeout(timer);
this.timers.delete(id);
}
const progress = this.progressTimers.get(id);
if (progress) {
clearInterval(progress);
this.progressTimers.delete(id);
}
// Remove notification
const index = this.notifications.findIndex(n => n.id === id);
if (index !== -1) {
this.notifications.splice(index, 1);
}
}
clear(): void {
// Clear all timers
this.timers.forEach(timer => clearTimeout(timer));
this.timers.clear();
this.progressTimers.forEach(interval => clearInterval(interval));
this.progressTimers.clear();
// Clear all notifications
this.notifications = [];
}
}Component (notification-container.ts)
// src/components/notification-container.ts
import { resolve } from '@aurelia/kernel';
import { INotificationService } from '../services/notification-service';
export class NotificationContainer {
private notificationService = resolve(INotificationService);
get notifications() {
return this.notificationService.notifications;
}
dismiss(id: string) {
this.notificationService.dismiss(id);
}
getIcon(type: string): string {
switch (type) {
case 'success': return '✓';
case 'error': return '✕';
case 'warning': return '⚠';
case 'info': return 'ⓘ';
default: return '';
}
}
getProgressWidth(notification: any): number {
if (notification.duration === 0) return 0;
const remaining = notification.remaining ?? notification.duration;
return Math.max((remaining / notification.duration) * 100, 0);
}
}Template (notification-container.html)
<!-- src/components/notification-container.html -->
<div class="notification-container">
<div
repeat.for="notification of notifications"
class="notification notification-${notification.type}">
<div class="notification-icon">
${getIcon(notification.type)}
</div>
<div class="notification-content">
<div class="notification-title">${notification.title}</div>
<div class="notification-message">${notification.message}</div>
<!-- Progress bar for auto-dismiss -->
<div
if.bind="notification.duration > 0"
class="notification-progress">
<div
class="notification-progress-bar"
style.width.bind="getProgressWidth(notification) + '%'">
</div>
</div>
</div>
<button
if.bind="notification.dismissible"
type="button"
click.trigger="dismiss(notification.id)"
class="notification-close"
aria-label="Dismiss notification">
×
</button>
</div>
</div>Styles (notification-container.css)
.notification-container {
position: fixed;
top: 1rem;
right: 1rem;
z-index: 9999;
display: flex;
flex-direction: column;
gap: 0.75rem;
max-width: 400px;
width: calc(100% - 2rem);
}
.notification {
display: flex;
align-items: flex-start;
gap: 0.75rem;
padding: 1rem;
border-radius: 8px;
background: white;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
animation: slideIn 0.3s ease-out;
position: relative;
overflow: hidden;
}
@keyframes slideIn {
from {
transform: translateX(100%);
opacity: 0;
}
to {
transform: translateX(0);
opacity: 1;
}
}
.notification-icon {
width: 24px;
height: 24px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-weight: bold;
font-size: 16px;
flex-shrink: 0;
}
.notification-success {
border-left: 4px solid #4caf50;
}
.notification-success .notification-icon {
background-color: #4caf50;
color: white;
}
.notification-error {
border-left: 4px solid #f44336;
}
.notification-error .notification-icon {
background-color: #f44336;
color: white;
}
.notification-warning {
border-left: 4px solid #ff9800;
}
.notification-warning .notification-icon {
background-color: #ff9800;
color: white;
}
.notification-info {
border-left: 4px solid #2196f3;
}
.notification-info .notification-icon {
background-color: #2196f3;
color: white;
}
.notification-content {
flex-grow: 1;
}
.notification-title {
font-weight: 600;
margin-bottom: 0.25rem;
color: #333;
}
.notification-message {
font-size: 0.875rem;
color: #666;
line-height: 1.4;
}
.notification-close {
background: none;
border: none;
font-size: 1.5rem;
cursor: pointer;
color: #999;
padding: 0;
width: 24px;
height: 24px;
line-height: 1;
flex-shrink: 0;
}
.notification-close:hover {
color: #333;
}
.notification-progress {
position: absolute;
bottom: 0;
left: 0;
right: 0;
height: 4px;
background-color: rgba(0, 0, 0, 0.1);
overflow: hidden;
}
.notification-progress-bar {
height: 100%;
background-color: currentColor;
transition: width 0.1s linear;
}
.notification-success .notification-progress-bar {
background-color: #4caf50;
}
.notification-error .notification-progress-bar {
background-color: #f44336;
}
.notification-warning .notification-progress-bar {
background-color: #ff9800;
}
.notification-info .notification-progress-bar {
background-color: #2196f3;
}
/* Responsive */
@media (max-width: 640px) {
.notification-container {
top: auto;
bottom: 0;
left: 0;
right: 0;
max-width: 100%;
width: 100%;
border-radius: 0;
}
.notification {
border-radius: 0;
border-left: none;
border-top: 4px solid;
}
}Registration (main.ts)
// src/main.ts
import Aurelia from 'aurelia';
import { NotificationContainer } from './components/notification-container';
import { INotificationService } from './services/notification-service';
Aurelia
.register(NotificationContainer, INotificationService)
.app(component)
.start();Usage in Root Component (my-app.html)
<!-- src/my-app.html -->
<notification-container></notification-container>
<!-- Your app content -->
<au-viewport></au-viewport>Usage in Any Component
// src/pages/dashboard.ts
import { resolve } from '@aurelia/kernel';
import { INotificationService } from '../services/notification-service';
export class Dashboard {
private notifications = resolve(INotificationService);
async saveData() {
try {
await this.apiClient.save(this.data);
this.notifications.success(
'Saved!',
'Your changes have been saved successfully.'
);
} catch (error) {
this.notifications.error(
'Error',
'Failed to save changes. Please try again.',
0 // Don't auto-dismiss errors
);
}
}
showWarning() {
this.notifications.warning(
'Low Storage',
'You are running low on storage space.',
7000
);
}
showInfo() {
this.notifications.info(
'Tip',
'You can use keyboard shortcuts to navigate faster.'
);
}
}How It Works
Singleton Service Pattern
The INotificationService is registered as a singleton, so the same instance is shared across the entire application. Any component can inject it and trigger notifications.
Auto-Dismiss Timer
When a notification is added with duration > 0, a timer is created that automatically dismisses it after the specified time. The timer is stored in a Map so it can be cleared if the user manually dismisses the notification.
Reactive Array
The notifications array is a reactive property. When notifications are added or removed, Aurelia's binding system automatically updates the DOM.
Progress Bar Animation
The progress bar uses a computed property (getProgressWidth) that calculates the percentage remaining based on elapsed time. This creates a smooth countdown animation.
Variations
Stacking vs Replacing
Current implementation stacks notifications. For "replacing" behavior (only show one at a time):
show(options: Partial<Notification>): string {
// Clear existing notifications of the same type
this.notifications = this.notifications.filter(n => n.type !== options.type);
// ... rest of implementation
}Position Options
Make position configurable:
<div class="notification-container notification-container-${position}">.notification-container-top-right { top: 1rem; right: 1rem; }
.notification-container-top-left { top: 1rem; left: 1rem; }
.notification-container-bottom-right { bottom: 1rem; right: 1rem; }
.notification-container-bottom-left { bottom: 1rem; left: 1rem; }Action Buttons
Add action buttons to notifications:
export interface NotificationAction {
label: string;
callback: () => void | Promise<void>;
}
export interface Notification {
// ... existing properties
actions?: NotificationAction[];
}<div if.bind="notification.actions" class="notification-actions">
<button
repeat.for="action of notification.actions"
type="button"
click.trigger="action.callback()"
class="btn btn-small">
${action.label}
</button>
</div>Pause on Hover
Pause the auto-dismiss timer when hovering:
pauseTimer(id: string) {
const timer = this.timers.get(id);
if (timer) {
clearTimeout(timer);
}
}
resumeTimer(notification: Notification) {
if (notification.duration > 0) {
const elapsed = Date.now() - notification.timestamp.getTime();
const remaining = Math.max(0, notification.duration - elapsed);
const timer = setTimeout(() => {
this.dismiss(notification.id);
}, remaining);
this.timers.set(notification.id, timer);
}
}<div
mouseover.trigger="notifications.pauseTimer(notification.id)"
mouseout.trigger="notifications.resumeTimer(notification)">
<!-- notification content -->
</div>Related
Dependency Injection - Singleton services
Event Aggregator - Alternative global communication
Conditional Rendering -
if.binddocumentationList Rendering -
repeat.fordocumentation
Last updated
Was this helpful?