第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 練習。
部分
讓我們從 Alert List 和 Alert Component 開始,因為 DaisyUI 已經提供了現成的警示範例。Alert List 組件是一個容器,用來迭代警示清單以顯示不同類型的警示。
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";
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";
我從 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
組件中。
<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
組件可重複使用,適用於不同類型、圖示和文字。因此,type
、icon
和 text
必須可以配置。
<script setup lang="ts">
type Prop = {
type: string;
}
const { type } = defineProps<Prop>()
</script>
Prop 定義了警示的 type
,例如 info、success、warning 和 error。
<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 用來推導出 alertColor
和 alertClasses
這兩個計算屬性(computed refs)。alertColor
計算屬性會在字典中使用 type
作為索引,以取得對應的 CSS 類別;alertClasses
計算屬性則將這些 CSS 類別串接起來,並綁定到 class 屬性上。
<div role="alert" :class="alertClasses"></div>
使用 v-if
和 v-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>
這個解決方案可行,但擴展性不足。我們會在第二部分使用圖示組件和動態渲染來重構此方案。
<div role="alert" :class="alertClasses">
<!-- HTML to render the SVG icon -->
<span><slot /></span>
</div>
// 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>
<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 用來衍生 alertColor
和 alertClasses
計算屬性。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>
Svelte 4 支援 slot,但 Svelte 5 使用 snippets
和 render
標籤來投射內容。
type Props = {
alert: AlertMessage;
alertMessage: Snippet<[string]>;
}
<div role="alert" class={alertClasses}>
<!-- HTML to render the SVG icon -->
{@render alertMessage(alert.message) }
</div>
export type AlertType = 'info' | 'success' | 'warning' | 'error';
@Component({
selector: 'app-alert',
template: '',
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class AlertComponent {
type = input.required<AlertType>();
}
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
輸入訊號用來推導 alertColor
和 alertClasses
這兩個計算訊號。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 {}
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
組件新增一個關閉按鈕,以關閉該組件並向 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)的類型。
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)的類型。
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
組件,並在迴圈中顯示 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)的類型。
導入 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)的類型。
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
傳遞一個陣列進來。
<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
組件。
<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
組件。
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) 被傳遞給 AlertListComponent
的 alerts
輸入訊號 (input signal)。
我們已成功在 Vue、Svelte 和 Angular 框架中建立了 alert list。