input type='number'でmaxlengthが使えない
<input>のtypeがtextの場合であれば、先頭n文字だけ入力可能にするにはmaxlength属性を使用すれば良い。しかしtype='number'の場合はmaxlength属性が利用できない。
Vueでの実現方法を考える。
Vue: v3.3.9
event.target.value.sliceで先頭n文字を切り出す
まず考えられるものとしてsliceを使用して先頭n文字を切り出す方法がある。
inputに入力されている値が切り詰められない問題
しかし以下のように:valueと@inputで記載すると、5文字以上入力した時にh1で表示している内容は4文字に切り詰められているのに対してinputの中身は5文字以降も残ったままとなってしまう。
<script setup>
import { ref } from 'vue'
const n = ref(0)
</script>
<template>
  <h1>{{ n }}</h1>
  <input
    type='number'
    :value='n'
    @input='event => { n = event.target.value.slice(0, 4) }'
  />
</template>原因は変数nが4文字以降変わらないことにある。
例えば1, 2, 3, 4, 5と順にinputに入力していくことを考える。
4文字目を入力したとき、inputには1234が入力され、変数nには1234が設定される。5文字目を入力したとき、inputには12345が入力されている。このとき変数nにはevent => { n = event.target.value.slice(0, 4) }の結果、1234が設定され、4文字入力したときと5文字入力したときとで変数の値が変わらないことになる。
変数の値が変わらないということは、Vueのrefが変更を検出しないということになり、inputの:valueの値をnで更新するタイミングがなくなってしまう。
そのため変数nは正しく1234となっているのに、inputには12345と入力されたままになる問題がある。
inputに入力されている値も切り詰める
方法1: 変数を2回に分けて変更する
解決策の1つに変数nを2回に分けて変更する方法がある。@inputでsliceした結果を変数nに設定する前に変数nにevent.target.valueを設定してあげることで、refが変更を検出できるようにしてあげる。
<script setup>
import { ref } from 'vue'
const n = ref(0)
</script>
<template>
  <h1>{{ n }}</h1>
  <input
    type='number'
    :value='n'
    @input='event => { n = event.target.value; n = n.slice(0, 4) }'
  />
</template>方法2: v-modelと@inputを組み合わせる
もう一つの解決策は、:valueと@inputの組み合わせで双方向バインディングを実現するのではなく、v-modelと@inputを組み合わせること。
<script setup>
import { ref } from 'vue'
const n = ref(0)
</script>
<template>
  <h1>{{ n }}</h1>
  <input
    type='number'
    v-model='n'
    @input='event => { n = event.target.value.slice(0, 4) }'
  />
</template>Vue SFC PlaygroundのJSタブを開くと分かる通り、onUpdate:modelValue(v-modelの記述があるため)とonInput(@inputの記述があるため)の二つが存在する。
"onUpdate:modelValue": _cache[0] || (_cache[0] = $event => ((n).value = $event)),
onInput: _cache[1] || (_cache[1] = event => { n.value = event.target.value.slice(0, 4) })変数nはまず$eventに設定されて、その後にslice(0, 4)した値に設定されるため、12345 -> 1234と変化する。そのためrefが変数nの変更を検出し、inputの入力項目も変数nに設定されている値に毎回上書きをしてくれる。
onUpdate:modelValue -> onInputの順を明確化するために、event.target.valueではなく変数nにsliceを適用した方がわかりやすいかもしれない。
@input='n = n ? Number(n.toString().slice(0, 4)) : undefined'