現在、もりけん塾にてJavaScriptを勉強させていただいております。その記録。
今回は課題25についての備忘録です。

考え方が間違っている箇所もあるかと思いますが、お気づきの際はご指摘いただければと思います。

課題25について

仕様

課題25の仕様

  • 初回は送信ボタンとチェックボックスはdisabled状態。CSSは画像のように灰色にしてください
  • ユーザー名は16文字未満とし、もしinvalidならバリデーションテキストは 「ユーザー名は15文字以下にしてください。」
  • メールアドレスは一般的なメール形式のバリデーションにしてください。もしinvalidならバリデーションテキストは「メールアドレスの形式になっていません。」
  • パスワードのバリデーションは8文字以上の大小の英数字を交ぜたものとし、もしinvalidならバリデーションテキストは「8文字以上の大小の英数字を交ぜたものにしてください。」
  • 利用規約のスクロール実装に併せて、チェックボックスのdisabledは外し、checkedになる
  • 全ての入力がvalidの場合にのみ送信ボタンは緑色になり押下でき、register-done.htmlに遷移できる。

制作物

コードはこちらから(codesandbox)

※codesandboxではエラーが出る場合がありますが、リロードすれば動きます。

実装内容

前回の課題24(会員登録画面作成)にバリデーション機能を実装しました。

バリデーションについての情報をオブジェクトにまとめる

①isValidStatus
ユーザー名、メールアドレス、パスワードのバリデーションが有効かそうでないかを表す。入力した値が有効であったら、trueになり、全てtrueになったら、送信ができるようになる。

②validationInfo
それぞれのフォームのバリデーションの条件等についての情報がまとめてあります。それぞれのバリデーションの条件と、条件を満たしていなかった場合に出力されるエラーメッセージが格納してあります。

const isValidStatus = {
  name: false,
  mail: false,
  password: false,
}

const validationInfo = {
  name: {
    maxNameLength: 16,
    minNameLength: 1,
    validation: (value) => value.length > validationInfo.name.minNameLength && value.length < validationInfo.name.maxNameLength,
    errorMessage: '※ユーザー名は1文字以上15文字以下にしてください。',
  },
  mail: {
    //Reference: https://1-notes.com/javascript-determine-if-it-is-an-email-address/ 
    validation : (value) => /^(([^<>()[\]\\.,;:\s@"]+(\.[^<>()[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/.test(value),
    errorMessage: '※メールアドレスの形式になっていません。' 
  },
  password: {
    validation : (value) => /(?=.*[a-zA-Z])(?=.*[0-9])[a-zA-z0-9]{8,}/.test(value),
    errorMessage: '※8文字以上の大小の英数字を交ぜたものにしてください。'  
  }
}

今回実装したバリデーションの仕組み

関数ごとに説明します。

◆validateInputValue()

入力された値を検証します。
①e.targetでどこのフォームに入力されたかを検出
②そのフォームの条件について検証する関数(validationInfoオブジェクト内)に入力値を渡し、入力値が有効かを検証する。
③結果をsetValidationEvents()に渡す。

◆setValidationEvents()

検証結果が渡ってきたら実行する関数。

①結果がtrueであったら、
・対象フォームに’valid’のクラスを追加。(’invalid’がある際はremove)
・isValidStatusオブジェクトのステータスをtrueに変更。
・エラーメッセージは表示なし

結果がfalseであったら、
・対象フォームに’invalid’のクラスを追加。(’valid’がある際はremove)
・isValidStatusオブジェクトのステータスをfalseに変更。
・エラーメッセージを表示

const validateInputValue = (e) => {
  const targetForm = e.target;
  const value = targetForm.value.trim();
  const result = validationInfo[targetForm.id].validation(value);
  setValidationEvents(result,targetForm);
  renderRequiredFieldMessages(targetForm);  
  switchSubmitButton(checkAllValidity());
} 

const setValidationEvents = (isValid, targetForm) => {
  if (isValid) {
    targetForm.classList.add('valid');
    targetForm.classList.remove('invalid');
    targetForm.nextElementSibling.textContent = ''; 
    isValidStatus[targetForm.id] = true;
  } else {
    targetForm.classList.add('invalid');
    targetForm.classList.remove('valid');
    targetForm.nextElementSibling.textContent = validationInfo[targetForm.id].errorMessage;
    isValidStatus[targetForm.id] = false; 
  }
}
レビューPOINT

元々は、フォームごとに処理をそれぞれ分けて書いていました。同じような関数をつらつら書いてしまっているので、共通化できるのではとレビューいただきました。(今見ると本当に冗長…!)

それを受け、共通化できない箇所はオブジェクトに格納し、共通化できるところは、上記のvalidateInputValue()でまとめました。

//元々のコード
//それぞれのフォームごとに関数を分けていたけど、ほとんど共通の関数を使用している

// const checkNameLength = (e) => {
//   const nameValue = name.value.trim(); 
//   const result = nameValue.length > validationInfo.name.minNameLength && nameValue.length < validationInfo.name.maxNameLength ;
//   checkValidation(result,e.target);
//   switchSubmitButton();
//   CheckEmptyCharacter(e.target); 
// }

// const checkEmail = (e) => {
//   const mailValue = mail.value.trim();
//   const check = /^(([^<>()[\]\\.,;:\s@"]+(\.[^<>()[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/;
//   const result = check.test(mailValue);
//   checkValidation(result,e.target);
//   switchSubmitButton();
//   CheckEmptyCharacter(e.target); 
// }

// const checkPassword = (e) => {
//   const checkPasswordCondition = /(?=.*[a-zA-Z])(?=.*[0-9])[a-zA-z0-9]{8,}/;
//   const result = checkPasswordCondition.test(password.value);
//   checkValidation(result,e.target);
//   switchSubmitButton();
//   CheckEmptyCharacter(e.target); 
// }

もし空文字のみの入力があった場合の処理

空文字を検出し、エラーメッセージを表示する。

const renderRequiredFieldMessages = (targetForm) => {
  if (targetForm.value.trim() === "") {
    targetForm.nextElementSibling.textContent = "※入力必須項目です";
  }
};

//元々のコード
// const CheckEmptyCharacter = (targetForm) => {
//   if( !targetForm.value || !targetForm.value.match(/\S/g) ) {
//     targetForm.nextElementSibling.textContent = '※入力必須項目です';
//   }
// }

//省略形でも書けるがこの書き方だと他の関数の実行を邪魔してしまう
// const renderRequiredFieldMessages = (targetForm) => {
//   targetForm.nextElementSibling.textContent = targetForm.value.trim() === "" ? "※入力必須項目です" : "";
// };
レビューPOINT

空文字判定について、正規表現を使用していたのですが、trim()を使用しても書き換え可能とレビューいただきました。その方が短く、正規表現よりも可読性が高いと感じたのでそちらに修正しました。

レビューPOINT

このif文を省略形でも書けるとレビューをいただきました。
しかしながら、この形で書くと、検証時に通らなかった場合のエラーメッセージ(setValidationEvents()の部分)が出なくなってしまいました。なんで別の関数が動かなくなるんだろう…と疑問だったのですが、「入力があった場合エラーメッセージは表示しない」というここの関数での条件が強制的に実行されてしまっているからと気付きました。

同じspanタグにエラーを表示させているので、他の関数を上書きするような書き方はダメなんだと勉強になりました。
結果、元のif文の書き方に戻しました。

全てのフォームが検証に通ったか確認

◆checkAllValidity
isValidStatusオブジェクト内の値が全てtrueだったら、trueを返す。
ここでは、ユーザー名、メールアドレス、パスワードの検証が全て通った場合にtrueを返す。ここでの結果をswitchSubmitButton()に渡す。

◆switchSubmitButton
trueが渡ってきて、尚且つ、チェックボックスにチェックが入っていたら、ボタンのdisableをfalseにする。(ボタンを有効化させる)

const checkAllValidity = () => {
  return Object.values(isValidStatus).every((result) => result );
}

const switchSubmitButton = (isValid) => {
  submitButton.disabled = isValid && checkbox.checked ? false : true;
} 

//当初のコード
// const switchSubmitButton = () => {
//   if(validationInfo.name.status === true && validationInfo.mail.status === true && validationInfo.password.status === true && checkbox.checked) {
//     submitButton.disabled = false; 
//   } else {
//     submitButton.disabled = true;  
//   }
// } 
レビューPOINT

checkAllValidityは後から追加した関数です。
元々ここは単純に全てのステータスがtrueだったらと、&&でつらつら繋げて書いていました。これではチェック対象が増えれば増えるほど、ここの記載を追加していかなければならないとレビューいただきました。(確かに。。!)

今回は「全てがtrueだったら」という条件なので、every()メソッドを使用しました。(初めての使用)
Object.values()で、検証結果の配列を作成し、配列の中が全てtrueの場合はtrueを返します。

元々検証結果についても、validationInfoオブジェクトに入れてまとめていたのですが、ネストが深くなるとここでほしい配列作成が難しかったので、isValidStatusオブジェクトとして分けて新たに作成したのが経緯となります。

blurイベントで処理が走る

バリデーションの検証処理の発火はaddEventListenerのblurイベントを使用しました。
checkboxについては、チェックが入ったり外れたりの動きで即時にtrueとfalseを切り替えたいのでinputイベントを使用しました。実際にはchangeイベントでも良さそうで、動きは変わらないのでそっちのがよかったかな?

nameInputArea.addEventListener('blur', validateInputValue);
mailInputArea.addEventListener('blur', validateInputValue);
passwordInputArea.addEventListener('blur', validateInputValue);
checkbox.addEventListener("input", () => {
  switchSubmitButton(checkAllValidity());
});
その他レビュー箇所【CSS】

ボタンが活性化されていない時は、cursor: not-allowed; を追加した方が良い。

.submit_button:disabled {
    background: #ccc;
    cursor: not-allowed;
}
その他レビュー箇所

今回いろんな箇所で、関数名と引数名についてご指摘をいただきました。
中身と関数名が合致していないことや、返り値がbooleanである場合には、is***を使用すると良い(これは以前にも言われた気がする…!)等々。それらを綺麗に整備していくと本当に読みやすいコードになったなと感じました。命名って大事だな。これからもっと気をつけよう。

後記

初めてのバリデーション実装は難しかったです、考えることが沢山出てきて、こっちは動いたけどこっちが動かないという連続でした。細かいところまでレビューいただき、最終的にはとてもいいコードになった気がして嬉しいです、大変勉強になりました、ありがとうございました!

c.sakyou

コメントを残す

メールアドレスが公開されることはありません。 * が付いている欄は必須項目です

CAPTCHA