iT邦幫忙

2025 iThome 鐵人賽

DAY 4
1
Vue.js

需求至上的 Vue 魔法之旅系列 第 4

Day 3 : 輸入框的需求:v-model 背後的雙向資料流思維

  • 分享至 

  • xImage
  •  

前言|延續 Day 1 的思維

到目前為止,我們已經完成:

  • Day 1:用「流程控制」把點餐步驟拆開(飲料 → 甜度 → 冰量),並以 v-if、:disabled 負責 UX 節奏。

  • Day 2:用 事件驅動(@click)把使用者的「送出」行為寫進系統狀態(orders),完成第一個資料落袋的流程。

但現實世界從不會就此滿足 /images/emoticon/emoticon02.gif。使用者/老闆下一句常常是:

我要知道這筆是誰點的,還想加備註(少冰、3 點拿)!」

這就引出今天 Day 3 的主角:v-model。我們將把「輸入框 ↔ 狀態」打通,讓姓名、備註和點餐內容一起被儲存。

明天教完bind之後,會補充為什麼我不單單用@change就好了?

今天的學習路徑圖一樣奉上

https://ithelp.ithome.com.tw/upload/images/20250918/201210524AGd0EL6CA.png

一、需求分析

1. User Story

如同前言提到的,因為我們是需要統計點餐人的各品項

好不容易統計完畢了~

可是卻沒有紀錄是誰點的? 如果要幾點拿? 在哪裡? 我們負責統計的秘書也很難整理資料/images/emoticon/emoticon02.gif

這時候就有新的user story出現惹!!

角色 故事 驗收條件
點餐者 作為使用者,我想輸入「姓名」與「備註」,並在送出時與飲料選擇一起被保存,讓統計者能分辨誰點了什麼。 送出後:1. 訂單被加入 orders,含 { name, note, drink, sweetness, ice };2. 表單清空;3. 清單顯示姓名與內容。

2.需求表格

建立在昨天的基礎上

我們勢必多兩個欄位點餐者姓名、備註 但是姓名是必填欄位

接者我們就把資料面跟行為都補充吧!!

條列出來就會顯示以下的表格可以參照

面向 需求描述
功能 在 Day 2 的基礎上,新增輸入欄位:點餐者姓名(必填)、備註(選填),送單時一併寫入。
使用者 點餐者(仍以單一角色為主)。
資料 每筆訂單包含 { name, note?, drink, sweetness, ice }
狀態 新增 namenoteorders 陣列元素結構擴充。
行為 送出前需驗證:name 與三個選項皆完成;成功後 push、清空含 name/note
畫面 顯示姓名輸入框(必要時提示必填)、備註輸入框、與更新後的訂單清單(包含姓名)。

接者我們再把時序圖畫出來:

這是以下的功能時序圖,基於前兩天的功能

我們的程式越來越越龐大了

但是不用擔心~ 我們可是有vue魔法呢!

下面HL的就是我們今天要學習的重點,也是新添加的功能

https://ithelp.ithome.com.tw/upload/images/20250918/20121052KlUHXkBOow.png

二. 對應的 Vue 技術

接者一樣我們需要有輸入表單

這時候可以去找官方document或是google

也是十之八九會告訴你用v-model搞定~

這時候你可能會問我不能用onchange或是bind處理嗎?

沒關係先別急~人別這麼猴急我們後續再說

需求 Vue 技術與重點
姓名、備註輸入 v-model:input/textarea 與狀態雙向同步(可搭配 .trim.lazy 修飾符)
表單驗證 computed 或直接在 addOrder 內檢查 name && drink && sweetness && ice
寫入清單 orders.value.push({...});元素結構擴充含 namenote
清空狀態 name/note/drink/sweetness/ice 設為空字串
顯示清單 v-for 迭代 orders,多顯示 namenote

今天要介紹的新朋友就是v-model拉

這邊我覺得官方寫得很簡單了

就直接拿官方的範例用瞜

<script setup>
import { ref } from 'vue'

const message = ref('')
</script>

<template>
  <p>Message is: {{ message }}</p>
	<input v-model="message" placeholder="edit me" />
</template>

參考vue官方 v-model demo

📃 tips這邊有兩個v-model的小技巧分享給大家~蠻好用的

2.1 v-model.trim

今天你只要在v-model後面新增.trim的修飾詞

即可自動幫你處理莫名其妙的空格問題

例如有些使用者可能是輸入 :

空格空格ALICE => 這樣就會造成輸入的值不太對存到後面server很麻煩

這時候有些小修飾詞加上去可以避免掉要自己處理字串空格的麻煩

但要注意的是這邊是處理字首前方跟最後方後面的空格喔!! 中間的空格不算

使用者輸入過程中,變更值時就會修剪空白(Vue 3 的行為是每次 input 事件都處理)。

<template>
  <input v-model.trim="name" placeholder="輸入名字" />
  <p>儲存的值:'{{ name }}'</p>
</template>

<script setup>
import { ref } from 'vue'
const name = ref('')
</script>

2.2 v-model.lazy

有時候使用者在思考,或是打字過程但是還不想這麼快顯示未打完的東西

其實這個方式也可以做到避免未輸入完就去call api執行避免效能浪費

  • 用途:
    延後更新資料,只有在:

使用者 離開輸入框 (blur)

  • 或按 Enter
    時才更新綁定的值。

這個有v-model + debounce的用法~ 不過我這邊還不想講先埋耿

後續我串接後端的時候會詳細說明使用情境喔!!/images/emoticon/emoticon25.gif

當然你也可以同時使用 v-model.lazy.trim="message" 達成去掉空格跟延遲輸入的功能

三、程式實作

3.1 流程圖

有了使用情境根具備的vue技巧後

我們就可以開始操刀了

這時候可以先把流程圖畫出來~

方便自己寫code梳理邏輯

https://ithelp.ithome.com.tw/upload/images/20250918/20121052Lhj9WM2HMi.png

其實今天沒有很難

只有新增兩個data跟綁定v-model輸入框框而已

3.2 UI呈現

我們期待的UI大概會這樣長

  • 已經有人RONI點餐

https://ithelp.ithome.com.tw/upload/images/20250918/20121052P9b5AvIn4s.png

  • 自己(Alice)點餐但是尚未送出會長這樣

https://ithelp.ithome.com.tw/upload/images/20250918/20121052KlDYFzXc3S.png

3.3 實作程式碼

直接go完成我們的程式碼

我們只要新增兩個欄位是姓名跟備註即可

新增 v-if="!name"判斷是否填姓名

<!-- DrinkOrdersDay3.vue -->
<template>
  <h2>飲料點單(Day 3:加入姓名/備註 + 送單)</h2>

  <!-- Day 3 新增:點餐者姓名(必填)與備註(選填) -->
  <div>
    <label>
      姓名(必填)
      <input type="text" v-model.trim="name" placeholder="請輸入你的名字" />
    </label>
    <p v-if="!name">⚠️ 尚未填寫姓名</p>
  </div>

  <div>
    <label>
      備註(選填)
      <textarea v-model.trim="note" placeholder="例如:三點拿、少冰"></textarea>
    </label>
  </div>

  <!-- 步驟 1:飲料 -->
  <fieldset>
    <legend>步驟 1:選擇飲料</legend>
    <label>
      <input type="radio" name="drink" value="紅茶" @change="onDrinkChange('紅茶')" /> 紅茶
    </label>
    <label>
      <input type="radio" name="drink" value="綠茶" @change="onDrinkChange('綠茶')" /> 綠茶
    </label>
    <p v-if="!drink">⚠️ 尚未選取飲料</p>
    <p v-else>✅ 已選:{{ drink }}</p>
  </fieldset>

  <!-- 步驟 2:甜度(只在選好飲料後顯示) -->
  <fieldset v-if="drink">
    <legend>步驟 2:選擇甜度</legend>
    <label>
      <input type="radio" name="sweetness" value="正常甜" @change="onSweetnessChange('正常甜')" /> 正常甜
    </label>
    <label>
      <input type="radio" name="sweetness" value="去糖" @change="onSweetnessChange('去糖')" /> 去糖
    </label>
    <p v-if="!sweetness">⚠️ 尚未選擇甜度</p>
    <p v-else>✅ 已選:{{ sweetness }}</p>
  </fieldset>

  <!-- 步驟 3:冰量(只在選好甜度後顯示) -->
  <fieldset v-if="drink && sweetness">
    <legend>步驟 3:選擇冰量</legend>
    <label>
      <input type="radio" name="ice" value="正常冰" @change="onIceChange('正常冰')" /> 正常冰
    </label>
    <label>
      <input type="radio" name="ice" value="去冰" @change="onIceChange('去冰')" /> 去冰
    </label>
    <p v-if="!ice">⚠️ 尚未選擇冰量</p>
    <p v-else>✅ 已選:{{ ice }}</p>
  </fieldset>

  <!-- 送出按鈕:需姓名 + 三選皆完成 -->
  <button :disabled="!canSubmit" @click="addOrder">送出</button>

  <!-- 簡短摘要(可選) -->
  <p v-if="canSubmit">
    你的選擇:{{ drink }} / {{ ice }} / {{ sweetness }}({{ name }}<span v-if="note">,備註:{{ note }}</span>)
  </p>

  <!-- 已送出的訂單清單 -->
  <section v-if="orders.length">
    <h3>目前已送出的訂單</h3>
    <ul>
      <li v-for="(o, i) in orders" :key="i">
        {{ i + 1 }}. {{ o.name }}:{{ o.drink }} / {{ o.ice }} / {{ o.sweetness }}
        <span v-if="o.note">(備註:{{ o.note }})</span>
      </li>
    </ul>
  </section>
</template>

<script setup>
import { ref, computed } from 'vue'

// Day 3 新增:姓名、備註
const name = ref('')
const note = ref('')

// Day 1/2:三個選項(維持 @change,不用 v-model)
const drink = ref('')
const sweetness = ref('')
const ice = ref('')

// Day 2:訂單清單
const orders = ref([])

// 事件處理(選項)
function onDrinkChange(v) { drink.value = v }
function onSweetnessChange(v) { sweetness.value = v }
function onIceChange(v) { ice.value = v }

// 送出條件:姓名 + 三選皆完成
const canSubmit = computed(() =>
  name.value && drink.value && sweetness.value && ice.value
)

// Day 2 + Day 3:送出一筆訂單,寫入姓名/備註
function addOrder() {
  if (!canSubmit.value) return
  orders.value.push({
    name: name.value,
    note: note.value,
    drink: drink.value,
    sweetness: sweetness.value,
    ice: ice.value
  })
  // 清空,準備下一筆
  name.value = ''
  note.value = ''
  drink.value = ''
  sweetness.value = ''
  ice.value = ''
}
</script>

Day3的程式碼跟play ground

🏁 總結

我們來整理一下今天學的東西

  1. 需求回顧

    • 在 Day 1 的流程控制(v-if / :disabled)和 Day 2 的事件驅動(@clickaddOrder)基礎上,
    • 讓使用者輸入 點餐者姓名(必填)與 備註(選填),並和飲料選項一起送出、顯示。
  2. 技術關鍵

    • v-model:把 <input><textarea> 的值與 Vue ref 變數雙向綁定。
      • 優勢:輸入時即時同步狀態,送出時一定是最新值。
      • 修飾符:.trim(去除前後空白)、.lazy(失焦後才同步)可依需求選用。
    • @click + addOrder():沿用 Day 2 送單邏輯,但 push 的物件新增 namenote
    • v-for:在畫面上即時顯示包含姓名、備註的新訂單。

✅ 小結

  • 概念遞進:Day 1 → 條件控制、Day 2 → 事件驅動、Day 3 → 雙向資料流
  • 我們學會了如何讓「使用者輸入 ↔ 系統狀態」無縫同步,並完整儲存姓名與備註。
  • 為 Day 4 的「狀態驅動樣式 (:class)」和 Day 5 的「清單統計」奠定了堅實基礎。

上一篇
Day 2 : 事件觸發的本質:為什麼需要 @click 才能把使用者行為轉成狀態
下一篇
Day 4 : 為什麼需要資料綁定?用「狀態→樣式」引出 :class
系列文
需求至上的 Vue 魔法之旅5
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言