input type='number'でmaxlengthが使えない

<input>typetextの場合であれば、先頭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文字以降も残ったままとなってしまう。

Vue SFC Playground

<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回に分けて変更する方法がある。@inputsliceした結果を変数nに設定する前に変数nevent.target.valueを設定してあげることで、refが変更を検出できるようにしてあげる。

Vue SFC Playground

<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を組み合わせること。

Vue SFC Playground

<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ではなく変数nsliceを適用した方がわかりやすいかもしれない。

@input='n = n ? Number(n.toString().slice(0, 4)) : undefined'