21
5

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.

JavaScriptでライブラリっぽくバリデーションを実装する

Last updated at Posted at 2020-12-03

Ateam Hikkoshi samurai Inc.× Ateam Connect Inc. Advent Calendar 2020 3日目の記事です。


フロントエンド / バックエンドエンジニアの @hinora です。

普段JSでの開発というと、おそらく多くの人がVue, React, TSなどを用いていると思います。
ことバリデーションに関しては、ライブラリを用いる場合がほとんどです。

ですが最近、「バリデーション系のAPIって結構充実してるし、ライブラリなんてなくてもいけるのでは・・?」と思ったので、バニラJSで実際に作ってみました。
せっかくなので、ただ作るだけではなくライブラリっぽく取り回しやすい形で作ってみます。
あとは、できる限り今っぽく。

普段あまり目にすることのないバリデーション系のプロパティについては、要所で説明していきます。

実際に作った画面

image.png

デモページはこちら
※ バックエンドのバリデーション(モック)が通らないようにしてあります

なにをやっているのか

HTML5 Validationのエラーメッセージをそのままインラインバリデーションメッセージとして表示させています。
なので検証処理はネイティブに完全に任せている形です。楽ちん。

また、バックエンド側検証をするインターフェースもあるので、両方でチェックできます。

実装

HTML

HTML側で準備することは以下です。

  • フォームコントロールにnameを設定
  • フォームコントロールにバリデーションする制約(required)などを設定
  • エラーメッセージを表示する要素のdata-attr-name属性に対応するフォームのnameを設定

data属性の設定以外は、HTML5でバリデーションする場合と同じです。

JS

以下がメイン処理になります。

import { FormValidator } from './class/FormValidator'

const formContainer = document.querySelector('form')
const formValidator = new FormValidator(formContainer)

基本これだけです。

メインはFormValidatorになります。
FormValidatorの引数として渡されたformContainer: HTMLFormElementの所有するelementsにバリデータを付与します。

submitchangeのイベントにバリデーションがハンドルされています。
formValidator.validate()として、明示的に実行することもできます。

FormValidatorクラス

まずはコードです。
雑なJSDocはアノテーション目的なのでご容赦ください。

import { Validator } from './Validator'
import { InputValidator } from './InputValidator'

export class FormValidator extends Validator {
  /**
   * @param {HTMLFormElement} formContainer
   * @param {Function<Promise>} remoteValidator
   */
  constructor(formContainer, remoteValidator) {
    super()
    this.formContainer = formContainer
    this.remoteValidator = remoteValidator
    this._errors = []

    this.inputValidators = this.createInputValidatorList([
      ...formContainer.elements,
    ])
    formContainer.addEventListener('change', event => {
      this.handleChangeFormControll(event)
    })
    formContainer.addEventListener('submit', event => {
      this.handleSubmit(event)
    })
  }

  get isValid() {
    return this.formContainer.reportValidity()
  }

  /**
   * @param {Array<HTMLInputElement>} elements
   */
  createInputValidatorList(elements) {
    return elements.map(element => new InputValidator(element))
  }

  /**
   * @param {Event}
   */
  handleChangeFormControll({ target: { name } }) {
    this.findValidatorByName(name).validate()
  }

  /**
   * @param {Event}
   */
  async handleSubmit(event) {
    event.preventDefault()
    ;(await this.validate()) && this.formContainer.submit()
  }

  /**
   * @param {String} lookingName
   */
  findValidatorByName(lookingName) {
    return this.inputValidators.find(({ name }) => name === lookingName)
  }

  async validate() {
    if (!this.isValid) return false

    if (this.remoteValidate) {
      this.errors = await this.remoteValidate()
    }

    return !this.errors.length
  }

  async remoteValidate() {
    return this.remoteValidator ? this.remoteValidator() : []
  }

  set errors(errors) {
    errors.forEach(error => {
      this.errorMessage = error
    })
    this._errors = errors
  }

  get errors() {
    return this._errors
  }

  set errorMessage({ attributeName, message }) {
    const inputValidator = this.findValidatorByName(attributeName)
    inputValidator.errorMessage = message
    return message
  }
}

特筆するフォームバリデーション要素は2つ、

1つはHTMLFormElement.reportValidityです。
これはフォームエレメント内のすべてのフォームコントロールがHTMLバリデーション制約をクリアしているかをBooleanで返します。
今回は使いませんでしたが、checkValidityとすると、プラスで標準のtooltipが表示されます。

もう一つはremoteValidatorです。
第二引数として渡される独自の関数です。
API等を使ったバックエンドでの検証をする関数を渡すためのインターフェースとして用意しています。
渡されている場合は、Submit時にHTML5 Validationが通過したのちに実行されます。
ここで返されるエラーの配列もエラーメッセージとして表示されます。

remoteValidatorを使う場合は以下のようになります

const formContainer = document.querySelector('form')
const mockRemoteValidator = async () => {
  /**
   * モック >> 実際はAPIで検証する処理を書く
   */
  return [
    { attributeName: 'name', message: '既に使用されている名前です' },
    { attributeName: 'email', message: '無効なメールアドレスです' },
    { attributeName: 'flavor', message: '嘘をついてはいけません' },
  ]
}

new FormValidator(formContainer, mockRemoteValidator)

次に継承元クラスについて説明します。

Validatorクラス

継承しているValidatorはabstructクラスです。
この規模だとあまり恩恵がないですが、拡張を見越して作っておきます。

export class Validator {
  get isValid() {
    throw new Error('not implemented')
  }
}

最後にFormValidatorで使われていたInputValidatorクラスについて解説します。

InputValidatorクラス

このクラスは各フォームコントロールに適用されます。
まずはコード全体です。

import { Validator } from './Validator'

export class InputValidator extends Validator {
  /**
   * @param {HTMLInputElement} element
   */
  constructor(element, formContainer = null) {
    super()
    this.name = element.name
    this.inputElement = element
    this.formContainer = formContainer
    this.validity = element.validity
  }

  static get ERROR_MESSAGE_ATTRIBUTE_NAME() {
    return 'data-attr-name'
  }

  static get CHANGED_CLASS_NAME() {
    return 'changed'
  }

  validate() {
    this.errorMessage = this.validationMessage
    this.addChangedClass()
    return this.isValid
  }

  addChangedClass() {
    return this.inputElement.classList.add(InputValidator.CHANGED_CLASS_NAME)
  }

  get isValid() {
    return this.validity.valid
  }

  get parent() {
    return this.formContainer ?? window.document
  }

  get errorMessageElement() {
    return this.parent.querySelector(this.errorMessageElementSelector)
  }

  get errorMessageElementSelector() {
    return `[${InputValidator.ERROR_MESSAGE_ATTRIBUTE_NAME}="${this.name}"]`
  }

  /**
   * @param {String} text
   */
  set errorMessage(text) {
    this.errorMessageElement.textContent = text
  }

  get validationMessage() {
    return this.inputElement.validationMessage
  }
}

ここでのフォームバリデーションにおける特別な要素は一つ、HTMLInputElement.validityです。

これにはHTML5 Validationでのバリデーション結果が含まれています。
詳細は割愛しますが、アクセスできるプロパティのうちvalidのみを利用しています。
validは名前通り、検証の可否がBooleanで保存されています。

単体で使う場合

FormValidatorから利用される他、単体で利用することも可能です。

const inputValidator = new InputValidator(element)

// バリデーションし、結果に応じたエラーメッセージを紐づいたメッセージ表示要素に表示します。
inputValidator.validate()

// このように表示されるエラーメッセージを設定できます
// FormValidatorでのバックエンド検証のエラーはこれで反映されています
inputValidator.errorMessage = 'エラーメッセージです'

まとめ

ほとんど標準のAPIだけで、比較的簡単に実装できました。
独自の拡張をしたい場合などに、1つの選択肢になるかもしれません。

GitHubにコードがあるので、よかったら見てみてくださいね。


明日は @kawanakashotaro さんです! お楽しみに〜!

21
5
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
21
5

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?