[vue] v-model 的拆解與拼音輸入法的處理


Posted by vii120 on 2021-08-10

前言

Vue 提供的 v-model directive 讓 input 資料的雙向綁定變得很方便,省去寫 valueoninput 的過程,但若搭配元件使用,還是會需要把 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] 中可以找到,是使用了 compositionstartcompositionend 監聽輸入狀態,並定義了一個 composing 變數儲存當前狀態

[註1] 參考 vue version 2.6

composition event and IME

來試一下把原本的 input event 改成跟 v-model 的行為一樣:首先加入監聽 compositionstartcompositionend,然後加上一個儲存打字狀態的變數 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>

參考資料


#Vue #v-model #composition #ime







Related Posts

Linux 基本操作

Linux 基本操作

物件參考觀念 小試身手

物件參考觀念 小試身手

React 入門 0 - 開始寫 React 之前

React 入門 0 - 開始寫 React 之前


Comments