【もりけん塾】JavaScript課題25 – Vanilla JSでフォームバリデーションを作成 –

JavaScript

Vanilla JSを使用してフォームのバリデーションを実装しました。

こんにちは。Webコーダーのはるです。

現在、所属している「もりけん塾」でハンズオン課題 に取り組み、レビューをいただいています。

今日は、課題25 についてアウトプットをしていきます。
(前回までのアウトプットは、こちらです。)

実装する過程で学んだことを、学習ノートとして記録していきます。

認識の間違えている箇所がありましたら、お問い合わせからご指摘いただけるとうれしいです。

JavaScript課題25の仕様

今回、取り組んだ課題はこちらです。

【仕様】

  • 初回は送信ボタンとチェックボックスはdisabled状態。CSSは画像のように灰色にしてください

  • ユーザー名は16文字未満とし、もしinvalidならバリデーションテキストは 「ユーザー名は15文字以下にしてください。」

  • メールアドレスは一般的なメール形式のバリデーションにしてください。もしinvalidならバリデーションテキストは「メールアドレスの形式になっていません。」

  • パスワードのバリデーションは8文字以上の大小の英数字を交ぜたものとし、もしinvalidならバリデーションテキストは「8文字以上の大小の英数字を交ぜたものにしてください。」

  • 利用規約のスクロール実装に併せて、チェックボックスのdisabledは外し、checkedになる

  • 全ての入力がvalidの場合にのみ送信ボタンは緑色になり押下でき、register-done.htmlに遷移できる。

(もりけん先生のJavaScriptハンズオン課題より)

前回までの課題はこちらです。

マークアップ

前回の課題まとめで書いているので割愛します。

バリデーションの条件

レビューをいただいて改善した大きな部分に、バリデーションの条件や内容をオブジェクトでまとめたことがあります。

まず、正規表現などの条件をオブジェクトにまとめました。

const validationTerms = {
 name: {
  upperLimitOfText: 15
 },
 password: {
  pattern: /^(?=.*?[a-z])(?=.*?[A-Z])(?=.*?\d)[a-zA-Z\d]{8,}$/
 }
}

名前の文字数は15を上限。

パスワードの制限は正規表現を使用しました。

仕様が「8文字以上の大小の英数字を交ぜたもの」だったので、

・ (?=.*?[a-z]) → 英小文字含む

・ (?=.*?[A-Z]) → 英大文字含む

・ (?=.*?\d) → 数字を含む ※(?=.*[0-9])とも書ける。\d が “数字に一致”という意味。

[a-zA-Z\d]{8,}→ 最低8文字

としました。

書籍の独習JavaScriptや他記事なども参考にしましたが、一番わかりやすかったのは下のzennでした。ありがとうございます。

 

次に、バリデーションの内容をオブジェクトにまとめました。

const validationOptions = {
 name: {
  isValid: () => {
   return nameOfInput.value.length < validationTerms.name.upperLimitOfText;
  },
  errorMessage: "ユーザー名は15文字以内で入力してください",
 },
 email: {
  isValid: () => {
   return emailOfInput.validity.valid;
  },
  errorMessage: "メールアドレスの形式になっていません",
 },
 password: {
  isValid: () => {
   return validationTerms.password.pattern.test(passwordOfInput.value);
 },
  errorMessage: "8文字以上の大小の英数字を交ぜたものにしてください",
 }
};

isValidには、validになる条件。name/email/passwordに分けて、それぞれを管理するようにしました。

passwordのバリデーションには正規表現を使用したので、testメソッドを使用してみました。

return validationTerms.password.pattern.test(passwordOfInput.value);

引数に指定した値をテストして、指定したpatternに当てはまるかをboolean値で返します。

このようにバリデーションのオプションとしてオブジェクトで管理することで、見やすいしメンテナンス性も上がります。

フォーム全体のバリデーション

フォーム全体のバリデーションを集計するために、

それぞれの値がinvalidの時にinvalidクラスを付与するようにしました。

const addInvalidClass = target => target.classList.add("invalid");
const removeInvalidClass = target => target.classList.remove("invalid");
targetには、inputの要素が渡ってきます。

 

isValidInputAndCheckbox()関数では、全体のinvalidクラスの数とチェックボックスにチェックが入っているかを調べてboolean値を返します。

const isValidInputAndCheckbox = () => {
 const invalidItem = document.getElementsByClassName("invalid");
 return invalidItem.length === 0 && checkbox.checked;
};
このboolean値をもとに、trueであればsubmitボタンのdisabledをfalseにする関数も作成しました。
const checkFormValidityToEnableSubmitButton = () => submitButton.disabled = isValidInputAndCheckbox() ? false : true;

これでフォーム全体のバリデーションは完成です。

input個別のバリデーション

次に、input個別のバリデーションです。

addEventListenerのイベントtype “blur” の時に、バリデーションが発動するようにしました。

MDN引用

blurイベントは、要素がフォーカスを失ったときに発生します。

イベントtype “input”にバリデーションを走らせると、ユーザーが一文字入力するたびに調査されてしまいパフォーマンスもユーザビリティも劣ってしまいます。

formElements.forEach(element => {
 element.classList.add("invalid");

 element.addEventListener("blur", (e) => {
  const target = e.target;
  checkFormValidityInBlur(target);
 });
});

formElementsには、name/email/passwordのinput要素が渡ってきます。

それぞれの要素に、あらかじめinvalidクラスを付与しておきます。

inputのフォーカスが外れたタイミングで checkFormValidityInBlur(target)関数が実行されます。

解説のためにコメントを入れてみます。

const checkFormValidityInBlur = (target) => {
 submitButton.disabled = true;

 // inputが空だったら
 if (!isNotEmptyOfInput(target)) { 
  addInvalidClass(target);
  target.nextElementSibling.textContent = "入力してください";
  return;
 }

 // inputがバリデーションの条件を満たしていなかったら
 if (!isValidFormInput(target)) {
  showErrorMessage(target);
  addInvalidClass(target);
  return;
 }

 // 空でない かつ バリデーションの条件を満たしている時
 target.nextElementSibling.textContent = "";
 removeInvalidClass(target);
 checkFormValidityToEnableSubmitButton();
};

それぞれのif文で使ったboolean値を返す関数はこちらです。

// inputが空でなければtrue
const isNotEmptyOfInput = target => target.value.trim() !== "";

// inputがバリデーションの条件を満たして入ればtrue
const isValidFormInput = target => validationOptions[target.id].isValid();

 

これらのバリデーション作業を、inputからフォーカスが外れるたびに実行します。

checkboxのバリデーション

前回の課題で、利用規約を下まで読んだら自動的にcheckboxにチェックが入るように実装済みです。

今回は、その後にユーザーが意図的にチェックを外した場合の実装を行いました。

checkbox.addEventListener("input", () => {
 submitButton.disabled = !checkbox.checked;
 checkFormValidityToEnableSubmitButton();
})

checkboxには、イベントtype “input”を使用しました。checkboxの値が変わったら発火します。

① submit buttonのdisabledをチェックがついているか否かで切り替えます。

・チェックがついている時(checkbox.checked) → disabledはfalse
・チェックがついていない時(!checkbox.checked) → disabledはtrue

常にお互いが反対のbooleanを持つことから、下記のように表せるとレビューで教わりました。

submitButton.disabled = !checkbox.checked;

②submit buttonをアクティブにするか否かを判定するcheckFormValidityToEnableSubmitButton()を実行します。

submitボタンの挙動

前回はバリデーションに関係なく、ボタンを押下するとresister-done.html に飛ぶところまで実装しました。

今回の仕様では、全てのバリデーションが通った場合のみページ遷移するように変更しました。

全てのバリデーションが通ったかどうかは、先述のisValidInputAndCheckbox()関数で調べます。

const isValidInputAndCheckbox = () => {
 const invalidItem = document.getElementsByClassName("invalid");
 return invalidItem.length === 0 && checkbox.checked;
};

全てのinputにinvalidクラスがないか、checkboxにチェックが入っているかを調べてbooleanが返ってきます。

submitButton.addEventListener("click", (e) => {
 e.preventDefault();
 if (isValidInputAndCheckbox()) window.location.href = "./register-done.html";
});

CSS: pointer-events

全ての項目がvalidの場合、ブラウザのどこかをクリックしないとバリデーションが走りませんでした。

イベントtype “blur”時にバリデーションを実行するようにしていたので、フォーカスが外れるのを待つ必要があるからです。

(GitHub PR画面より)

pointer-events を使用することで、submitボタンを押す前に、他のどこかを押すことなくバリデーションを発動できます。レビューで教わりました。

・pointer-events: none → ポインターイベントの対象から外す
・pointer-events: auto → ポインターイベントが通常通り当たる(プロパティが指定されていないときと同様になる)

.form__button input{
 pointer-events: none;
}

.form__button input:not([disabled]) {
 pointer-events: auto;
}

該当buttonにdisabledがない時のみ、pointer-events: auto;になるように設定しました。

 

全てがvalidの時、直でsubmit buttonを押してバリデーションが通るようになりました!

CSS: cursor: not-allowed

ユーザーに明確に押せないことが伝わるカーソルがあることをレビューで教えていただきました。

cursorはpointerしか指定したことがなかった..

cursor: not-allowed を指定した時のカーソルです。

こちらを、checkboxとsubmitボタンに指定しました。

※下記のCSSは必要な部分のみ抜粋しています

.form__check input[type=checkbox][disabled]{
 cursor: not-allowed;
}

.form__button{
 cursor: not-allowed;
}

.form__button input{
 pointer-events: none;
}

.form__button input:not([disabled]) {
 pointer-events: auto;
}

pointer-eventsと併用するために、cursor: not-allowedは親要素に付与しました。

普通に併用すると、pointer-evenetsが効かなくなってしまいます。

レビューでいただいた参考記事です。

 

今回のコード

OpenSandboxから詳細を見ることができます。

//
学習に使用している本は、JavaScript本格入門・独習JavaScriptです。

  

あとがき

さえさん(@sae_prog)、まいさん(@mai2022web)、もなかさん(@ruby443n)、もりけん先生(@terrace_tec)レビューいただきありがとうございました!

バリデーションの仕様を満たすのが思ったよりずっと難しくて、何度もレビューでアドバイスをいただいてここまで完成しました。

JSのオブジェクトを使ったバリデーション条件の管理や、細かいバリデーションの制御、CSSでのユーザー体験の向上など、たくさんのことを学ぶことができました。

お時間を割いていただきありがとうございました!

今日は以上です。

//

【もりけん塾で勉強しています】

もりけん先生(@terrace_tec)のHPはこちら