在第 25 天,我擴充了 Alert Bar 元件來顯示已關閉 Alert 的按鈕。接著,使用者可以選擇重新開啟所有已關閉的 Alert,或是特定類型的 Alert。
<script setup lang="ts">
import { ref } from 'vue' 
const closedNotifications = defineModel<string[]>('closedNotifications', { default: [] })
</script>
在 AlertBar 元件中,使用 defineModel 建立一個類型為 string[] 的 closedNotifications ref。
closedNotifications 的預設值為空清單。
針對此 ref 中的每種類型,會顯示一個按鈕,可用來開啟該警示。
當此 ref 不為空時,亦會顯示 Open all alerts 按鈕。
<script lang="ts">
    type Props = {
        ... other prop ...
        closedNotifications: string[];
    }
    let { 
        ... other bindables ...
        closedNotifications = $bindable(),
    }: Props = $props();
</script>
將 closedNotifications 回呼屬性 (callback prop) 新增到 Props 類型中。
接著,從 $props() 巨集中解構 closedNotifications,並使用 $bindable 巨集建立雙向綁定。
import { ChangeDetectionStrategy, Component, input, model } from '@angular/core';
import { FormsModule } from '@angular/forms';
@Component({
  selector: 'app-alert-bar',
  imports: [FormsModule],
  template: `...inline template...`,
  changeDetection: ChangeDetectionStrategy.OnPush,
})
export class AlertBarComponent {
  ... other models ...
  
  closedNotifications = model<string[]>([]);
}
在 AlertBarComponent 中,建立一個類型為 string[] 的 closedNotifications 模型信號。closedNotifications 的預設值為空字串陣列。
在 AlertBar 元件中,有兩種用戶事件會操作 closedNotifications ref。當使用者點擊 "Open all alerts" 按鈕時,closedNotifications ref 應該設為空陣列。
當使用者點擊按鈕重新開啟特定類型的警示時,該類型必須從 closedNotifications ref 中移除。
<script setup lang="ts">
const closedNotifications = defineModel<string[]>('closedNotifications', { default: [] })
function removeNotification(type: string) {
  closedNotifications.value = closedNotifications.value.filter((t) => t !== type)
}
function clearAllNotifications() {
  closedNotifications.value = []
}
function hasClosedNotifications() {
  return closedNotifications.value.length > 0
}
</script>
removeNotification 函數會從 closedNotifications ref 中移除特定的 type。clearAllNotifications 函數會從 closedNotifications ref 中移除所有類型。
hasClosedNotifications 函數用來判斷 closedNotifications ref 是否為空。
<template>
  <div>
    <p class="mb-[0.75rem]">
      <button v-for="type in closedNotifications"
        :key="type" @click="removeNotification(type)"
      >
        <OpenIcon />{{ capitalize(type) }}
      </button>    
      <button
        v-if="hasClosedNotifications()"
        @click="clearAllNotifications">
        Open all alerts
      </button>
    </p>
  </div>
</template>
v-for 指令會遍歷 closedNotifications ref,為每個已關閉的通知顯示按鈕。當發生 click 事件時,removeNotification 函數會從該 ref 移除該 type。
v-if 指令會在 closedNotifications ref 非空陣列時顯示 Open all alerts 按鈕。當發生 click 事件時,clearAllNotifications 函數會從 ref 中移除所有類型。
In the AlertBar component.
<script lang="ts">
    type Props = {
        ... omitted due to brevity ...
        closedNotifications: string[];
    }
    let { 
        ... omitted due to brevity ...
        closedNotifications = $bindable(),
    }: Props = $props();
    
    function removeNotification(type: string) {
     closedNotifications = closedNotifications.filter((t) => t !== type)
    }
    function clearAllNotifications() {
        closedNotifications = []
    }
    function hasClosedNotifications() {
        return closedNotifications.length > 0
    }
</script>
removeNotification 函數會從 closedNotifications ref 中移除特定的 type。clearAllNotifications 函數會從 closedNotifications ref 中移除所有類型。
hasClosedNotifications 函數用來判斷 closedNotifications ref 是否為空。
<div>
    <p class="mb-[0.75rem]">
        {#each closedNotifications as type (type)}
            <button
                class={getBtnClass(type) + ' mr-[0.5rem] btn'}
                onclick={() => removeNotification(type)}
            >
                <OpenIcon />{ capitalize(type) }
            </button>    
        {/each}
        {#if hasClosedNotifications()}
            <button
                class="btn btn-primary" 
                onclick={clearAllNotifications}>
                Open all alerts
            </button>
        {/if}
      </p>
</div>
在 Svelte 中,事件是以 'on' 開頭的屬性,因此點擊事件的屬性名稱為 onclick。
onclick={() => removeNotification(type)} 會執行 removeNotification 函數。
onclick={clearAllNotifications} 會執行 clearAllNotifications 函數,這是一個簡寫,因為該函數不需要任何參數。
#each 會遍歷 closedNotifications 回呼屬性,為每個已關閉的通知顯示按鈕。當發生 click 事件時,removeNotification 函數會被觸發。
#if 在 closedNotifications 回呼屬性非空陣列時顯示 Open all alerts 按鈕。當發生 click 事件時,clearAllNotifications 函數會被觸發。
In the AlertBarComponent
import { ChangeDetectionStrategy, Component, input, model } from '@angular/core';
import { FormsModule } from '@angular/forms';
import { capitalize } from '../capitalize';
import { OpenIconComponent } from '../icons/icon.component';
@Component({
  selector: 'app-alert-bar',
  imports: [FormsModule, OpenIconComponent],
  template: `...inline template...`,
  changeDetection: ChangeDetectionStrategy.OnPush,
})
export class AlertBarComponent {
  closedNotifications = model<string[]>([]);
  removeNotification(type: string) {
    this.closedNotifications.update((prev) => prev.filter((t) => t !== type));
  }
    
  clearAllNotifications() {
    this.closedNotifications.set([])
  }
    
  hasCloseButtonChanged() {
    return this.closedNotifications().length > 0;
  }
}
removeNotification 方法呼叫 closedNotifications 模型信號的 update 方法,以移除特定的 type。
clearAllNotifications 方法呼叫 closedNotifications 模型信號的 set 方法,以移除所有類型。
hasClosedNotifications 方法用來判斷 closedNotifications 模型信號是否為空。
<div>
    <p class="mb-[0.75rem]">
      @for (type of closedNotifications(); track type) {
        <button (click)="removeNotification(type)">
          <app-open-icon />{{ capitalize(type) }}
        </button>
      }
      @if (hasClosedNotifications()) { 
        <button (click)="closedNotifications.set([])">
          Open all alerts
        </button>
      }
    </p>
</div>
在 Angular 中,事件透過發射(emit)產生,並使用香蕉語法 (event) 將事件發射給父元件。
(click)="removeNotification(type)" 會執行 removeNotification 方法。
(click)="clearAllNotifications" 會執行 clearAllNotifications 方法。
@each 會遍歷 closedNotifications 模型信號,為每個已關閉的通知顯示按鈕。 當發生 click 事件時,會觸發 removeNotification 方法。
@if 在 closedNotifications 模型信號非空陣列時顯示 Open all alerts 按鈕。 當發生 click 事件時,會觸發 clearAllNotifications 方法。
AlertList 元件的雙向綁定<script setup lang="ts">
import type { AlertType } from '@/types/alert-type';
import { computed, ref } from 'vue';
import Alert from './Alert.vue';
import AlertBar from './AlertBar.vue';
const closedNotifications = ref<string[]>([])
const alerts = computed(() => props.alerts.filter((alert) => 
  !closedNotifications.value.includes(alert.type))
)
function handleClosed(type: string) {
  closedNotifications.value.push(type)
}
</script>
closedNotifications ref 綁定到 AlertBar 元件的 closedNotifications ref。
alerts 計算型 ref 是一個已開啟警示的陣列。當 AlertBar 元件從 closedNotification 中移除任何項目時,alerts ref 會重新計算要顯示的警示。
handleClosed 函數會將已關閉警示的類型新增到 closedNotifications ref 中。此函數執行後,alerts ref 會重新計算以取得新值。
<template>
  <AlertBar
    v-model:closedNotifications="closedNotifications"
  />
</template>
v-model 指令將 closedNotifications ref 綁定到 AlertBar 元件的 closedNotifications ref。 當 AlertBar 元件中的 closedNotifications 更新時,AlertList 元件中的 closedNotifications 也會更新。v-for 指令會遍歷 alerts 計算型 ref,顯示已開啟的警示。
<template>
  <Alert v-for="{ type, message } in alerts"
    :key="type"
    :type="type"
    @closed="handleClosed">
    {{  message }}
  </Alert>
</template>
v-for 指令會遍歷 alerts 計算型 ref,顯示已開啟的警示。
<script lang="ts">
	import AlertBar from './alert-bar.svelte';
    import Alert from './alert.svelte';
    let closedNotifications = $state<string[]>([]);
    let filteredNotifications = $derived.by(() => 
        alerts.filter(alert => !closedNotifications.includes(alert.type))
    );
    function notifyClosed(type: string) {
        console.log(`Alert of type ${type} closed`);
        closedNotifications.push(type);
    }
</script>
closedNotifications 變數綁定到 AlertBar 元件的 closedNotifications 變數。filteredNotifications 衍生變數是一個已開啟警示的陣列。當 AlertBar 元件從 closedNotifications 中移除任何項目時,filteredNotifications 衍生變數會重新計算要顯示的警示。
handleClosed 函數會將已關閉警示的類型新增到 closedNotifications 變數中。該函數執行後,filteredNotifications 衍生變數會重新計算以取得新值。
<AlertBar 
    bind:closedNotifications={closedNotifications}
/>
{#each filteredNotifications as alert (alert.type) } 
    <Alert {notifyClosed} />
{/each}
bind:closedNotifications={closedNotifications} 表示 AlertList 元件會監聽 AlertBar 元件的 closedNotifications prop。
#each 會遍歷 filteredNotifications 衍生變數,顯示已開啟的警示。
import { ChangeDetectionStrategy, Component, computed, input, signal } from '@angular/core';
import { AlertType } from '../alert.type';
import { AlertComponent } from '../alert/alert.component';
import { AlertBarComponent } from '../alert-bar/alert-bar.component';
@Component({
  selector: 'app-alert-list',
  imports: [AlertComponent, AlertBarComponent],
  template: `...inline template...`,
  changeDetection: ChangeDetectionStrategy.OnPush,
})
export class AlertListComponent {
  closedNotifications = signal<string[]>([]);
  filteredAlerts = computed(() => 
    this.alerts().filter(alert => !this.closedNotifications().includes(alert.type))
  ); 
  handleCloseNotification(type: string) {
    this.closedNotifications.update((prev) => ([...prev, type ]));
  }
}
closedNotifications 信號綁定到 AlertBar 元件的 closedNotifications 模型信號。
filteredAlerts 計算型信號是一個已開啟警示的陣列。當 AlertBar 元件從 closedNotifications 中移除任何項目時,filteredAlerts 計算型信號會重新計算要顯示的警示。
handleCloseNotification 方法會將已關閉警示的類型新增到 closedNotifications 信號中。該方法執行後,filteredAlerts 會重新計算以取得新值。
<app-alert-bar 
    [(closedNotifications)]="closedNotifications"
/>
@for (alert of filteredAlerts(); track alert.type) {
  <app-alert [type]="alert.type" 
    [alertConfig]="alertConfig()"(closeNotification)="handleCloseNotification($event)">
    {{ alert.message }}
  </app-alert>
}
[(closedNotifications)]="closedNotifications"
/> 表示 AlertList 元件會監聽 AlertBar 元件的 closedNotifications 模型信號。
@for 會遍歷 filteredAlerts 計算型信號,顯示已開啟的警示。
現在,使用者可以點擊 AlertBar 元件中的按鈕來開啟已關閉的警示。