在第26天,我檢視了 AlertBar
元件的程式碼,並發現兩項改進可以讓它更乾淨。
該元件包含一個靜態標籤 (static label) 和一個雙向綁定 (Two-way binding) 到 AlertList
元件的選擇元素 (select element),這部分可以抽取成一個 AlertDropdown
元件。
AlertList
和 AlertBar
元件都有管理closedNotifications參考狀態的邏輯,這些邏輯和參考可以封裝到一個狀態管理解決方案中。
Framework | State Management |
---|---|
Vue | Composable |
Angular | Service |
Svelte | $state in Store |
<script setup lang="ts">
type Props = {
label: string
items: { value: string, text: string }[]
}
const { label, items } = defineProps<Props>()
const selectedValue = defineModel<string>('selectedValue')
</script>
<template>
<span>{{ label }} </span>
<select class="select select-info mr-[0.5rem]" v-model="selectedValue">
<option v-for="{value, text} in items" :key="value" :value="value">
{{ text }}
</option>
</select>
</template>
Props
類型包含一個 label
字串和一個 items
陣列。使用defineModel
來建立一個與父元件綁定的 selectedValue
參考 (ref)。
<script lang="ts">
type Props = {
label: string;
items: { text: string, value: string }[];
selectedValue: string;
}
let { label, items, selectedValue = $bindable() }: Props = $props();
</script>
<span>{ label } </span>
<select class="select select-info mr-[0.5rem]" bind:value={selectedValue}>
{#each items as item (item.value) }
<option value={item.value}>
{ item.text }
</option>
{/each}
</select>
Props
類型包含一個 label
字串、一個 selectedValue
字串以及一個 items
陣列。使用 $bindable
巨集將 selectedValue
綁定到父元件。
import { ChangeDetectionStrategy, Component, input, model } from '@angular/core';
import { FormsModule } from '@angular/forms';
@Component({
selector: 'app-alert-dropdown',
imports: [FormsModule],
template: `
<span>{{ label() }} </span>
<select class="select select-info mr-[0.5rem]" [(ngModel)]="selectedValue">
@for (style of items(); track style.value) {
<option [ngValue]="style.value">
{{ style.text }}
</option>
}
`,
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class AlertDropdownComponent {
label = input.required<string>();
items = input.required<{ text: string, value: string }[]>();
selectedValue = model.required<string>();
}
label
和 items
是必填的訊號。selectedValue
是一個 model 訊號,用於將值與父元件綁定。
重構 AlertBar
元件以使用 AlertDropdown
,以減少8至10行程式碼。
使用 AlertDropdown
元件取代HTML的選擇元素和靜態標籤。
<template>
<p class="mb-[0.75rem]">
<span>Has close button? </span>
<input type="checkbox" class="mr-[0.5rem]" v-model="hasCloseButton" />
<AlertDropdown :label="config.styleLabel" :items="config.styles" v-model:selectedValue="style" />
<AlertDropdown :label="config.directionLabel" :items="config.directions" v-model:selectedValue="direction" />
</p>
</template>
對於 style 下拉選單,label
屬性接收config.styleLabel
,items
屬性接收config.styles
陣列。此外,v-model
更新為v-model:selectedValue
以綁定到 style
ref。
對於方 direction 下拉選單,label
屬性接收config.directionLabel
,items
屬性接收config.directions
陣列。此外,v-model
更新為v-model:selectedValue
以綁定到 direction
ref。
<p class="mb-[0.75rem]">
<span>Has close button?</span>
<input type="checkbox" class="mr-[0.5rem]" bind:checked={hasCloseButton} />
<AlertDropdown label={configs.styleLabel} items={configs.styles} bind:selectedValue={style} />
<AlertDropdown label={configs.directionLabel} items={configs.directions} bind:selectedValue={direction} />
</p>
對於 style 下拉選單,label
屬性接收 config.styleLabel
,items
屬性接收 config.styles
陣列。此外,bind:value
更新為 bind:selectedValue
以綁定到style
rune。
對於 direction 下拉選單,label
屬性接收config.directionLabel
,items
屬性接收 config.directions
陣列。此外,bind:value
更新為 bind:selectedValue
以綁定到direction
rune。
<p class="mb-[0.75rem]">
<span>Has close button? </span>
<input type="checkbox" class="mr-[0.5rem]" [(ngModel)]="hasCloseButton" />
<app-alert-dropdown [label]="c.styleLabel" [items]="c.styles" [(selectedValue)]="style" />
<app-alert-dropdown [label]="c.directionLabel" [items]="c.directions" [(selectedValue)]="direction" />
</p>
對於 style 下拉選單,label
輸入接收 c.styleLabel
,items
輸入接收 c.styles
陣列。此外,[(ngModel)]
更新為[(selectedValue)]
以綁定到 style
模型。
對於 direction 下拉選單,label
輸入接收 c.directionLabel
,items
輸入接收 c.directions
陣列。此外,[(ngModel)]
更新為 [(selectedValue)]
以綁定到 direction
模型。
AlertList
和 AlertBar
中有一些小函式用於新增、移除、清除與取得已關閉的通知。因此,我想將這些函式抽取到一個新檔案中,並在元件中調用這些函式。
closedNotification
被轉換為可共享的資料,供 AlertList
和 AlertBar
共同存取。
在composables
目錄下建立一個useNotifications
可組合函式。
import { readonly, ref } from "vue"
const closedNotifications = ref<string[]>([])
export function useNotifications() {
function remove(type: string) {
closedNotifications.value = closedNotifications.value.filter((t) => t !== type)
}
function removeAll() {
closedNotifications.value = []
}
function isNonEmpty() {
return closedNotifications.value.length > 0
}
function add(type: string) {
closedNotifications.value.push(type)
}
return {
closedNotifications: readonly(closedNotifications),
remove,
clearAll,
isNonEmpty,
add
}
}
closedNotifications
是元件之間共享的 ref。
useNotifications
composable 包含用於 CRUD 的函式。
Name | Purpose |
---|---|
remove | remove a notification's type |
removeAll | remove all notification types |
isNonEmpty | check the array has a closed notification |
add | append the type of the closed notification |
closedNotification
是唯讀的,因此可以防止程式碼中的意外變異。
在stores
目錄下建立一個新檔案。Svelte 4使用store來維護狀態,但在Svelte 5中runes能夠執行簡單的狀態管理。
const state = $state({
closedNotifications: [] as string[]
});
const closedNotifications = $derived(() => state.closedNotifications);
export function getClosedNotification() {
return closedNotifications;
}
export function removeNotification(type: string) {
state.closedNotifications = state.closedNotifications.filter((t) => t !== type);
}
export function removeAllNotifications() {
state.closedNotifications = [];
}
export function isNotEmpty() {
return state.closedNotifications.length > 0
}
export function addNotification(type: string) {
state.closedNotifications.push(type);
}
Svelte 編譯器在終端機中記錄 "Cannot export state from a module if it is reassigned" 錯誤,因此 state
是一個包含closedNotifications
陣列的物件 rune。否則,我無法在removeNotification
函式中修改 closedNotifications
。
Name | Purpose |
---|---|
removeNotification | remove a notification's type |
removeAllNotifications | remove all notification types |
isNonEmpty | check the array has a closed notification |
addNotification | append the type of the closed notification |
getClosedNotification | return the readonly closedNotification rune |
closedNotifications
是一個唯讀的 rune,儲存一個回傳closedNotifications
陣列的函式。
const a = getClosedNotification()
將該 rune 指派給 a
。呼叫 a()
會回傳響應 (reactive) 的closedNotifications
,用於模板渲染。
建立一個 NotificationsService
,並稍後將該服務注入到元件中。
import { Injectable, signal } from '@angular/core';
@Injectable({
providedIn: 'root'
})
export class NotificationsService {
#closedNotifications = signal<string[]>([]);
closedNotifications = this.#closedNotifications.asReadonly();
remove(type: string) {
this.#closedNotifications.update((prev) => prev.filter((t) => t !== type));
}
removeAll() {
this.#closedNotifications.set([])
}
isNonEmpty() {
return this.#closedNotifications().length > 0;
}
add(type: string) {
this.#closedNotifications.update((prev) => ([...prev, type ]));
}
}
#closedNotifications
是一個儲存字串陣列的訊號 (signal)。Signal
類別的 asReadonly
方法會建立一個唯讀訊號 (readonly signal)。因此,closedNotifications
是一個用於內嵌模板渲染的唯讀訊號。
Name | Purpose |
---|---|
remove | remove a notification's type |
removeAll | remove all notification types |
isNonEmpty | check the array has a closed notification |
add | append the type of the closed notification |
新的邏輯已應用於 AlertBar
和 AlertList
元件。
在 AlertBar
元件中,
import { useNotifications } from '@/composables/useNotification'
const { closedNotifications, removeAll, isNonEmpty, remove } = useNotifications()
匯入 useNotifications
並解構函式。接著,刪除先前宣告的closedNotification
ref。然後,更新按鈕以呼叫相應函式。
<button v-for="type in closedNotifications"
:key="type"
@click="remove(type)"
>
<OpenIcon />{{ capitalize(type) }}
</button>
將removeNotification
替換為remove
,且v-for
指令不變。
<button
v-if="isNonEmpty()"
class="btn btn-primary"
@click="removeAll">
Open all alerts
</button>
將 clearAllNotifications
替換為 removeAll
,並且 v-if
指令會檢查 isNonEmpty
條件。
在 AlertBar
元件中,
import {
getClosedNotification,
removeNotification,
removeAllNotifications,
isNotEmpty
} from './stores/notification.svelte';
type Props = {
... omitted for brevity ...
style: string;
direction: string;
}
let {
... omitted for brevity ...
style = $bindable(),
direction = $bindable(),
}: Props = $props();
const closedNotifications = getClosedNotification();
從 ./stores/notification.svelte
匯入函式。然後,刪除先前宣告的closedNotification
參考。更新按鈕以呼叫相應函式。
從 Props
類型和 $props()
中刪除 closedNotifications
。
呼叫 getClosedNotification()
並將 rune 指派給 closedNotifications
。
接著,更新按鈕的函式呼叫。
{#each closedNotifications() as type (type)}
<button
class={getBtnClass(type) + ' mr-[0.5rem] btn'}
onclick={() => removeNotification(type)}
>
<OpenIcon />{ capitalize(type) }
</button>
{/each}
#each
迴圈迭代 closedNotifications()
,因為 closedNotifications
rune的結果是一個響應的陣列。
{#if isNotEmpty()}
<button
class="btn btn-primary"
onclick={removeAllNotifications}>
Open all alerts
</button>
{/if}
將 clearAllNotifications
替換為 removeAllNotifications
,並且 if
control flow 判斷 isNonEmpty
條件。
AlertBarComponent
注入 NotificationService
並存取其方法。
@Component({
selector: 'app-alert-bar',
imports: [FormsModule, OpenIconComponent, AlertDropdownComponent],
template: `...inline template...`,
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class AlertBarComponent {
notificationService = inject(NotificationsService);
... omit other models ...
closedNotifications = this.notificationService.closedNotifications;
capitalize = capitalize;
remove(type: string) {
this.notificationService.remove(type);
}
removeAll() {
this.notificationService.removeAll();
}
isNonEmpty() {
return this.notificationService.isNonEmpty();
}
}
重新命名元件內的方法,並在方法內呼叫服務的方法。 closedNotification
為服務中的唯讀訊號 (readonly signal)。
@for (type of closedNotifications(); track type) {
<button (click)="remove(type)">
<app-open-icon />{{ capitalize(type) }}
</button>
}
點擊事件在重新命名後呼叫remove
。
@if (isNonEmpty()) {
<button (click)="removeAll()">
Open all alerts
</button>
}
@if
control flow 判斷 isNonEmpty
條件。點擊事件在重新命名後呼叫removeAll
。
在 AlertList
元件中,
import { useNotifications } from '@/composables/useNotification'
const { closedNotifications, add } = useNotifications()
const alerts = computed(() => props.alerts.filter((alert) =>
!closedNotifications.value.includes(alert.type))
)
匯入 userNotifications
composable 並解構其函式。
將 closedNotification
ref 替換為 composable 的 ref。
alerts
計算型參考 (computed ref) 使用新的 closedNotification
ref,衍生當前開啟的通知。
<Alert v-for="{ type, message } in alerts"
:key="type"
:type="type"
:alertConfig="alertConfig"
@closed="add">
{{ message }}
</Alert>
當 Alert
元件發出自訂 closed 事件時,add
函式會將 type
新增到響應式陣列中。模板不會渲染已關閉的通知。
import { addNotification, getClosedNotification } from './stores/notification.svelte';
const closedNotifications = getClosedNotification();
let filteredNotifications = $derived.by(() =>
alerts.filter(alert => !closedNotifications().includes(alert.type))
);
getClosedNotification
的結果指派給 closedNotifications
。
filteredNotification
rune 呼叫 closedNotification()
取得可響應陣列,並在回呼函式 (callback function) 中執行過濾,最終結果為目前開啟的通知。
{#each filteredNotifications as alert (alert.type) }
<Alert {alert} {alertMessage} notifyClosed={() => addNotification(alert.type)} {alertConfig} />
{/each}
notifyClosed
屬性呼叫 addNotification
函式,將 type
附加到可反應陣列中。模板不會渲染已關閉的通知。
在 AlertListComponent
中注入 NotificationService
。
@Component({
selector: 'app-alert-list',
imports: [AlertComponent, AlertBarComponent],
template: `...inline template...`,
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class AlertListComponent {
notificationService = inject(NotificationsService);
... omitted for brevity ...
filteredAlerts = computed(() =>
this.alerts().filter(alert =>
!this.notificationService.closedNotifications().includes(alert.type))
);
add(type: string) {
this.notificationService.add(type);
}
}
filteredAlerts
計算型訊號 (computed signal) 使用服務的closedNotifications
來衍生目前開啟的通知。
add
方法會將 type
加入服務的響應式陣列中。
@for (alert of filteredAlerts(); track alert.type) {
<app-alert [type]="alert.type"
[alertConfig]="alertConfig()"
(closeNotification)="add($event)">
{{ alert.message }}
</app-alert>
}
當自訂的closeNotification
事件發出 type
時,add
方法會將其加入響應式陣列。模板不會渲染已關閉的通知。