5
4

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

VeeValidate に電話番号のバリデーションルールを追加する

Posted at

先週投稿した VeeValidate で非同期的なバリデーションを行う記事に続いて、今回も VeeValidate ネタです。

VeeValidate には電話番号のバリデーションルールはデフォルトで用意されていません。そのため google-libphonenumber を使ってカスタムルールを実装してみたので、そのメモです。

ちなみに google-libphonenumber は電話番号のパースやバリデーションのための国際対応ライブラリです。非常に便利なので是非使ってみてください!

TL;DR

  • google-libphonenumber を使えば国内・国外どちらでも(あるいは両方について)電話番号のバリデーションが実装できる。
  • 入力文字列のフィルタは watch() 関数を使えばリアクティブに実装できる。

環境

実装

以下では 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 に numbertel を指定するアプローチだと幾つか問題があります。

まず number ですが、こちらはユーザーがコピペで +1 202-456-1414 のような記号を含む番号を入力した場合に value が空で評価されてしまいます!またデフォルトではスピンボタンが表示されますが、電話番号の数値の増減は不要であるため消去するのが結構手間です。

次に tel ですが、こちらは pattern を指定しても入力値をフィルターする機能はないため、許可していない文字も入力できてしまいます。

このような事情もあり、最終的に Vue 側で入力値をコントロールするアプローチに落ち着きました。

  1. この変換方法は YoheiM.NET さんの記事から拝借しました。

5
4
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
5
4

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?