在第 6 天,我將說明 Vue 3、SvelteKit 與 Angular 如何在購物車元件中回應 HTML 事件。
在購物車元件中,我們會在表單送出事件時,將新項目加入項目清單;同時,當刪除按鈕被點擊時,該項目會從清單中移除。
<script> 區塊中新增 saveItem 方法,用以將新項目加入 items 的 ref 陣列。由於 items、newItem、newItemHighPriority 都是 Vue 的 ref,因此存取它們的值時需要使用 .value。新的項目包含 newItem 與 newItemHighPriority 的值,然後被加入陣列中。
<script setup lang="ts">
import { Icon } from "@iconify/vue";
import { ref } from 'vue';
const items = ref<Item[]>([])
const newItem = ref('')
const newItemHighPriority = ref(false)
const saveItem = () => {
  items.value.push({
    id: items.value.length + 1,
    label: newItem.value,
    highPriority: newItemHighPriority.value,
    purchased: false,
  })
  newItem.value = ''
  newItemHighPriority.value = false
}
</script>
<template>
  <form @submit.prevent="saveItem">
    <input v-model.trim="newItem" />
    <label>
      <input type="checkbox" v-model="newItemHighPriority" />
      <span>High Priority</span>
    </label>
    <button>Save Item</button>
  </form>
</template>
@submit.prevent 監聽表單送出事件並呼叫 saveItem,同時透過 preventDefault 避免頁面重新載入。v-model 雙向綁定 newItem 與輸入欄位、newItemHighPriority 與勾選框雙向綁定。
在 <script> 中定義 saveItem 方法,將新項目加入狀態陣列中。新項目包含 newItem 與 newItemHighPriority 的值。
<script lang="ts">
import Icon from '@iconify/svelte';
type Item = { id: number; label: string; purchased: boolean; higherPriority: boolean };
let newItem = $state('');
let newItemHigherPriority = $state(false);
let items = $state([] as Item[]);
function saveItem() {
  if (newItem) {
    items.push({
      id: items.length + 1,
      label: newItem,
      purchased: false,
      higherPriority: newItemHigherPriority
    });
    newItem = '';
    newItemHigherPriority = false;
  }
}
async function handleSubmit(event: SubmitEvent) {
  event.preventDefault();
  saveItem();
}
</script>
<form method="POST" onsubmit={handleSubmit}>
  <input id="newItem" name="newItem" type="text" bind:value={newItem} />
  <label>
    <input id="higherPriority" name="higherPriority" type="checkbox" bind:checked={newItemHigherPriority} />
    <span> Higher Priority</span>
  </label>
  <button class="btn btn-primary">Save Item</button>
</form>
Svelte 5 以 on 開頭的事件處理器名稱偵測並處理 DOM 事件。表單使用 onSubmit 執行 handleSubmit,該函式使用 preventDefault 避免頁面重新載入,並呼叫 saveItem 新增項目。
Angular 信號 (signal) 的變異處理方式與 Vue 3、Svelte 不同。當新項目加入陣列時,items signal 不會自動更新,必須更新陣列的引用 (array reference)。Angular 提供 update 方法使用回呼函式 (callback function) 從前一狀態建立新的陣列。
import { ChangeDetectionStrategy, Component, signal } from '@angular/core';
import { FormsModule } from '@angular/forms';
import { NgIcon, provideIcons } from '@ng-icons/core';
import { matRemove } from '@ng-icons/material-icons/baseline';
type Item = { id: number; label: string; purchased: boolean; highPriority: boolean };
@Component({
  selector: 'app-shopping-cart',
  imports: [FormsModule, NgIcon],
  viewProviders: [provideIcons({ matRemove })],
  template: `
    <form class="add-item-form" (ngSubmit)="saveItem()">
      <input type="text" name="newItem" [(ngModel)]="newItem" />
      <label>
        <input type="checkbox" [(ngModel)]="newItemHighPriority" name="newItemHighPriority" />
        <span> High Priority</span>
      </label>
      <button type="submit">Save Item</button>
    </form>
  `,
  changeDetection: ChangeDetectionStrategy.OnPush,
})
export class ShoppingCartComponent {
  items = signal<Item[]>([]);
  newItem = signal('');
  newItemHighPriority = signal(false);
  saveItem() {
    if (!this.newItem()) return;
    const id = this.items().length + 1;
    this.items.update(items => [
      ...items,
      {
        id,
        label: this.newItem(),
        purchased: false,
        highPriority: this.newItemHighPriority()
      }
    ]);
    this.newItem.set('');
    this.newItemHighPriority.set(false);
  }
}
Angular 的 ngSubmit 事件會處理表單送出,且會自動呼叫 preventDefault 避免頁面重載。表單使用 banana syntax (ngSubmit)="saveItem()" 綁定事件處理函式。
deleteItem 函式利用 filter 過濾 items.value 陣列以去除指定 ID 的項目,並將結果重新指定給 items.value。
<script setup lang="ts">
import { Icon } from "@iconify/vue";
import { ref } from 'vue';
const items = ref<Item[]>([])
const newItem = ref('')
const newItemHighPriority = ref(false)
const deleteItem = (id: number) => {
  items.value = items.value.filter(item => item.id !== id)
}
</script>
<template>
  <!-- 先前的表單 -->
  <ul>
    <div v-for="item in items" :key="item.id">
      <li>{{ item.id }} - {{ item.label }}</li>
      <button aria-label="Delete" @click="deleteItem(item.id)">
        <Icon icon="ic:baseline-remove" />
      </button>
    </div>
  </ul>
</template>
按鈕的 @click 事件會觸發 deleteItem,刪除指定項目。
同樣用 filter 過濾 items 陣列以刪除指定 ID 項目。
<script lang="ts">
import Icon from '@iconify/svelte';
type Item = { id: number; label: string; purchased: boolean; higherPriority: boolean };
let newItem = $state('');
let newItemHigherPriority = $state(false);
let items = $state([] as Item[]);
function deleteItem(id: number) {
  items = items.filter(item => item.id !== id);
}
</script>
<ul>
  {#each items as item (item.id)}
    <li>{item.id} - {item.label}</li>
    <button onclick={() => deleteItem(item.id)} aria-label="delete an item">
      <Icon icon="ic:baseline-remove" />
    </button>
  {/each}
</ul>
Svelte 5 使用 onClick 綁定點擊事件並執行刪除。
使用 update 方法與 Array.filter 創建新陣列,刪除指定 ID 項目。
type Item = { id: number; label: string; purchased: boolean; highPriority: boolean };
@Component({
  selector: 'app-shopping-cart',
  imports: [FormsModule, NgIcon],
  viewProviders: [provideIcons({ matRemove })],
  template: `
    <ul>
      @for (item of items(); track item.id) {
        <li [class]="itemClasses">
          {{ item.id }} - {{ item.label }}
        </li>
        <button aria-label="Delete" (click)="deleteItem(item.id)">
          <ng-icon name="matRemove"></ng-icon>
        </button>
      }
    </ul>
  `,
  changeDetection: ChangeDetectionStrategy.OnPush,
})
export class ShoppingCartComponent {
  items = signal<Item[]>([]);
  newItem = signal('');
  newItemHighPriority = signal(false);
  deleteItem(id: number) {
    this.items.update(items => items.filter(item => item.id !== id));
  }
}
Angular 使用 (click)="deleteItem(item.id)" 綁定點擊事件。
這樣我們成功讓購物車元件在送出表單時新增項目,並在點擊按鈕時刪除項目。