🗝️

Vue3×TypeScript×TailwindCSSで、ワンタイムパスワードの入力フォームを作る方法

2024/04/15に公開

業務で必要になったのでワンタイムパスワードの入力フォームの実装方法について調べたところ、情報があまりなかったので、今回は、Vue3×TypeScript×TailwindCSSで、ワンタイムパスワードの入力フォームを作る方法について解説したいと思います。

まず、今回僕が作ったワンタイムパスワードの入力フォームの見た目は以下のような感じです。

大まかな仕様は以下のとおり。

仕様

  • 1つ1つの入力欄には0~9までの1桁のみしか入れられない
  • ワンタイムパスワードはコピペでも入力できる
  • 入力時に次の入力欄にフォーカスが自動で移動する

それでは、実装するにあたってのポイントをいくつか紹介していきます。

Vue3×TypeScript×TailwindCSSで、ワンタイムパスワードの入力フォームを作る方法

テンプレートの実装

まず、vueファイルのテンプレート部分を実装していきます。
ここでは、otp.vueというファイルを作成します。(otpはonetime-passwordの略。)

<template>
  <div class="flex items-center justify-center>
    <div class="flex items-center justify-center>
      <form class="flex gap-4">
        <input
          v-for="(n, index) in code"
          :id="'input_' + index"
          :key="index"
          v-model="code[index]"
          type="number"
          pattern="\d*"
          maxlength="1"
          @input="handleInput"
          @keypress="isNumber"
          @keydown.delete="handleDelete"
          @paste="onPaste"
          class="w-24 h-12 text-5xl text-center appearance-none focus:outline-none"
        />
      </form>
    </div>
  </div>
</template>

HTML部分だけ先に見てもわかりにくいと思いますが、inputタグ6つを用意して、それぞれに1桁の数値を文字列の形で入力していることがわかればここではOKです。(scriptタグの中を理解した後にテンプレートのコードに戻ってくると読みやすいかと思います。)

ちなみに、inputタグを1つだけ用意してそれを非表示にしつつ、v-forなどでdivタグを6つ用意するやり方もあるのですが、CSSの使い方が少し難しいと感じたので断念しました。

スクリプトの実装

次に、script部分ですが、こちらは以下のような感じです。

<script setup lang="ts">
/** 入力値の配列 */
const code: string[] = Array(6)
/** コピー&ペーストした文字列を1文字ずつ要素として持つ配列 */
let dataFromPaste: string[] | undefined
/** 許可する入力値の配列 */
const keysAllowed: string[] = ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9']

/** 入力値のバリデーション */
const isNumber = (event: Event) => {
  // prettier-ignore
  (event.currentTarget as HTMLInputElement).value = ''
  const keyPressed: string = (event as KeyboardEvent).key
  if (!keysAllowed.includes(keyPressed)) {
    event.preventDefault()
  }
}

/** 入力の仕方に応じてフォーカス移動 */
const handleInput = (event: Event) => {
  const inputType = (event as InputEvent).inputType
  let currentActiveElement = event.target as HTMLInputElement

  // 手入力のとき
  if (inputType === 'insertText')
    (currentActiveElement.nextElementSibling as HTMLElement)?.focus()

  // コピー&ペーストのとき
  if (inputType === 'insertFromPaste' && dataFromPaste) {
    for (const num of dataFromPaste) {
      const id: number = parseInt(currentActiveElement.id.split('_')[1])
      currentActiveElement.value = num
      code[id] = num
      if (currentActiveElement.nextElementSibling) {
        currentActiveElement =
          currentActiveElement.nextElementSibling as HTMLInputElement
        (currentActiveElement.nextElementSibling as HTMLElement)?.focus()
      }
    }
  }
}

/** 削除時に入力欄に値があるかどうかに応じてフォーカス移動 */
const handleDelete = (event: Event) => {
  const value = (event.target as HTMLInputElement).value
  const currentActiveElement = event.target as HTMLInputElement
  if (!value)
    (currentActiveElement.previousElementSibling as HTMLElement)?.focus()
}

/** クリップボードにある文字列から空白を取り除く */
const onPaste = (event: Event) => {
  dataFromPaste = (event as ClipboardEvent).clipboardData
    ?.getData('text')
    .trim()
    .split('')

  if (dataFromPaste) {
    for (const num of dataFromPaste) {
      if (!keysAllowed.includes(num)) event.preventDefault()
    }
  }
}
</script>

定義した関数について順番に解説していきます。

isNumber

isNumberは、入力が0~9までの数値の文字列かどうかを判定するための関数です。
もしそれ以外のものが入力された場合は、preventDefaultで処理を無効化しています。

handleInput

handleInputは、入力の仕方に応じてフォーカス移動するための関数です。
手入力の場合は、次の要素があればそこにフォーカスを移動、コピペの場合はコピペした文字列の分だけフォーカスを移動します。

ちなみに、currentActiveElement.nextElementSiblingは、currentActiveElementに隣接する次の要素を表します。

handleDelete

handleDeleteは、削除時に入力欄に値があるかどうかに応じてフォーカス移動する関数です。
こちらは仕様にはない部分なので、実装しなくても仕様は満たせますが、あると便利なので実装しています。

ちなみに、currentActiveElement.previousElementSiblingは、currentActiveElementに隣接する手前の要素を表します。

onPaste

onPasteは、ペースト時、クリップボードにある文字列から空白を取り除くための関数です。
こちらも仕様にはない部分なので、実装しなくても仕様は満たせますが、あると便利なので実装しています。

styleの実装

最後に、inputタグのスピナーを非表示にするために以下のCSSをstyleタグ内に記述すればOKです。

<style>
/* Chrome, Safari, Edge */
input::-webkit-outer-spin-button,
input::-webkit-inner-spin-button {
  -webkit-appearance: none;
  margin: 0;
}
</style>

参考記事

以下の記事がとても参考になりました。
How to Implement Verification Code With Vue 3 & TypeScript

Xについて

僕のXアカウントでは、主にweb開発、AI、会社経営のノウハウについて発信しています。もし興味があれば、フォローしていただけると嬉しいです。

プログラミング学習サポート&キャリア相談について

プログラミング学習サポート&キャリア相談も始めました。興味のある方はこちらから詳細をご覧ください。
https://shibayama-masaki.com/consulting/

Discussion