【Vue 3】v-model.trim の動きが textarea タグとコンポーネントで異なる件
Vue 3.2 で textarea タグをラップした UI コンポーネントを用意して v-model.trim を使ったところ、入力中に改行や空白文字など突然取り除かれる現象が発生しました:
<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-modelはupdate:modelValueというカスタムイベントを購読するが、inputやchangeといったネイティブイベントは購読しない - 
v-model.trimのように.trim修飾子(Modifier)を付けてv-modelを使用すると、modelModifiersという名前の prop で{ trim: true }のようなオブジェクトが渡される [6] 
 コンポーネントの v-model.trim の振る舞いを textarea タグと同じようにするには?
親コンポーネントが .trim 修飾子を指定している場合は textarea タグに .trim 修飾子を付けて対応します:
<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 のように振る舞うことはありません。
必要であれば、上記のサンプルコードと同様の対応が必要となります。
- 
inputタグでtype="text"に設定されている場合も同様です ↩︎ - 
https://github.com/vuejs/core/blob/v3.2.45/packages/runtime-dom/src/directives/vModel.ts#L50-L60 ↩︎
 - 
https://github.com/vuejs/core/blob/v3.2.45/packages/runtime-dom/src/directives/vModel.ts#L61-L65 ↩︎
 - 
https://github.com/vuejs/core/blob/v3.2.45/packages/runtime-dom/src/directives/vModel.ts#L80-L99 ↩︎
 - 
https://github.com/vuejs/core/blob/v3.2.45/packages/runtime-core/src/componentEmits.ts#L118-L131 ↩︎
 - 
https://v3.ja.vuejs.org/guide/component-custom-events.html ↩︎
 - 
任意の値に対して、関数を 1 回適用した結果と 2 回適用した結果が等しい ↩︎
 
Discussion