How to customize an Alert Message Box in Angular?

文章目錄

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

建立模型

建立 Alert 模型,包含 typemessage 兩個屬性,並建立 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;
    }