How to customize an Alert Message Box in Angular?

文章目錄

本篇文章,將以 Angular 17 實作一個簡單的 Alert 通知系統。 包含 Alert 模型、Alert 服務和 Alert 元件的建立,並且使用 Angular Animations 添加動畫效果。

建立模型

建立 Alert 模型,包含 type 和 message 兩個屬性,並建立 enum AlertType 來表示不同類型的通知。

export interface IAlertOption {
  message: string;
  type?: AlertType;
  autoRemove?: boolean;
  removeAfter?: number;
}

export class Alert {
  type: AlertType;
  message: string;

  constructor(msg: string, type: AlertType = AlertType.Success) {
    this.type = type;
    this.message = msg;
  }
}

export enum AlertType {
  Error,
  Success,
  Info,  
}

建立服務

  1. 使用 Angular CLI 指令,建立 Alert 服務,管理通知邏輯。

    ng g s alert
    
  2. 建立 BehaviorSubject 發佈保留通知。

    • value:取得保留的所有通知。
    • next:用來發佈通知給訂閱者。
  3. 通知邏輯包含,發佈通知(set)、取代最後通知的訊息(replace)、移除通知(remove)、自動移除通知(autoRemove)。

    export class AlertService {
    
        alerts$ = new BehaviorSubject<Alert[]>([]);
    
        constructor() { }
    
        set({message, type, autoRemove = true, removeAfter}: IAlertOption) {
            const alert = new Alert(message, type);
            this.alerts$.next([...this.alerts$.value, alert]);
            if(autoRemove) this.autoRemove(alert, removeAfter).subscribe();
        }
    
        replace(msg: string) {
            const alerts = this.alerts$.value;
            const length = alerts.length;
            if(length > 0) alerts[length - 1].message = msg;
        }
    
        remove(alert: Alert) {
            this.alerts$.next(this.alerts$.value.filter(_ => _ != alert));
        }
    
        autoRemove(alert: Alert, removeAfter: number = 10000) { // 10 sec
            return timer(removeAfter).pipe(
                mergeMap(() => of(this.remove(alert)))
            );
        }
    }
    

建立動畫效果

  1. 建立可重複使用的動畫。

    import { animate, style, transition, trigger } from "@angular/animations";
    
    export const fadeInOut = trigger('fadeInOut', [
        transition(':enter', [
            style({opacity: 0}),
            animate(500, style({opacity: 1})) 
        ]),
        transition(':leave', [
            animate(500, style({opacity: 0})) 
        ])
    ]);
    
  2. 啟用動畫模組。

    // app.config.ts
    
    import { provideAnimations } from '@angular/platform-browser/animations';
    
    export const appConfig: ApplicationConfig = {
        providers: [
            provideAnimations()
        ]
    };
    

建立元件

  1. 使用 Angular CLI 指令建立 Alert 元件。

    ng g c alert
    
  2. 實現元件

    • animations: [fadeInOut]:引入動畫效果。
    • alerts$:從 AlertService 取得發佈通知的 BehaviorSubject,並在模板中使用 AsyncPipe 訂閱。
    • close:手動關閉通知。
    • color:根據 AlertType 設置顏色樣式。
    @Component({
    selector: 'app-alert',
    standalone: true,
    imports: [AsyncPipe, NgClass],
    animations: [fadeInOut],
    templateUrl: './alert.component.html',
    styleUrl: './alert.component.css'
    })
    export class AlertComponent {
    
        alerts$: Observable<Alert[]> = this._service.alerts$;
    
        constructor(private _service: AlertService) {}
    
        close(alert: Alert) {
            this._service.remove(alert);
        }
    
        color(type: AlertType): string {
            const colorMap: { [key in AlertType]: string } = {
                [AlertType.Error]: 'alert-danger',
                [AlertType.Success]: 'alert-success',
                [AlertType.Info]: 'alert-info'
            };
    
            return colorMap[type] || 'alert-info';
        }
    }
    
  3. 建構模板

    • alerts$ | async:使用 AsyncPipe 訂閱通知。
    • [@fadeInOut]:綁定動畫觸發器。
    <!-- alert.component.html -->
    @if(alerts$ | async; as alerts) {
        <div class="alert">
            <ul class="list">
                @for (alert of alerts; track $index) {
                    <li class="list-item" [ngClass]="color(alert.type)" [@fadeInOut]>
                        <button class="btn btn-close" (click)="close(alert)">x</button>
                        <span>{{ alert.message }}</span>
                    </li>
                }
            </ul>
        </div>
    }
    
  4. 裝飾元件

    /* alert.component.css */
    .alert {
        position: fixed;
        right: 0;
        z-index: 2000;
        width: 100%;
        max-width: 400px;
    }
    
    .list {
        list-style: none;
        padding: 0;
        margin: 0;
    }
    
    .list-item {
        position: relative;
        padding: 1.5em;
        margin: 0 1em;
        border-radius: .25em;
        box-shadow: rgba(0, 0, 0, 0.16) 0px 1px 4px;
    }
    
    .list-item:not(:last-child) {
        margin-bottom: 1em;
    }
    
    .btn-close {
        position: absolute;
        top: 8px;
        right: 12px;
        color: inherit;
        padding: 0;
    }
    .btn-close:hover {
        filter: brightness(1.5);
    }
    
    .alert-danger {
        color: #721c24;
        background-color: #f8d7da;
    }
    
    .alert-success {
        color: #155724;
        background-color: #d4edda;
    }
    
    .alert-info {
        color: #0c5460;
        background-color: #d1ecf1;
    }