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'