iT邦幫忙

2025 iThome 鐵人賽

DAY 23
0
Vue.js

作為 Angular 專家探索 Vue 3 和 Svelte 5系列 第 23

第22天 - Alert Component 第一部份 - Alert List 和 Alert Components

  • 分享至 

  • xImage
  •  

第22天,我開始進行 Vue 3、Angular 20 以及 Svelte 5 的 Alert Component 練習。

該 Alert component 使用 DaisyUI 的 Alert 組件和 TailwindCSS 實用程式類別 (utility classes) 來進行樣式設計。我也學到了 Vue 3.5+ 中使用 defineModel 在組件間實現雙向綁定 (two-way binding) 的方法。此外,我了解到 Svelte 5 使用 $bindable 來實現子組件向父組件的資料傳遞。而在 Angular 中,則是透過可寫的訊號 (Writable Signal) model,允許輸入資料在父子組件之間雙向流動。

這個小練習將分成五個部分。第3、4和第5部分是額外的,因為我希望能夠更改警示樣式並重新打開警示訊息。第22天,我開始進行 Vue 3、Angular 20 以及 Svelte 5 的 Alert Component 練習。

部分

  • 第1部分:安裝 DaisyUI,Alert List 和 Alert Components
  • 第2部分:為 Alert Component 動態渲染圖示
  • 第3部分:(額外)Alert Bar Component - 第1部分
  • 第4部分:(額外)重構 Alert Dropdown 組件
  • 第5部分:(額外)Alert Bar Component - 第2部分

讓我們從 Alert List 和 Alert Component 開始,因為 DaisyUI 已經提供了現成的警示範例。Alert List 組件是一個容器,用來迭代警示清單以顯示不同類型的警示。

安裝

Vue 3 and SvelteKit

npm install tailwindcss@latest @tailwindcss/vite@latest daisyui@latest

將 tailwind CSS 加入到 Vite

import tailwindcss from "@tailwindcss/vite";

export default defineConfig({
  plugins: [tailwindcss(), ...other plugins...],
});

在 CSS 中啟用 DaisyUI 插件

@import "tailwindcss";
@plugin "daisyui";

Angular 20 application

npm install daisyui@latest tailwindcss@latest @tailwindcss/postcss@latest postcss@latest --force

配置檔案

// .postcssrc.json
{
  "plugins": {
    "@tailwindcss/postcss": {}
  }
}

在 CSS 中啟用 DaisyUI 插件

// src/style.css

@import "tailwindcss";
@plugin "daisyui";

將警示 (Alert) 的 HTML 複製到 AlertList 組件中

我從 https://daisyui.com/components/alert 複製了 info、success、warning 和 error 警示的 HTML,並貼到 AlertList 組件中。

<script setup lang="ts"></script>
<template>
  <div role="alert" class="alert alert-info">
    <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" class="h-6 w-6 shrink-0 stroke-current">
      <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"></path>
    </svg>
    <span>New software update available.</span>
  </div>
  <div role="alert" class="alert alert-success">
    <svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6 shrink-0 stroke-current" fill="none" viewBox="0 0 24 24">
      <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" />
    </svg>
    <span>Your purchase has been confirmed!</span>
  </div>
  <div role="alert" class="alert alert-warning">
    <svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6 shrink-0 stroke-current" fill="none" viewBox="0 0 24 24">
      <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z" />
    </svg>
    <span>Warning: Invalid email address!</span>
  </div>
  <div role="alert" class="alert alert-error">
    <svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6 shrink-0 stroke-current" fill="none" viewBox="0 0 24 24">
      <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 14l2-2m0 0l2-2m-2 2l-2-2m2 2l2 2m7-2a9 9 0 11-18 0 9 9 0 0118 0z" />
    </svg>
    <span>Error! Task failed successfully.</span>
  </div>
</template>

對我來說,先看到清單的結構比較容易,然後再重構模板以利用Alert 組件。

我們先建立 Alert 組件,接著將它匯入到 AlertList 組件中。

Static HTML Template of the alert

<div role="alert" class="alert alert-info">
    <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" class="h-6 w-6 shrink-0 stroke-current">
      <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"></path>
    </svg>
    <span>New software update available.</span>
</div>

我們需要讓 Alert 組件可重複使用,適用於不同類型、圖示和文字。因此,typeicontext 必須可以配置。

Alert 組件

Vue 3 application

建立 Props

<script setup lang="ts">
    type Prop = {
        type: string;
    }
    
    const { type } = defineProps<Prop>()
</script>

Prop 定義了警示的 type,例如 info、success、warning 和 error。

Derive the CSS classes of the alert

<script setup lang="ts">
    const alertColor = computed(() => ({
        info: 'alert-info',
        warning: 'alert-warning',
        error: 'alert-error',
        success: 'alert-success'
    }[type]))   
    
    const alertClasses = computed(() => `alert ${alertColor.value}`)
</script>

type prop 用來推導出 alertColoralertClasses 這兩個計算屬性(computed refs)。alertColor 計算屬性會在字典中使用 type 作為索引,以取得對應的 CSS 類別;alertClasses 計算屬性則將這些 CSS 類別串接起來,並綁定到 class 屬性上。

<div role="alert" :class="alertClasses"></div>

依據 type 條件有條件地渲染圖示。

使用 v-ifv-else-if 指令 (directive),根據 type 有條件地渲染 SVG 圖示。

<div role="alert" :class="alertClasses">
    <svg v-if="type == 'info'" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" class="h-6 w-6 shrink-0 stroke-current">
      <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"></path>
    </svg>
    <svg v-else-if="type == 'success'" xmlns="http://www.w3.org/2000/svg" class="h-6 w-6 shrink-0 stroke-current" fill="none" viewBox="0 0 24 24">
      <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" />
    </svg>
    <svg v-else-if="type == 'warning'" xmlns="http://www.w3.org/2000/svg" class="h-6 w-6 shrink-0 stroke-current" fill="none" viewBox="0 0 24 24">
      <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z" />
    </svg>
    <svg v-else-if="type == 'error'"  xmlns="http://www.w3.org/2000/svg" class="h-6 w-6 shrink-0 stroke-current" fill="none" viewBox="0 0 24 24">
      <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 14l2-2m0 0l2-2m-2 2l-2-2m2 2l2 2m7-2a9 9 0 11-18 0 9 9 0 0118 0z" />
    </svg>
</div>

這個解決方案可行,但擴展性不足。我們會在第二部分使用圖示組件和動態渲染來重構此方案。

使用 Slot 來顯示文字。

<div role="alert" :class="alertClasses">
    <!-- HTML to render the SVG icon -->
    <span><slot /></span>
</div>

SvelteKit application

建立 Props

// lib/alert.type.ts
export type AlertType = 'info' | 'success' | 'warning' | 'error';

export type AlertMessage ={
    type: AlertType;
    message: string;
};
<script lang="ts">
    type Prop = {
        alert: AlertMessage; 
    }
    
    const { alert }: Prop = $props()
</script>

推導警示 (Alert) 的 CSS 類別 (Classes)

<script lang="ts">
    const alertColor = $derived.by(() => ({
        info: 'alert-info',
        warning: 'alert-warning',
        error: 'alert-error',
        success: 'alert-success'
    }[alert.type]))   
    
    const alertClasses = $derived(`alert ${alertColor}`)
</script>

alert prop 用來衍生 alertColoralertClasses 計算屬性。alertColor 計算屬性會以 alert.type 作為字典的索引來取得 CSS 類別。alertClasses 計算屬性則將這些 CSS 類別串接起來,並綁定到 class 屬性上。

<div role="alert" class={alertClasses}></div>

按類型有條件地渲染圖示。

使用 if-else-if 控制流程,根據 type 條件有條件地渲染 SVG 圖示。

<div role="alert" class={alertClasses}>
    {#if alert.type == 'info'}
    <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" class="h-6 w-6 shrink-0 stroke-current">
      <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"></path>
    </svg>
    {:else if alert.type == 'success'}
    <svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6 shrink-0 stroke-current" fill="none" viewBox="0 0 24 24">
      <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" />
    </svg>
    {:else if alert.type == 'warning'}
    <svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6 shrink-0 stroke-current" fill="none" viewBox="0 0 24 24">
      <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z" />
    </svg>
    {:else if alert.type == 'error'}
    <svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6 shrink-0 stroke-current" fill="none" viewBox="0 0 24 24">
      <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 14l2-2m0 0l2-2m-2 2l-2-2m2 2l2 2m7-2a9 9 0 11-18 0 9 9 0 0118 0z" />
    </svg>
    {/if}
</div>

使用 Snippet 來渲染文字

Svelte 4 支援 slot,但 Svelte 5 使用 snippetsrender 標籤來投射內容。

type Props = {
    alert: AlertMessage;
    alertMessage: Snippet<[string]>;
}
<div role="alert" class={alertClasses}>
    <!-- HTML to render the SVG icon -->
    {@render alertMessage(alert.message) }
</div>

Angular 20 application

建立 Input Signals

export type AlertType = 'info' | 'success' | 'warning' | 'error';
@Component({
  selector: 'app-alert',
  template: '',
  changeDetection: ChangeDetectionStrategy.OnPush,
})
export class AlertComponent {
  type = input.required<AlertType>();
}

Derive the CSS classes of the alert

export class AlertComponent {
  type = input.required<AlertType>();
    
  alertColor = computed(() => {
    return {
        info: 'alert-info',
        warning: 'alert-warning',
        error: 'alert-error',
        success: 'alert-success'
    }[this.type()]
  });
    
  alertClasses = computed(() => `alert ${this.alertColor()}`);
}

type 輸入訊號用來推導 alertColoralertClasses 這兩個計算訊號。alertColor 計算訊號會以 dictionary 中的 type 索引來取得 CSS 類別;而 alertClasses 計算訊號則會串接 (concatenate) 這些 CSS 類別,並綁定到 class 屬性上。

@Component({
  selector: 'app-alert',
  template: `<div role="alert" [class]="alertClasses()"></div>`,
  changeDetection: ChangeDetectionStrategy.OnPush,
})
export class AlertComponent {}

根據類型有條件地渲染圖示

使用 if-else-if 控制流程,依照 type 有條件地渲染 SVG 圖示。

@Component({
  selector: 'app-alert',
  template: `
<div role="alert" [class]="alertClasses()">
    @if (type() === 'info'} {
    <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" class="h-6 w-6 shrink-0 stroke-current">
      <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"></path>
    </svg>
    } @else if (type() === 'success') {
    <svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6 shrink-0 stroke-current" fill="none" viewBox="0 0 24 24">
      <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" />
    </svg>
    } @else if (type() === 'warning') {
    <svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6 shrink-0 stroke-current" fill="none" viewBox="0 0 24 24">
      <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z" />
    </svg>
    } @else if (type() === 'error') {
    <svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6 shrink-0 stroke-current" fill="none" viewBox="0 0 24 24">
      <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 14l2-2m0 0l2-2m-2 2l-2-2m2 2l2 2m7-2a9 9 0 11-18 0 9 9 0 0118 0z" />
    </svg>
    }
</div>`,
  changeDetection: ChangeDetectionStrategy.OnPush,
})
export class AlertComponent {}

使用 NgContent 來渲染文字

Angular 使用 ng-content 來投射內容。

<div role="alert" class={alertClasses}>
    <!-- HTML to render the SVG icon -->
    {@render alertMessage(alert.message) }
</div>
```typescript
@Component({
  selector: 'app-alert',
  template: `
<div role="alert" [class]="alertClasses()">
   <!-- Render SVG icons conditionally by type -->
   <span><ng-content /></span>
</div>`,
  changeDetection: ChangeDetectionStrategy.OnPush,
})
export class AlertComponent {}

為 Alert 組件新增 Close 按鈕

Vue 3 application

Alert 組件新增一個關閉按鈕,以關閉該組件並向 AlertList 組件發出事件。

const emits = defineEmits<{
    (e: 'closed', type: string): void
}>()

定義一個 closed 事件,將關閉的類型(closed type)傳遞到父組件。

const closed = ref(false)

function closeAlert() {
    closed.value = true
    emits('closed', type)
}
<template>
  <div role="alert" :class="alertClasses" v-if="!closed">
    <div>
        <!-- previous html code -->
        <button class="btn btn-sm btn-primary" alt="Close button" @click="closeAlert">X</button>
    </div>
  </div>
</template>

新增一個 v-if,當 closed ref 為 true 時關閉 <div> 元素。

當按鈕被點擊時,closeAlert 函數會將 closed ref 設定成 true,並發送 closed 事件給父組件。該事件的參數是警示(alert)的類型。


SvelteKit application

type Props = {
    notifyClosed?: (type: string) => void;
}

與 Vue 3 和 Angular 不同,Svelte 5 不會將事件發送給父組件。父組件會提供一個回呼(callback prop) 給子組件,由子組件在事件處理函數中調用。

let closed = $state(false);

function closeAlert() {
    closed = true;
    notifyClosed?.(alert.type)
}
{#if !closed}
    <div role="alert" class={alertClasses}>
        <!-- previous HTML code -->
        <div>
            <button class="btn btn-sm btn-primary" title="Close button" onclick={closeAlert}>X</button>
        </div>
    </div>
{/if}

當按鈕被點擊時,closeAlert 方法會將 closed rune 設為 true,並調用 notifyClosed callback prop,在父組件執行邏輯。該 callback prop 的參數是警示(alert)的類型。


Angular 20 application

import { ChangeDetectionStrategy, Component, computed, input, output, signal, viewChild, ViewContainerRef } from '@angular/core';
import { AlertType } from '../alert.type';

@Component({
  selector: 'app-alert',
  template: `
    @if (!closed()) {
      <div role="alert" class="mb-[0.75rem]" [class]="alertClasses()">
        <!-- previous HTML codes -->
        <div>
          <button class="btn btn-sm btn-primary" alt="Close button" (click)="closeAlert()">X</button>
        </div>
      </div>
    }
  `,
  changeDetection: ChangeDetectionStrategy.OnPush,
})
export class AlertComponent {
  /* omit other codes to keep the class succinct */
    
  closed = signal(false);

  closeNotification = output<string>();

  closeAlert() {
    this.closed.set(true);
    this.closeNotification.emit(this.type());
  }
}

closed 訊號 (signal) 決定是否顯示或隱藏 <div> 元素。

closeNotification 輸出 (output) 會在 Alert 組件關閉時通知父組件。

使用 @if 控制流程,當 closed 訊號 (signal) 為 true 時隱藏 <div> 元素。

當按鈕被點擊時,closeAlert 方法會將 closed 訊號設為 true,並發出 closeNotification 自訂事件給父組件。該自訂事件的參數是警示(alert)的類型。


Alert List 組件

Vue 3 application

導入 Alert 組件,並在迴圈中顯示 info、success、warning 和 error 警示。alerts 是由 App 傳入的 prop

<script setup lang="ts">
import Alert from './Alert.vue'

const props = defineProps<{
  alerts: { type: string; message: string }[]
}>()

function handleClosed(type: string) {
   console.log(type)
} 
</script>
<template>
  <h2>Alert Components (Vue ver.)</h2>

  <Alert v-for="{ type, message } in alerts"
    class="mb-[0.75rem]"
    :key="type"
    :type="type"
    @closed="handleClosed">
    {{  message }}
  </Alert>
</template>

closed 事件發生時,handleClosed 函數會在控制台印出警示(alert)的類型。


SvelteKit application

導入 Alert 組件,並在迴圈中顯示 info、success、warning 和 error 警示。alerts 是由 page 路由提供的 prop。

<script lang="ts">
    import Alert from './alert.svelte';
    import type { AlertMessage } from './alert.type';

    type Props = {
        alerts: AlertMessage[];
    }

    const { alerts }: Props = $props()

    function notifyClosed(type: string) {
        console.log(type);
    }
</script>
{#snippet alertMessage(text: string)}
    <span>{text}</span>
{/snippet}

<h2>Alert Components (Svelte ver.)</h2>

{#each alerts as alert (alert.type) } 
    <Alert {alert} {alertMessage} {notifyClosed} />
{/each}

alerts rune 將類型提供給 Alert 組件,alertMessage snippet 用來渲染訊息。

notifyClosed 事件發生時,notifyClosed 函數會在控制台印出警示(alert)的類型。


Angular 20 application

AlertListComponent 匯入 AlertComponent,並將其加入 @Component 裝飾器的 imports 陣列。該組件在 for 迴圈中顯示 info、success、warning 和 error 類型的警示。

import { ChangeDetectionStrategy, Component, computed, input, signal } from '@angular/core';
import { AlertType } from '../alert.type';
import { AlertComponent } from '../alert/alert.component';

@Component({
  selector: 'app-alert-list',
  imports: [AlertComponent],
  template: `
    @for (alert of alerts(); track alert.type) {
      <app-alert [type]="alert.type" >
        {{ alert.message }}
      </app-alert>
    }
  `,
  changeDetection: ChangeDetectionStrategy.OnPush,
})
export class AlertListComponent {
  alerts = input.required<{ type: AlertType; message: string }[]>();
}

alerts 是一個必需的輸入訊號 (input signal),由 AppComponent 傳遞一個陣列進來。


App 組件

Vue 3 application

<script setup lang="ts">
import { ref } from 'vue'
import AlertList from './components/AlertList.vue'

const alerts = ref([
  { 
    type: 'info',
    message: 'New software update available.'
  }, 
  { 
    type: 'success',
    message: 'Your purchase has been confirmed!'
  }, 
  { 
    type: 'warning',
    message: 'Warning: Invalid email address!'
  }, 
  { 
    type: 'error',
    message: 'Error! Task failed successfully.'
  }])
</script>

App 組件建立一個 alerts ref,用來儲存警示類型 (alert types) 和訊息 (message) 。在 <script> 標籤中,它匯入 AlertList 組件,並在 <template> 標籤裡渲染該組件。

<template>
  <main>
    <AlertList :alerts="alerts" />
  </main>
</template>

alerts 被作為 prop 傳遞給 AlertList 組件。


SvelteKit application

<script lang="ts">
	import AlertList from '$lib/alert-list.svelte';
	import type { AlertMessage } from '$lib/alert.type';

	const alerts = $state<AlertMessage[]>(...same alert array to save space...)
</script>

App 組件建立一個 alerts rune,用以儲存警示類型和訊息。在 <script> 標籤中,它匯入 AlertList 組件,並在 <main> 元素中渲染該組件。

<main>
	<AlertList alerts={alerts} />
</main>

alerts 被作為 prop 傳遞給 AlertList 組件。


Angular 20 application

import { ChangeDetectionStrategy, Component, signal } from '@angular/core';
import { AlertListComponent } from './alert-list/alert-list.component';
import { AlertType } from './alert.type';

@Component({
  selector: 'app-root',
  imports: [AlertListComponent],
  template: `
<div id="app">
  <main>
    <app-alert-list [alerts]="alerts()" />
  </main>
</div>
  `,
  changeDetection: ChangeDetectionStrategy.OnPush,
})
export class AppComponent {
  alerts = signal<{ type: AlertType, message: string }[]>(... same alert array to save space ...)
}

AppComponent 匯入 AlertListComponent 並將其加入 @Component 裝飾器的 imports 陣列中。alerts 訊號 (signal) 被傳遞給 AlertListComponentalerts 輸入訊號 (input signal)。


我們已成功在 Vue、Svelte 和 Angular 框架中建立了 alert list。

Github Repositories

Github Pages

資源


上一篇
第21天 - 部署 Github 個人檔案專案到 Github Pages
系列文
作為 Angular 專家探索 Vue 3 和 Svelte 523
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言