第 15 天- Add a Coffee Plan Form
在第15天,我擴展了 PlanPicker 組件,加入了一個 AddCoffeePlan 組件,用來向咖啡計劃列表中 (plan list) 添加新的咖啡計劃 (coffee plan)。接著,PlanPicker 組件有兩個子組件,分別是 AddCoffeePlan 和 CoffeePlan。
建立一個新的 components/AddCoffeePlan.vue 檔案。
<style scoped>
input {
padding: 0.5rem 0.75rem;
}
.add-plan-form {
display: flex;
align-items: center;
justify-content: space-between;
}
.add-plan-form input {
width: 70%;
border-radius: 3px;
box-shadow: 0 2px 4px 0 rgba(0, 0, 0, 0.1);
border: 1px solid #f1f5f8;
color: #606f7b;
padding: 0.5rem 0.75rem;
box-sizing: border-box;
font-size: 1rem;
letter-spacing: 0.5px;
margin: 0.5rem 0;
}
</style>
為 AddCoffeePlan 添加範圍限定的樣式。script 標籤具有 scoped 屬性。
<script setup lang="ts">
import { ref } from 'vue';
const emit = defineEmits<{
(e: 'newCoffeePlan', name: string): void
}>();
const newPlan = ref('');
function addPlan() {
const trimmedPlan = newPlan.value.trim();
if (!trimmedPlan) {
return;
}
emit('newCoffeePlan', trimmedPlan);
newPlan.value = '';
}
</script>
newPlan 這個 ref 用來儲存將要新增到咖啡計劃列表 (plan list) 中的新咖啡計劃 (coffee plan)。
defineEmits 定義了一個自訂的 newCoffeePlan 事件,用來將 newPlan 的值傳遞給 PlanPicker 組件。
addPlan 用來處理表單提交事件,觸發 newCoffeePlan 事件,並清空 newPlan。
<template>
<form class="add-plan-form" @submit.prevent="addPlan">
<input v-model.trim="newPlan" type="text" placeholder="Add a new plan" />
<button class="btn btn-primary" type="submit" :disabled="newPlan.length < 5">
Add Plan
</button>
</form>
</template>
模板使用 v-model.trim 將文本框綁定到 newPlan 這個 ref。
除非使用者在文本框輸入至少5個字元,否則 Add Plan 按鈕會被禁用。
當使用者提交表單時,addPlan 函數會驗證新的咖啡計劃 (coffee plan),並將該值發送給父組件。
建立一個新的 lib/add-coffee-plan.svelte 檔案。
為 AddCoffeePlan 添加範圍限定 (scoped) 的樣式 (CSS)。 樣式 (CSS) 預設即為範圍限定 (scoped)。
<style>
input {
padding: 0.5rem 0.75rem;
}
.add-plan-form {
display: flex;
align-items: center;
justify-content: space-between;
}
.add-plan-form input {
width: 70%;
border-radius: 3px;
box-shadow: 0 2px 4px 0 rgba(0, 0, 0, 0.1);
border: 1px solid #f1f5f8;
color: #606f7b;
padding: 0.5rem 0.75rem;
box-sizing: border-box;
font-size: 1rem;
letter-spacing: 0.5px;
margin: 0.5rem 0;
}
</style>
<script lang="ts">
interface Props {
addCoffeePlan: (plan: string) => void;
}
const { addCoffeePlan }: Props = $props();
let newPlan = $state('');
const addPlan = (e: SubmitEvent) => {
e.preventDefault();
const trimmedPlan = newPlan.trim();
if (!trimmedPlan) {
return;
}
addCoffeePlan(trimmedPlan);
newPlan = '';
}
</script>
newPlan 變數用來儲存將要新增到咖啡計劃列表 (plan list) 中的新咖啡計劃 (coffee plan)。
PlanPicker 組件將 addCoffeePlan 函數傳遞給 AddCoffeePlan 組件。 AddCoffeePlan 組件從 $props 解構出addCoffeePlan 函數,此函數用於向 PlanPicker 組件發出新的咖啡計劃 (coffee plan)。
addPlan 負責以下事項:
addCoffeePlan 函數通知父組件,<form class="add-plan-form" onsubmit={addPlan}>
<input type="text" placeholder="Add a new plan" bind:value={newPlan} />
<button type="submit" class="btn btn-primary" disabled={newPlan.length < 5}>
Add Plan
</button>
</form>
模板使用 bind:value 將文本框綁定到 newPlan 這個變數。
除非使用者在文本框輸入至少5個字元,否則 Add Plan 按鈕會被禁用。
表單註冊了 addPlan 用來監聽 onsubmit 事件。當使用者提交表單時,addPlan 會使用 addCoffeePlan 函數將新值傳遞給父組件。
Use the Angular CLI to create a new AddCoffeePlanComponent
ng g c AddCoffeePlan
該組件的 CSS 定義在獨立的 add-coffee-plan.component.css 檔案中。也支援內嵌的 CSS 樣式,具體使用哪種方式取決於程式設計的。
input {
padding: 0.5rem 0.75rem;
}
.add-plan-form {
display: flex;
align-items: center;
justify-content: space-between;
}
.add-plan-form input {
width: 70%;
border-radius: 3px;
box-shadow: 0 2px 4px 0 rgba(0, 0, 0, 0.1);
border: 1px solid #f1f5f8;
color: #606f7b;
padding: 0.5rem 0.75rem;
box-sizing: border-box;
font-size: 1rem;
letter-spacing: 0.5px;
margin: 0.5rem 0;
}
import { ChangeDetectionStrategy, Component, output, signal } from '@angular/core';
import { FormsModule } from '@angular/forms';
@Component({
selector: 'app-add-coffee-plan',
imports: [FormsModule],
template: `... inline template ...`,
styleUrl: './add-coffee-plan.component.css',
changeDetection: ChangeDetectionStrategy.OnPush
})
export class AddCoffeePlanComponent {
newPlan = signal('');
addCoffeePlan = output<string>();
addPlan() {
this.addCoffeePlan.emit(this.newPlan());
this.newPlan.set('');
}
}
該組件匯入了 FormsModule,以啟用 ngSubmit 並使信號 (signal) 可以雙向綁定 (two-way binding) 到 HTML 元素。
newPlan 信號用來儲存將要新增到咖啡計劃列表 (plan list) 中的新咖啡計劃 (coffee plan)。
addCoffeePlan 是一個輸出函數,用於將新值發送給 PlanPicker 組件。
addPlan 方法處理 ngSubmit 事件,隱式阻止頁面重新加載,觸發addCoffeePlan 自訂事件,並清空 newPlan 信號。該組件匯入了 FormsModule,以啟用 ngSubmit 並使信號 (signal) 可以雙向綁定到 HTML 元素。
template: `
<form class="add-plan-form" (ngSubmit)="addPlan()">
<input name="newPlan" type="text" placeholder="Add a new plan" [(ngModel)]="newPlan" />
<button class="btn btn-primary" type="submit" [disabled]="newPlan().length < 5">
Add Plan
</button>
</form>
`
[(ngModel)] 雙向綁定 newPlan 信號到輸入框。當使用者在文字框輸入新的咖啡計劃 (coffee plan) 時,newPlan 會接收新的值。
Add Plan 按鈕使用方括號語法 [expression] 將輸入綁定到屬性。當使用者輸入少於 5 個字元到文字框時,[disabled] 為 true。
ngSubmit 是一個自訂的 Angular 事件,當表單提交時觸發,事件處理函式 (event handler) 為 addPlan。addPlan 呼叫 addCoffeePlan 輸出事件 (event emitter),將新值傳遞給父組件。
將 AddCoffeePlan 匯入到 PlanPicker
<script setup lang="ts">
import AddCoffeePlan from './AddCoffeePlan.vue'
</script>
在模板中使用 AddCoffeePlan 組件
<template>
<div class="plans">
<AddCoffeePlan @newCoffeePlan="(plan) => plans.push(plan)" />
</div>
</template>
@newCoffeePlan 自訂事件會呼叫一個內聯函式,以將新的計劃 (coffee plan) 加入咖啡計劃列表 (plan list) 中。
Import AddCoffeePlan to PlanPicker
<script lang="ts">
import CoffeePlan from './coffee-plan.svelte';
</script>
Use the AddCoffeePlan in the template
<div class="plans">
<AddCoffeePlan addCoffeePlan={(plan) => plans.push(plan)} />
</div>
addCoffeePlan 自訂事件也會呼叫一個內聯函式,將新的計劃 (coffee plan) 加入到咖啡計劃列表 (plan list) 中。
請將 AddCoffeePlanComponent 匯入到 PlanPickerComponent 中
import { ChangeDetectionStrategy, Component, signal } from '@angular/core';
import { AddCoffeePlanComponent } from '../add-coffee-plan/add-coffee-plan.component';
import { CoffeePlanComponent } from '../coffee-plan/coffee-plan.component';
@Component({
selector: 'app-plan-picker',
imports: [CoffeePlanComponent, AddCoffeePlanComponent],
template: `... inline template ...`,
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class PlanPickerComponent {
plans = signal(['The Single', 'The Curious', 'The Addict', 'The Hacker']);
addPlan(name: string) {
this.plans.update((plans) => [...plans, name]);
}
}
將 AddCoffeePlanComponent 組件加入到 imports 陣列中。
addPlan 方法使用 Signal API 的 update 方法,將新的咖啡計劃加入到 plans 信號中。
<div class="plans">
<app-add-coffee-plan (addCoffeePlan)="addPlan($event)" />
</div>
addCoffeePlan 自訂事件使用特殊的 ``$event來傳遞新的咖啡計劃給addPlan` 方法,以便將新的咖啡名稱加入。
我們已成功建立了一個 AddCoffeePlan 組件,該組件可以將新的咖啡計劃新增到 PlanPicker 組件中的咖啡計劃列表。