先週投稿した VeeValidate で非同期的なバリデーションを行う記事に続いて、今回も VeeValidate ネタです。
VeeValidate には電話番号のバリデーションルールはデフォルトで用意されていません。そのため google-libphonenumber を使ってカスタムルールを実装してみたので、そのメモです。
ちなみに google-libphonenumber は電話番号のパースやバリデーションのための国際対応ライブラリです。非常に便利なので是非使ってみてください!
- Googleの電話番号を扱うライブラリlibphonenumberを使ってみたのでメモ - t-miliya612のブログ
- Javascriptで電話番号のバリデーションと変換(E.164)に便利なライブラリの紹介 - Qiita
TL;DR
- google-libphonenumber を使えば国内・国外どちらでも(あるいは両方について)電話番号のバリデーションが実装できる。
- 入力文字列のフィルタは
watch()
関数を使えばリアクティブに実装できる。
環境
- Vue: 2.x
- Composition API を使用
- VeeValidate: 3.2.5
- TypeScript: 3.7.4
実装
以下では VeeValidate のモジュール import は省略しています。
カスタムルール
まずは VeeValidate のカスタムルールを extend() で追加します。
import { PhoneNumberUtil } from 'google-libphonenumber'
extend('phone', {
message: 'The {_field_} field format is invalid',
validate(value) {
const util = PhoneNumberUtil.getInstance()
try {
const phoneNumber = util.parseAndKeepRawInput(value, 'JP')
return util.isValidNumber(phoneNumber)
} catch (err) {
return false
}
}
})
パースやバリデーションの機能は基本的に PhoneNumberUtil
から利用できます。
入力値が短すぎるなど、場合によっては parseAndKeepRawInput
でうまくパースできずに例外がスローされるので、その場合は false となるように try-catch で囲んでいます。
ちなみに国コードを指定せずにバリデーションするメソッドは(探した限りでは)用意されていないようです。
そのため日本の電話番号フォーマットにヒットしなかった場合は国外のフォーマットもチェックしたいといったケースでは以下のように全ての国コードを取得してループ処理する必要がありそうです。
validate(value) {
const util = PhoneNumberUtil.getInstance()
const jpFirstRegions = ['JP'].concat(
util.getSupportedRegions().filter(regionCode => regionCode !== 'JP')
)
const validRegionCode = jpFirstRegions.find((regionCode) => {
try {
const phoneNumber = util.parseAndKeepRawInput(value, regionCode)
return util.isValidNumber(phoneNumber)
} catch (err) {
return false
}
})
return validRegionCode !== undefined
}
コンポーネント
次にコンポーネントです。
これは単に ValidationProvider の rules に phone
を指定するだけです。
<template>
<ValidationProvider
v-slot="{ errors }"
ref="provider"
name="Phone Number"
rules="phone"
>
<input
v-model="value"
type="text"
/>
<span>{{ errors[0] }}</span>
</ValidationProvider>
</template>
<script lang="ts">
import { createComponent, ref } from '@vue/composition-api'
export default createComponent({
setup() {
const value = ref<string | null>(null)
return {
value
}
}
})
</script>
ただこれだと数字以外の文字も入力できてしまうため、よりユーザーフレンドリーになるように数字のみ許可する場合は以下のようになります。
<script lang="ts">
import { createComponent, ref, watch } from '@vue/composition-api'
export default createComponent({
setup() {
const value = ref<string | null>(null)
watch(() => {
value.value = value.value.replace(/[0-9]/g, (s) => {
// 全角から半角への変換
return String.fromCharCode(s.charCodeAt(0) - 0xFEE0)
}).replace(/[^0-9]/g, '')
})
return {
value
}
}
</script>
watch()
メソッドを用いて value
の更新があった際に数字以外の文字をフィルターアウトするようにしており、ついでに全角数字は半角数字に変換しています1。
ちなみに google-libphonenumber 自体は +1 202-456-1414
のような記号付きの電話番号フォーマットにも対応しているので、 + や - などを除外対象から外すように上記のルールを書き換えれば記号付きのケースにも対応できます。
input タグの type で制御すれば良いのでは?
input タグの type に number
や tel
を指定するアプローチだと幾つか問題があります。
まず number
ですが、こちらはユーザーがコピペで +1 202-456-1414
のような記号を含む番号を入力した場合に value
が空で評価されてしまいます!またデフォルトではスピンボタンが表示されますが、電話番号の数値の増減は不要であるため消去するのが結構手間です。
次に tel
ですが、こちらは pattern を指定しても入力値をフィルターする機能はないため、許可していない文字も入力できてしまいます。
このような事情もあり、最終的に Vue 側で入力値をコントロールするアプローチに落ち着きました。
-
この変換方法は YoheiM.NET さんの記事から拝借しました。 ↩