前言
Vue 提供的 v-model
directive 讓 input 資料的雙向綁定變得很方便,省去寫 value
與 oninput
的過程,但若搭配元件使用,還是會需要把 v-model 拆成 value & oninput。本篇主要分享 v-model 結合 component 的使用,以及拼音輸入法在監聽 input 事件時資料更新的行為
註:以下程式碼使用 Vue 2 的寫法,但概念也可應用在 Vue 3
v-model on components
先看一下 v-model 正常的使用方式,以 input text 為例,只要用 v-model 把定義好的 data 綁定在 input 上,即可做到即時更新
<template>
<div class="home">
<div class="input-wrapper">
<input type="text" v-model="message" />
</div>
</div>
</template>
<script>
export default {
data() {
return { message: '' };
},
};
</script>
但假設這個 input 會用在很多地方,我們通常會把它做成元件以減少重複的 code,這時 v-model 的使用方式就會比較不一樣,在子元件內會需要用到 value
與 @input
來接收與更新參數
父元件:定義 data,並用 v-model 綁定在 component 上
<template>
<div class="home">
<!-- use component here -->
<InputBox v-model="message" />
</div>
</template>
<script>
import InputBox from './components/InputBox.vue';
export default {
components: { InputBox },
data() {
return { message: '' };
},
};
</script>
子元件:接收 props value
,在 input
觸發時用 emit
把 value 更新到父層
<template>
<div class="input-wrapper">
<input
type="text"
:value="value"
@input="$emit('input', $event.target.value)"
/>
</div>
</template>
<script>
export default {
props: {
value: { type: String, default: '' }, // 需使用 'value' 這個名字才拿得到
},
};
</script>
延伸問題一:另一個事件
如果這個情況下,我還想要在 input 上加入別的事件,像是按 enter 後幫我送出資料呢?這個也是寫在子元件內,並用 emit 呼叫父層的事件
父元件:在子元件上寫好自定義的事件名稱及調用的 method
<template>
<div class="home">
<InputBox v-model="message" @onSubmit="handleSubmit" />
</div>
</template>
子元件:在 input 寫入 keypress event,並使用 emit 呼叫父層的自定義的事件
<template>
<div class="input-wrapper">
<input
type="text"
:value="value"
@input="$emit('input', $event.target.value)"
@keypress.enter="$emit('onSubmit')"
/>
</div>
</template>
延伸問題二:why keypress?
上面的 enter 監聽為什麼是用 keypress
而非 keyup
/keydown
呢?這牽涉到等等要講的輸入法問題,這邊先簡單說明:
中文輸入法是用拼音輸入,最後需要按 enter 完成打字,但這會跟我們想監聽的按 enter 送出資料這件事情打架 QQ 若使用 keyup/keydown 會導致剛打完中文字按下 enter 時,也同時送出資料,所以這裡使用在拼音輸入過程中不會被觸發的 keypress
input event and IME
前面的作法看起來是在元件內將 v-model 拆成了 value 與 input event,但這兩種寫法的效果是一樣的嗎?
<input type="text" v-model="message" />
// is it the same as below...🤨?
<input type="text" :value="message" @input="message = $event.target.value" />
直接看例子:
⬇️ v-model 打英文字會即時更新;中文字則會等到打字完成(底線消失)後才會更新
⬇️ input event 打英文字會即時更新;中文字在打字中也會即時更新,包含注音符號
為什麼會有這個差異呢?先看看 vue 文件上對 v-model 的其中一段說明:
IME 是指 input method engine,也就是使用拼音的輸入法,像是中文、日文、韓文。以中文來說,我們用注音打字時,過程會是這樣:1 打出注音 -> 2 拼成文字 -> 3 按 enter 結束打字,在 1 & 2 的過程中,文字下方都會有一條底線,告知使用者還在打字階段,且可以進行選字,按下 enter 後才會結束打字階段。然而這個打字的過程中,input event 也會持續被觸發,為避免過度頻繁的更新,v-model 處理掉了這段,在拼音的過程中不會一直更新 v-model,而是直到拼音結束才會更新
這段的做法在 v-model 原始碼 [註1] 中可以找到,是使用了 compositionstart
與 compositionend
監聽輸入狀態,並定義了一個 composing
變數儲存當前狀態
[註1] 參考 vue version 2.6
composition event and IME
來試一下把原本的 input event 改成跟 v-model 的行為一樣:首先加入監聽 compositionstart
與 compositionend
,然後加上一個儲存打字狀態的變數 isTyping
(像 vue 使用的 composing),compositionstart
時先切換成 isTyping = true,compositionend
時切換 isTyping = false 並執行原本的 input method。需要注意的是 composition event 只有監聽拼音輸入法,輸入英文時仍然是觸發 input event,所以這三個 event 都需要監聽,但 input event 只有在非打字中(isTyping === false)才呼叫更新,最終結果如下:
<template>
<div class="input-wrapper">
<input
:value="value"
type="text"
@compositionstart="isTyping = true"
@compositionend="
(event) => {
isTyping = false;
handleInput(event);
}
"
@input="
(event) => {
if (!isTyping) handleInput(event);
}
"
/>
<!-- 雖然拼音打字會觸發 input,但 isTyping === true 時不做任何事 -->
</div>
</template>
<script>
export default {
props: {
value: { type: String, default: '' },
},
data() {
return { isTyping: false }; // 目前是否為打字狀態
},
methods: {
handleInput(e) {
this.$emit('input', e.target.value);
},
},
};
</script>