🐩

【Vue 3】v-model.trim の動きが textarea タグとコンポーネントで異なる件

2022/12/20に公開

Vue 3.2 で textarea タグをラップした UI コンポーネントを用意して v-model.trim を使ったところ、入力中に改行や空白文字など突然取り除かれる現象が発生しました:

App.vue
<script setup>
const msg = ref("");
</script>

<template>
  <CustomTextarea v-model.trim="msg" />
</template>

入力中に改行や空白文字など突然取り除かれる現象

このような現象は textarea タグで v-model.trim を使ったときには発生していなかったので、v-model.trim のドキュメントや実装を確認してどのような仕様となっているかまとめてみました。

textarea タグに v-model.trim を使う場合 [1]

  • v-model.trim による文字列のセット時には trim() メソッド を適用した文字列がセットされる [2]
  • フォーカスが外れたときに発生する change イベント が発生するまで textarea 要素の入力内容(value 属性の値)に対する trim() メソッドの適用を待つ機能がある [3][4]

コンポーネントに v-model.trim を使う場合

  • v-model.trim による文字列のセット時には trim() メソッドを適用した文字列がセットされる [5]
  • カスタムコンポーネントにおける v-modelupdate:modelValue というカスタムイベントを購読するが、inputchange といったネイティブイベントは購読しない
  • v-model.trim のように .trim 修飾子(Modifier)を付けて v-model を使用すると、modelModifiers という名前の prop で { trim: true } のようなオブジェクトが渡される [6]

コンポーネントの v-model.trim の振る舞いを textarea タグと同じようにするには?

親コンポーネントが .trim 修飾子を指定している場合は textarea タグに .trim 修飾子を付けて対応します:

CustomTextarea.vue
<script setup lang="ts">
import { computed, type PropType } from "vue";

const props = defineProps({
  modelValue: {
    type: String,
    required: true,
  },
  modelModifiers: {
    type: Object as PropType<{
      trim?: true;
    }>,
    default: () => ({}),
  },
});
const emit = defineEmits(["update:modelValue"]);

const data = computed({
  get() {
    return props.modelValue;
  },
  set(value) {
    emit("update:modelValue", value);
  },
});
</script>

<template>
  <textarea
    v-if="modelModifiers.trim"
    v-model.trim="data"
    :class="$style.textarea"
  />
  <textarea
    v-else
    v-model="data"
    :class="$style.textarea"
  />
</template>

<style lang="scss" module>
.textarea { /* 省略 */ }
</style>

v-if / v-else を用いて v-model.trim を使うか v-model を使うかを分岐させているので class 属性などの指定が重複してしまいますが、v-model の修飾子を動的に設定する方法が見当たらなかったためこの方法をとりました。

なお、textarea タグに対する v-model.trim とコンポーネントに対する v-model.trim によって trim() メソッドが二重に適用されることとなりますが、trim() メソッドは冪等[7]であるため許容できるものだと考えています。

その他の修飾子について

.number 修飾子

コンポーネントの v-model.number の振る舞いと textarea / input タグの v-model.number (または input[type="number"]v-model) の振る舞いは同じで、parseFloat() 関数で解釈できるものに限って数値をセットします。

.lazy 修飾子

コンポーネントの v-model.lazy 修飾子を適用しても動作に変化はなく、textarea / input タグの v-model.lazy のように振る舞うことはありません。
必要であれば、上記のサンプルコードと同様の対応が必要となります。

脚注
  1. input タグで type="text" に設定されている場合も同様です ↩︎

  2. https://github.com/vuejs/core/blob/v3.2.45/packages/runtime-dom/src/directives/vModel.ts#L50-L60 ↩︎

  3. https://github.com/vuejs/core/blob/v3.2.45/packages/runtime-dom/src/directives/vModel.ts#L61-L65 ↩︎

  4. https://github.com/vuejs/core/blob/v3.2.45/packages/runtime-dom/src/directives/vModel.ts#L80-L99 ↩︎

  5. https://github.com/vuejs/core/blob/v3.2.45/packages/runtime-core/src/componentEmits.ts#L118-L131 ↩︎

  6. https://v3.ja.vuejs.org/guide/component-custom-events.html ↩︎

  7. 任意の値に対して、関数を 1 回適用した結果と 2 回適用した結果が等しい ↩︎

株式会社ビザスク

Discussion