素のJSでフォームのバリデーションを実装する - ライブラリなど未使用、リアルタイムでの実行など...
素のJavaScriptでフォームのバリデーションを実装する方法を解説します。
最終的な制作物は以下のようなイメージです。(CodePenが動かない方は動画で確認する)
See the Pen
Form by dadada (@dadada-dadada)
on CodePen.
また、今回は以下前提で実装しています。
- jQuery未使用
- ライブラリ未使用
- ES6記法(必要に応じてbabelなどでコンパイルしてください)
- テキストボックス、チェックボックス、ラジオボタン、セレクトボックス、テキストボックスに対応
- はじめに
- 実際のコード
- 解説
- 大まかな流れ
- constructor() - オブジェクト生成時に実行されるメソッド
- getIsFormError() - フォームのバリデーションエラーを確認するメソッド
- getInputType() - input要素の種類を確認するメソッド
- errorCheck() - バリデーションエラーをチェックするメソッド
- createInputErrorMessage() - エラーメッセージを描画するメソッド
- toggleFormErrorMessage() - フォーム全体のエラーメッセージの表示を切り替えるメソッド
- errorReset() - バリデーションエラーをリセットするメソッド
- validate() - バリデーションの処理を「実行する」メソッド
- addEvent() - イベントを設定するメソッド
- init() - 初期化用メソッド
- 最後にクラスをインスタンス化させる
- まとめ
はじめに
サンプルコードを見たい方はこちら
フォームを実装する際の注意点
JavaScriptでフォームのバリデーションを実装する際、注意点があります。
それは、バリデーションを「サーバーサイド」でも実装するということです。
バリデーションはユーザーのPC側(フロントサイド)と、サイトデータがあるサーバー側(サーバーサイド)の2箇所で行えます。
フロントサイドでは、ユーザーが自由にコードを改変できます。
例えば、フロントサイド(JavaScript)でしかバリデーションを実装していないと、「ユーザーがJavaScriptを無効にして、フォームに不正なスクリプトやデータを仕込んで送信する」のようなことが簡単にできてしまいます。
フロントサイドは改変可能ですが、サーバーサイドは基本的には改変できません。
なので、フォームを実装する際は、サーバーサイドでも必ずバリデーションを実装するようにしましょう。
ES6記法のクラス構文
今回のコードはES6のクラス構文で実装しています。
クラス構文とは...
多くのオブジェクト指向プログラミング言語に備わるクラス(class)という概念について紹介します。
クラス(class)とは、端的にいえばモノの設計書です。
先ほどオブジェクト指向とは、プログラミングを現実のモノのように構成するものだと解説しましたが、クラスとはこのモノのように構造する際の、モノそのものの設計書にあたります。言葉としては、以下のような意味の対応表になります。
オブジェクト = モノ
クラス = モノ(オブジェクト)の設計書
クラスはモノ(オブジェクト)の設計書であるため、多くのプログラミング言語においてモノ(オブジェクト)を作成する際に必要なものとして定義されることが多いです。
参考:https://techplay.jp/column/482
文章だけを読むと難しく感じるかもしれませんが、結局JavaScriptを書いているだけです。
興味がある方は、以下の記事を読んでみてください。
前置きは以上になります。
次の項から実際にコードを解説していきます。
実際のコード
CodePen でも公開しています。
■JavaScript
/**
* フォームのバリデーションを実行するクラス
* `elmForm`, `elmTargetInputs`, `elmSubmitBtn`は必須
*
* 場合によっては、以下ライブラリでバリデーションするでも良いかも
* 公式: https://imbrn.github.io/v8n/#what-s-v8n
* 日本語解説記事: https://co.bsnws.net/article/182
*/
class FormValidator {
/**
* @property {Object} elmForm 【必須】form要素
* @property {Array} elmTargetInputs 【必須】バリデーション対象となるinput要素の配列
* @property {Object} elmSubmitBtn 【必須】送信ボタンの要素
* @property {Array} elmFormErrorMessages フォーム全体のエラーメッセージ要素の配列
* @property {String} classErrorInput エラーの場合、input要素に付与されるclass
* @property {String} classSecureInput エラーが無い場合、input要素に付与されるclass
* @property {String} attrElmErrorMessage エラーメッセージ要素をinput要素と紐付けるための属性名
* @property {String} attrRequiredErrorMessage `required`のエラーメッセージの文言を変更するための属性名
* @property {String} defaultErrorMessage デフォルトのエラーメッセージ
* @property {Object} inputStatuses バリデーション対象となる全てのinput要素のオブジェクト. エラーの状態などのプロパティを持つ
* @property {Boolean} isFirstSubmit 1回でも送信ボタンをクリックしたら`true`
*/
constructor(_parm) {
this.elmForm = document.querySelector(_parm.form) || false;
this.elmTargetInputs = [
...this.elmForm.querySelectorAll(_parm.targetInputs)
];
this.elmSubmitBtn = this.elmForm.querySelector(_parm.submitBtn);
this.elmFormErrorMessages = [
...this.elmForm.querySelectorAll("[data-js-form-error-message]")
];
this.classErrorInput = _parm.classErrorInput || "__error";
this.classSecureInput = _parm.classSecureInput || "__secure";
this.attrElmErrorMessage =
_parm.attrElmErrorMessage || "data-js-error-message";
this.attrRequiredErrorMessage =
_parm.attrRequiredErrorMessage || "data-required-error";
this.defaultErrorMessage =
_parm.defaultErrorMessage || "必須項目を入力してください";
const createInputStatuses = this.elmTargetInputs.map((_item) => {
let result = [];
result["name"] = _item.getAttribute("name");
result["isError"] = true;
return result;
});
this.inputStatuses = createInputStatuses.filter(
(_item, _index, _self) =>
_self.findIndex((_ev) => _ev.name === _item.name) === _index
);
this.isFirstSubmit = false;
}
/**
* input要素が一つでもバリデーションエラーなら`true`を返す
* @return {Boolean}
*/
getIsFormError() {
return this.inputStatuses.every((_item) => _item["isError"] === false)
? false
: true;
}
/**
* バリデーション対象となるinput要素の種類を返す
* @param {Object} _elmInput バリデーション対象のinput要素
* @return {String} 'checkOrRadio', 'select', 'input'
*/
getInputType(_elmInput) {
if (_elmInput.tagName === "SELECT") return "select";
if (_elmInput.getAttribute("type").match(/checkbox|radio/))
return "checkOrRadio";
return "input";
}
/**
* input要素に付与されている`required`と`pattern`でバリデーションチェックを行う
* エラーなら`true`を返す
* @param {Object} _elmInput バリデーション対象のinput要素
* @returns {Boolean}
*/
errorCheck(_elmInput) {
const patternValidate = _elmInput.getAttribute("pattern") || false;
const inputType = this.getInputType(_elmInput);
// セレクトボックスの場合
if (inputType === "select")
return !_elmInput.validity.patternMismatch && _elmInput.value.length
? false
: true;
// チェックボックスかラジオボタンの場合
if (inputType === "checkOrRadio") return _elmInput.checked ? false : true;
// input(パターン有り)の場合
if (patternValidate)
return !_elmInput.validity.patternMismatch && _elmInput.value.length
? false
: true;
// input(パターン無し)の場合
return _elmInput.validity.valueMissing;
}
/**
* input要素に紐づくエラーメッセージ要素のテキストを描画
* @param {Object} _elmInput バリデーション対象のinput要素
*/
createInputErrorMessage(_elmInput) {
const value = _elmInput.value;
const name = _elmInput.getAttribute("name");
const elmErrorMessage = this.elmForm.querySelector(
`[${this.attrElmErrorMessage}="${name}"]`
);
const patternErrorMessage = _elmInput.getAttribute("title") || false;
const requiredErrorMessage =
_elmInput.getAttribute(this.attrRequiredErrorMessage) ||
this.defaultErrorMessage;
let errorMessage = "";
if (patternErrorMessage) {
errorMessage = value.length ? patternErrorMessage : requiredErrorMessage;
} else {
errorMessage = requiredErrorMessage;
}
elmErrorMessage.textContent = errorMessage;
return;
}
/**
* フォーム全体のエラーメッセージの表示を切り替える
* @param {Boolean} _show `true`: 表示, `false`: 非表示
*/
toggleFormErrorMessage(_show) {
this.elmFormErrorMessages.forEach((_elmFormErrorMessage) => {
_show
? (_elmFormErrorMessage.style.display = "block")
: (_elmFormErrorMessage.style.display = "none");
});
}
/**
* input要素のエラーをリセット
* @param {Object} _elmInput バリデーション対象のinput要素
*/
errorReset(_elmInput) {
const name = _elmInput.getAttribute("name");
const elmErrorMessage = this.elmForm.querySelector(
`[${this.attrElmErrorMessage}="${name}"]`
);
_elmInput.classList.remove(this.classErrorInput);
_elmInput.classList.remove(this.classSecureInput);
elmErrorMessage.textContent = "";
return;
}
/**
* input要素に対してバリデーションチェックやエラーメッセージの描画など、バリデーションに関する関数を全て実行する
* @param {Object} _elmInput バリデーション対象のinput要素
*/
validate(_elmInput) {
// エラーリセット
this.errorReset(_elmInput);
// 必要な変数定義
const isError = this.errorCheck(_elmInput);
const targetInputStatus = this.inputStatuses.find((_item) => {
return _item.name === _elmInput.getAttribute("name");
});
const changeInputStatusArray = (_isErrorValue) => {
targetInputStatus["isError"] = _isErrorValue;
};
// チェックボックスかラジオボタンの場合、いずれかがチェックされていればエラーにしない
if (this.getInputType(_elmInput) === "checkOrRadio") {
const ElmsCheckOrRadio = [
...document.querySelectorAll(
`input[name="${targetInputStatus["name"]}"]`
)
];
const getIsAnyChecked = () => {
return ElmsCheckOrRadio.some((_elm) => {
return _elm.checked;
});
};
const toggleAllCheckOrRadioRequired = (_value) => {
ElmsCheckOrRadio.forEach((_elm) => {
_elm.required = _value;
});
};
if (getIsAnyChecked()) {
toggleAllCheckOrRadioRequired(false);
changeInputStatusArray(false);
} else {
toggleAllCheckOrRadioRequired(true);
changeInputStatusArray(true);
this.createInputErrorMessage(_elmInput);
}
return;
}
// バリデーションチェックやエラーメッセージの描画などを実行
if (isError) {
_elmInput.classList.add(this.classErrorInput);
changeInputStatusArray(true);
this.createInputErrorMessage(_elmInput);
} else {
changeInputStatusArray(false);
_elmInput.classList.add(this.classSecureInput);
}
return;
}
addEvent() {
/**
* Input
*/
this.elmTargetInputs.forEach((_elmTargetInput) => {
// 入力時
_elmTargetInput.addEventListener("change", (_ev) => {
this.validate(_elmTargetInput);
this.isFirstSubmit
? this.getIsFormError()
? this.toggleFormErrorMessage(true)
: this.toggleFormErrorMessage(false)
: false;
});
// エラー時
_elmTargetInput.addEventListener("invalid", (_ev) => {
this.validate(_elmTargetInput);
});
});
/**
* SubmitBtn
*/
this.elmSubmitBtn.addEventListener("click", (_ev) => {
this.isFirstSubmit = true;
this.getIsFormError()
? this.toggleFormErrorMessage(true)
: this.toggleFormErrorMessage(false);
});
/**
* Form
*/
this.elmForm.addEventListener("submit", (_ev) => {
_ev.preventDefault();
if (!this.getIsFormError()) {
alert("Validate OK!");
// this.elmForm.submit();
}
});
}
init() {
if (!this.elmForm) return;
this.addEvent();
}
}
window.addEventListener("DOMContentLoaded", () => {
const formValidator = new FormValidator({
form: "#js-form",
targetInputs: "input[required], select[required]",
submitBtn: 'button[type="submit"]'
});
formValidator.init();
});
■HTML
<div class="wrap">
<h2>お問い合わせ</h2>
<form class="c-form" id="js-form">
<div class="c-form__error-message u-mb40" data-js-form-error-message style="display: none;">入力内容に不備があります<br>お手数ですが再度入力内容をご確認ください</div>
<div class="c-form-inputs">
<div class="c-form-inputs__group">
<label class="c-form-inputs__label"><span class="__required">必須</span>氏名</label>
<div class="c-form-inputs__input">
<input type="text" name="name" id="name" required placeholder="例)氏名 太郎" autocomplete="off">
<div class="c-form-inputs__error-message" data-js-error-message="name"></div>
</div>
</div>
<div class="c-form-inputs__group">
<label class="c-form-inputs__label"><span class="__required">必須</span>メールアドレス</label>
<div class="c-form-inputs__input">
<input type="email" name="email" id="email" required pattern="^[a-zA-Z0-9-_.]+@[a-zA-Z0-9-_.]+$" title="メールアドレスの形式で入力してください" placeholder="例)example@ex.com" autocomplete="off">
<div class="c-form-inputs__error-message" data-js-error-message="email"></div>
</div>
</div>
<div class="c-form-inputs__group">
<label class="c-form-inputs__label"><span class="__required">必須</span>電話番号</label>
<div class="c-form-inputs__input">
<input type="tel" name="tel" id="tel" required pattern="^0[0-9]{9,10}$" title="電話番号の形式で入力してください" placeholder="例)09012345678" autocomplete="off">
<div class="c-form-inputs__error-message" data-js-error-message="tel"></div>
<div class="c-form-inputs__caption"><small>ハイフン無しで入力してください</small></div>
</div>
</div>
<div class="c-form-inputs__group">
<label class="c-form-inputs__label"><span class="__required">必須</span>年齢</label>
<div class="c-form-inputs__input">
<select name="selectbox" id="selectbox" required data-required-error="いずれかを選択してください">
<option value="">選択してください</option>
<option value="1">10代</option>
<option value="2">20代</option>
<option value="3">30代</option>
<option value="4">40代</option>
<option value="5">50代</option>
<option value="6">60歳以上</option>
</select>
<div class="c-form-inputs__error-message" data-js-error-message="selectbox"></div>
</div>
</div>
<div class="c-form-inputs__group--checkbox">
<label class="c-form-inputs__label"><span class="__required">必須</span>雇用形態</label>
<div class="c-form-inputs__input">
<div class="l-flex">
<div>
<input type="checkbox" name="job" id="job-1" required data-required-error="1つ以上選択してください">
<label for="job-1">会社員</label>
</div>
<div>
<input type="checkbox" name="job" id="job-2" required data-required-error="1つ以上選択してください">
<label for="job-2">公務員</label>
</div>
<div>
<input type="checkbox" name="job" id="job-3" required data-required-error="1つ以上選択してください">
<label for="job-3">パート・アルバイト</label>
</div>
<div>
<input type="checkbox" name="job" id="job-4" required data-required-error="1つ以上選択してください">
<label for="job-4">自営業(経営者)</label>
</div>
<div>
<input type="checkbox" name="job" id="job-5" required data-required-error="1つ以上選択してください">
<label for="job-5">自営業(その他)</label>
</div>
<div>
<input type="checkbox" name="job" id="job-6" required data-required-error="1つ以上選択してください">
<label for="job-6">その他</label>
</div>
</div>
<div class="c-form-inputs__error-message" data-js-error-message="job"></div>
</div>
</div>
<div class="c-form-inputs__group--radio">
<label class="c-form-inputs__label"><span class="__required">必須</span>住居</label>
<div class="c-form-inputs__input">
<div class="l-flex">
<div>
<input type="radio" name="house" id="house-1" required data-required-error="1つ以上選択してください">
<label for="house-1">持ち家</label>
</div>
<div>
<input type="radio" name="house" id="house-2" required data-required-error="1つ以上選択してください">
<label for="house-2">賃貸</label>
</div>
<div>
<input type="radio" name="house" id="house-3" required data-required-error="1つ以上選択してください">
<label for="house-3">その他</label>
</div>
</div>
<div class="c-form-inputs__error-message" data-js-error-message="house"></div>
</div>
</div>
<div class="c-form-inputs__group">
<label class="c-form-inputs__label"><span class="__any">任意</span>任意項目</label>
<div class="c-form-inputs__input">
<textarea name="any"></textarea>
</div>
</div>
<div class="c-form-inputs__group">
<div class="u-w100p">
<div class="l-flex __col-center">
<input type="checkbox" name="privacy-policy" id="privacy-policy" required data-required-error="プライバシーポリシーに同意される場合、チェックを入れてください">
<label for="privacy-policy"><a class="u-link" href="#" target="_blank" rel="noreferrer noopener">プライバシーポリシー</a>に同意する</label>
</div>
<div class="l-flex __col-center">
<div class="c-form-inputs__error-message" data-js-error-message="privacy-policy"></div>
</div>
</div>
</div>
</div>
<div class="c-form-submit">
<button class="c-btn" type="submit">送信する</button>
</div>
<div class="c-form__error-message u-mt40" data-js-form-error-message style="display: none;">入力内容に不備があります<br>お手数ですが再度入力内容をご確認ください</div>
</form>
</div>
■CSS
@charset "UTF-8";
* {
box-sizing: border-box;
}
/* webkit specific styles */
input[type=color]::-webkit-color-swatch {
border: none;
}
input[type=color]::-webkit-color-swatch-wrapper {
padding: 0;
}
html,
body,
div,
span,
object,
iframe,
h1,
h2,
h3,
h4,
h5,
h6,
p,
blockquote,
pre,
abbr,
address,
cite,
code,
del,
dfn,
em,
img,
ins,
kbd,
q,
samp,
small,
strong,
sub,
sup,
var,
b,
i,
dl,
dt,
dd,
ol,
ul,
li,
fieldset,
form,
label,
legend,
table,
caption,
tbody,
tfoot,
thead,
tr,
th,
td,
article,
aside,
canvas,
details,
figcaption,
figure,
footer,
header,
hgroup,
menu,
nav,
section,
summary,
time,
mark,
audio,
video {
margin: 0;
padding: 0;
border: 0;
outline: 0;
font-size: 100%;
vertical-align: baseline;
background: transparent;
font-weight: inherit;
}
body {
line-height: 1;
}
article,
aside,
details,
figcaption,
figure,
footer,
header,
hgroup,
menu,
nav,
section {
display: block;
}
nav ul {
list-style: none;
}
blockquote,
q {
quotes: none;
}
blockquote:before,
blockquote:after,
q:before,
q:after {
content: "";
content: none;
}
a {
margin: 0;
padding: 0;
font-size: 100%;
vertical-align: baseline;
background: transparent;
}
/* change colours to suit your needs */
mark {
background-color: #ff9;
color: #000;
font-style: italic;
font-weight: bold;
}
del {
text-decoration: line-through;
}
abbr[title],
dfn[title] {
border-bottom: 1px dotted;
cursor: help;
}
table {
border-collapse: collapse;
border-spacing: 0;
}
/* change border colour to suit your needs */
hr {
display: block;
height: 1px;
border: 0;
border-top: 1px solid #cccccc;
margin: 1em 0;
padding: 0;
}
input,
select {
vertical-align: middle;
}
input:focus {
outline: none;
}
ul,
ol {
list-style-type: none;
}
body {
-ms-text-size-adjust: 100%;
-webkit-text-size-adjust: 100%;
}
main {
display: block;
}
li {
list-style: none;
}
h1,
h2,
h3,
h4,
h5,
h6 {
font-size: 16px;
}
a {
color: inherit;
width: 100%;
text-decoration: none;
}
a:hover {
color: inherit;
}
img {
vertical-align: middle;
max-width: 100%;
height: auto;
}
address {
font-style: normal;
}
sup {
vertical-align: super;
font-size: smaller;
}
input,
button,
textarea,
select {
display: block;
color: #333;
font-size: 16px;
background: none;
border: none;
border-radius: 0;
outline: none;
width: 100%;
margin: 0;
padding: 0;
-webkit-appearance: none;
-moz-appearance: none;
appearance: none;
-moz-appearance: textfield;
}
input:-webkit-autofill,
button:-webkit-autofill,
textarea:-webkit-autofill,
select:-webkit-autofill {
color: #333;
-webkit-box-shadow: 0 0 0px 1000px #fff inset;
}
input::-moz-placeholder, button::-moz-placeholder, textarea::-moz-placeholder, select::-moz-placeholder {
color: #b5b5b6;
}
input:-ms-input-placeholder, button:-ms-input-placeholder, textarea:-ms-input-placeholder, select:-ms-input-placeholder {
color: #b5b5b6;
}
input::placeholder,
button::placeholder,
textarea::placeholder,
select::placeholder {
color: #b5b5b6;
}
input::-webkit-outer-spin-button, input::-webkit-inner-spin-button,
button::-webkit-outer-spin-button,
button::-webkit-inner-spin-button,
textarea::-webkit-outer-spin-button,
textarea::-webkit-inner-spin-button,
select::-webkit-outer-spin-button,
select::-webkit-inner-spin-button {
-webkit-appearance: none;
margin: 0;
}
input,
textarea,
select {
line-height: 1.5;
background-color: #fff;
border: solid 1px #d5d5d5;
border-radius: 3px;
padding: 10px 12px;
}
input[readonly],
textarea[readonly],
select[readonly] {
color: rgba(51, 51, 51, 0.7);
border: none;
border-radius: 0;
}
textarea {
min-height: 100px;
}
select::-ms-expand {
display: none;
}
button {
cursor: pointer;
}
input[type=checkbox], input[type=radio] {
position: absolute;
white-space: nowrap;
width: 1px;
height: 1px;
border: 0;
margin: -1px;
padding: 0;
overflow: hidden;
clip: rect(0 0 0 0);
-webkit-clip-path: inset(50%);
clip-path: inset(50%);
}
input[type=radio] + label {
display: block;
font-size: 14px;
margin-right: 20px;
margin-bottom: 4px;
padding-left: 28px;
position: relative;
cursor: pointer;
}
input[type=radio] + label::before {
content: "";
display: block;
width: 20px;
height: 20px;
background-color: #f5f5f5;
border: solid 1px #d5d5d5;
border-radius: 50%;
position: absolute;
top: 0;
left: 0;
}
input[type=radio]:checked + label::before {
border: solid 1px #014c86;
}
input[type=radio] + label::after {
content: "";
display: none;
width: 13.5px;
height: 13.5px;
background-color: #014c86;
border-radius: 50%;
position: absolute;
top: 4px;
left: 4px;
}
input[type=radio]:checked + label::after {
display: block;
}
input[type=checkbox] + label {
display: block;
font-size: 14px;
margin-right: 20px;
margin-bottom: 4px;
padding-left: 28px;
position: relative;
cursor: pointer;
}
input[type=checkbox] + label::before {
content: "";
display: block;
width: 20px;
height: 20px;
background-color: #f5f5f5;
border: solid 1px #d5d5d5;
border-radius: 2px;
position: absolute;
top: 0;
left: 0;
}
input[type=checkbox]:checked + label::before {
background-color: #014c86;
border-color: #014c86;
}
input[type=checkbox] + label::after {
content: "";
display: none;
width: 6px;
height: 10px;
border: solid #fff;
border-width: 0 2px 2px 0;
position: absolute;
top: 3px;
left: 7px;
transform: rotate(45deg);
}
input[type=checkbox]:checked + label::after {
display: block;
}
select {
cursor: pointer;
padding-right: 32px;
background-repeat: no-repeat;
background-position: right 8px center;
background-size: 10px 10px;
background-image: url(data:image/png;base64,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);
}
body,
input,
button,
textarea,
select {
color: #333;
font-size: 16px;
font-family: "游ゴシック Medium", "游ゴシック体", "Yu Gothic Medium", YuGothic, "ヒラギノ角ゴ ProN", "Hiragino Kaku Gothic ProN", "メイリオ", Meiryo, "MS Pゴシック", "MS PGothic", sans-serif;
}
.wrap {
line-height: 1.7;
padding: 40px 80px 80px;
}
.wrap h2 {
font-size: 40px;
font-weight: bold;
text-align: center;
margin-bottom: 64px;
}
.l-flex {
display: flex;
flex-wrap: wrap;
}
.l-flex.__col-center {
justify-content: center;
}
.l-flex.__col-between {
justify-content: space-between;
}
.l-flex.__col-start {
justify-content: flex-start;
}
.l-flex.__col-end {
justify-content: flex-end;
}
.c-form {
max-width: 600px;
margin: auto;
}
.c-form__error-message {
color: #e63444;
font-size: 13px;
text-align: center;
background-color: rgba(230, 52, 68, 0.1);
border: solid 1px #e63444;
border-radius: 2px;
padding: 12px 12px;
}
.c-form-inputs__group, .c-form-inputs__group--radio, .c-form-inputs__group--checkbox {
display: flex;
align-items: flex-start;
}
.c-form-inputs__group:not(:first-of-type), .c-form-inputs__group--radio:not(:first-of-type), .c-form-inputs__group--checkbox:not(:first-of-type) {
margin-top: 40px;
}
@media screen and (max-width: 750px) {
.c-form-inputs__group, .c-form-inputs__group--radio, .c-form-inputs__group--checkbox {
display: block;
}
}
.c-form-inputs__label {
flex: 0 0 auto;
display: flex;
align-items: center;
font-size: 15px;
width: 100%;
max-width: 200px;
}
@media screen and (max-width: 750px) {
.c-form-inputs__label {
max-width: 100%;
}
}
.c-form-inputs__label .__required,
.c-form-inputs__label .__any {
display: inline-block;
font-size: 12px;
border-radius: 2px;
margin-right: 8px;
padding: 2px 6px;
}
.c-form-inputs__label .__required {
color: #fff;
background-color: #e63444;
}
.c-form-inputs__label .__any {
background-color: #f5f5f5;
}
.c-form-inputs__input {
width: 100%;
padding-left: 40px;
}
@media screen and (max-width: 750px) {
.c-form-inputs__input {
margin-top: 10px;
padding-left: 0;
}
}
.c-form-inputs__input input.__error,
.c-form-inputs__input textarea.__error,
.c-form-inputs__input select.__error {
background-color: rgba(230, 52, 68, 0.1);
border: solid 1px #e63444;
}
.c-form-inputs__input input.__secure,
.c-form-inputs__input textarea.__secure,
.c-form-inputs__input select.__secure {
background-color: rgba(31, 218, 80, 0.05);
border: solid 1px #1fda50;
}
.c-form-inputs__error-message {
color: #e63444;
font-size: 12px;
margin-top: 4px;
}
.c-form-inputs__caption {
color: #939597;
font-size: 12px;
margin-top: 4px;
}
.c-form-submit {
max-width: 200px;
margin: 64px auto 0;
}
.c-btn {
color: #fff;
background-color: #014c86;
border-radius: 2px;
padding: 16px 20px;
transition: opacity 0.2s;
}
.c-btn:hover {
opacity: 0.7;
transition: opacity 0.2s;
}
.u-link {
color: #014c86;
text-decoration: underline;
}
.u-link:hover {
text-decoration: none;
}
.u-w100p {
width: 100% !important;
}
.u-mt40 {
margin-top: 40px !important;
}
.u-mb40 {
margin-bottom: 40px !important;
}
解説
まず、上記サンプルコードで重要なのはJavaScriptだけです。
HTMLは参考程度に、CSSはバリデーションの挙動には一切関係ありません。
あくまで見た目を整えているだけです。
(CSSは、CodePenのSassをエクスポートしたものを上記に載せています)
では、JavaScriptを中心に解説をしていきます。
サンプルコードを見ながら解説を読むと分かりやすいと思います。
大まかな流れ
- class FormValidator {...} でクラスを定義
- addEventListener('DOMContentLoaded... でクラスをインスタンス化してinit()を実行
(インスタンス化の際に、フォームの情報を渡す)
class FormValidatorでは、以下のような流れで処理をしています。
- constructor()でプロパティを定義
- エラーかどうかをチェックするメソッド、エラーメッセージを生成するメソッドなどを定義
- addEvent()でformやinputのイベントに「2.」のメソッドを設定
constructor() - オブジェクト生成時に実行されるメソッド
constructor(_parm) {
this.elmForm = document.querySelector(_parm.form) || false;
this.elmTargetInputs = [...this.elmForm.querySelectorAll(_parm.targetInputs)];
this.elmSubmitBtn = this.elmForm.querySelector(_parm.submitBtn);
this.elmFormErrorMessages = [...this.elmForm.querySelectorAll('[data-js-form-error-message]')];
this.classErrorInput = _parm.classErrorInput || '__error';
this.classSecureInput = _parm.classSecureInput || '__secure';
this.attrElmErrorMessage = _parm.attrElmErrorMessage || 'data-js-error-message';
this.attrRequiredErrorMessage = _parm.attrRequiredErrorMessage || 'data-required-error';
this.defaultErrorMessage = _parm.defaultErrorMessage || '必須項目を入力してください';
const createInputStatuses = this.elmTargetInputs.map((_item) => {
let result = [];
result['name'] = _item.getAttribute('name');
result['isError'] = true;
return result;
});
this.inputStatuses = createInputStatuses.filter(
(_item, _index, _self) => _self.findIndex((_ev) => _ev.name === _item.name) === _index
);
this.isFirstSubmit = false;
}
constructor()はオブジェクト生成時に実行される予め定義されたメソッドです。
フォームやinputなどの要素、エラー時に付与するclass、処理に必要な属性名、デフォルトのエラーメッセージ、各inputがエラーかどうかを確認するためのオブジェクトなどをプロパティとして定義しています。
これらのプロパティの値はインスタンス化する際、引数に値を渡すことで変更できます。
実際にどのような値が定義されているかは、constructor()内でconsole.log(this.elmForm);のように実行して確認してみてください。
this.inputStatusesは少しだけ複雑なので解説します。
まず、バリデーション対象となるinput要素やselect要素などを取得して、createInputStatuses変数に連想配列として格納します。
const createInputStatuses = this.elmTargetInputs.map((_item) => {
let result = [];
result['name'] = _item.getAttribute('name');
result['isError'] = true;
return result;
});
keyは2つあり、nameはバリデーション対象のname属性、isErrorはバリデーションエラーかどうかを確認するための値になります。
しかし、このままではチェックボックスやラジオボタンの要素が重複して連想配列に格納されています。
なので、重複している要素をfilter()を使って削除して、それをthis.inputStatusesに定義しています。
createInputStatusesとthis.inputStatusesをconsole.logで見れば、どういう処理をしているかイメージしやすいと思います。
getIsFormError() - フォームのバリデーションエラーを確認するメソッド
getIsFormError() {
return this.inputStatuses.every((_item) => _item['isError'] === false) ? false : true;
}
input要素がひとつでもバリデーションエラーの場合、trueを返します。
every()を使ってtrue, falseを出し分けています。
getInputType() - input要素の種類を確認するメソッド
getInputType(_elmInput) {
if (_elmInput.tagName === 'SELECT') return 'select';
if (_elmInput.getAttribute('type').match(/checkbox|radio/)) return 'checkOrRadio';
return 'input';
}
バリデーションを実行する際、通常のテキストボックス、チェックボックスやラジオボタン、セレクトボックスで処理を多少変えなければいけません。
その処理を出し分けるために、このメソッドを使って要素の種類を取得します。
引数にinput要素を与えて、input, checkOrRadio, selectのいずれかの文字列を返します。
errorCheck() - バリデーションエラーをチェックするメソッド
errorCheck(_elmInput) {
const patternValidate = _elmInput.getAttribute('pattern') || false;
const inputType = this.getInputType(_elmInput);
// セレクトボックスの場合
if (inputType === 'select')
return !_elmInput.validity.patternMismatch && _elmInput.value.length ? false : true;
// チェックボックスかラジオボタンの場合
if (inputType === 'checkOrRadio') return _elmInput.checked ? false : true;
// input(パターン有り)の場合
if (patternValidate)
return !_elmInput.validity.patternMismatch && _elmInput.value.length ? false : true;
// input(パターン無し)の場合
return _elmInput.validity.valueMissing;
}
引数にinput要素を与えて、その要素がバリデーションエラーになるかどうかを確認するメソッドです。
HTML属性のrequiredとpatternをチェックして、エラーならtrueを返します。
このメソッドはエラーかどうかを確認するだけで、エラーメッセージを表示したりするのは別のメソッドで行っています。
createInputErrorMessage() - エラーメッセージを描画するメソッド
引数にinput要素を与えて、それに紐づくエラーメッセージ要素にエラーメッセージを描画します。
input要素とエラーメッセージ要素は以下のように、「nameの属性値とdata-js-error-messageの属性値」で紐付けておきます。
<input type="text" name="username" required>
<div data-js-error-message="username"></div>
また、requiredとpatternどちらのエラーなのかを判別して、エラーメッセージの出し分けもしています。
■エラーメッセージのカスタマイズ
ケース | 解説 |
個別でrequiredのエラーメッセージを変更する | input要素にdata-required-error属性を追加して、値にエラーメッセージを記述する。 |
全体のrequiredのデフォルトエラーメッセージを変更する | this.defaultErrorMessageで変更する |
patternのエラーメッセージを変更する | input要素のtitle属性の値にエラーメッセージを記述する (サンプルのHTMLが参考になります) |
toggleFormErrorMessage() - フォーム全体のエラーメッセージの表示を切り替えるメソッド
toggleFormErrorMessage(_show) {
this.elmFormErrorMessages.forEach((_elmFormErrorMessage) => {
_show
? (_elmFormErrorMessage.style.display = 'block')
: (_elmFormErrorMessage.style.display = 'none');
});
}
以下のように、input要素のいずれかがエラーの場合に表示されるメッセージを制御するメソッドです。
予めエラーメッセージ要素を用意しておき、style属性のdisplayで表示非表示を切り替えているだけのシンプルな処理です。
errorReset() - バリデーションエラーをリセットするメソッド
errorReset(_elmInput) {
const name = _elmInput.getAttribute('name');
const elmErrorMessage = this.elmForm.querySelector(`[${this.attrElmErrorMessage}="${name}"]`);
_elmInput.classList.remove(this.classErrorInput);
_elmInput.classList.remove(this.classSecureInput);
elmErrorMessage.textContent = '';
return;
}
input要素のエラーをリセットするメソッドです。
エラー時には、要素にエラー用のclassを付与したり、エラーメッセージを表示させたりしているので、それらがリセットされます。
validate() - バリデーションの処理を「実行する」メソッド
validate(_elmInput) {
// エラーリセット
this.errorReset(_elmInput);
// 必要な変数定義
const isError = this.errorCheck(_elmInput);
const targetInputStatus = this.inputStatuses.find((_item) => {
return _item.name === _elmInput.getAttribute('name');
});
const changeInputStatusArray = (_isErrorValue) => {
targetInputStatus['isError'] = _isErrorValue;
};
// チェックボックスかラジオボタンの場合、いずれかがチェックされていればエラーにしない
if (this.getInputType(_elmInput) === 'checkOrRadio') {
const ElmsCheckOrRadio = [
...document.querySelectorAll(`input[name="${targetInputStatus['name']}"]`),
];
const getIsAnyChecked = () => {
return ElmsCheckOrRadio.some((_elm) => {
return _elm.checked;
});
};
const toggleAllCheckOrRadioRequired = (_value) => {
ElmsCheckOrRadio.forEach((_elm) => {
_elm.required = _value;
});
};
if (getIsAnyChecked()) {
toggleAllCheckOrRadioRequired(false);
changeInputStatusArray(false);
} else {
toggleAllCheckOrRadioRequired(true);
changeInputStatusArray(true);
this.createInputErrorMessage(_elmInput);
}
return;
}
// バリデーションチェックやエラーメッセージの描画などを実行
if (isError) {
_elmInput.classList.add(this.classErrorInput);
changeInputStatusArray(true);
this.createInputErrorMessage(_elmInput);
} else {
changeInputStatusArray(false);
_elmInput.classList.add(this.classSecureInput);
}
return;
}
これまで様々なメソッドを解説してきましたが、それらを実行しているメソッドがこのvalidate()になります。
最終的には、このメソッドをフォームやinput要素のイベントに登録して、入力時や送信時にバリデーションを実行します。
このメソッドは、あくまで1つのinput要素に対して実行します。
つまり、input要素が5つある場合、5回このメソッドが実行されます。
このメソッドで行っている処理のおおまかな流れは以下の通りです。
■ input要素がチェックボックスかラジオボタンの場合
- エラーをリセット
- エラーかどうかを確認
- いずれかにチェックが入っていればエラーにはせず、処理は終了
- エラーの場合、エラーメッセージを表示させたり、this.inputStatusesのisErrorをtrueにするなどのエラー処理を行う
■ input要素が上記以外の場合
- エラーをリセット
- エラーかどうかを確認
- エラーの場合、エラーメッセージを表示させたり、this.inputStatusesのisErrorをtrueにするなどのエラー処理を行う
addEvent() - イベントを設定するメソッド
addEvent() {
/**
* Input
*/
this.elmTargetInputs.forEach((_elmTargetInput) => {
// 入力時
_elmTargetInput.addEventListener('change', (_ev) => {
this.validate(_elmTargetInput);
this.isFirstSubmit
? this.getIsFormError()
? this.toggleFormErrorMessage(true)
: this.toggleFormErrorMessage(false)
: false;
});
// エラー時
_elmTargetInput.addEventListener('invalid', (_ev) => {
this.validate(_elmTargetInput);
});
});
/**
* SubmitBtn
*/
this.elmSubmitBtn.addEventListener('click', (_ev) => {
this.isFirstSubmit = true;
this.getIsFormError()
? this.toggleFormErrorMessage(true)
: this.toggleFormErrorMessage(false);
});
/**
* Form
*/
this.elmForm.addEventListener('submit', (_ev) => {
_ev.preventDefault();
if (!this.getIsFormError()) {
alert('Validate OK!');
// this.elmForm.submit();
}
});
}
フォームやinput要素のイベントにvalidate()メソッドを設定するメソッドです。
サンプルコードでは分かりやすくするために、送信時にエラーが無かったらアラートが出るようにしています。
実運用時には、// this.elmForm.submit();のコメントアウトを解除して、アラートの記述は削除します。
init() - 初期化用メソッド
init() {
if (!this.elmForm) return;
this.addEvent();
}
クラスをインスタンス化させた際、最初に実行するメソッドです。
フォームが存在しなかったら処理は何も行われません。
最後にクラスをインスタンス化させる
window.addEventListener("DOMContentLoaded", () => {
const formValidator = new FormValidator({
form: "#js-form",
targetInputs: "input[required], select[required]",
submitBtn: 'button[type="submit"]'
});
formValidator.init();
});
addEventListenerのDOMContentLoadedで、ページ読み込み完了時にインスタンス化させます。
その際、コンスタラクタ内のプロパティを設定します。
const formValidator = new FormValidator({
form: "#js-form",
targetInputs: "input[required], select[required]",
submitBtn: 'button[type="submit"]'
});
上記の値3つ(form, targetInputs, submitBtn)は必ず設定します。
インスタンス化したら、最後にformValidator.init();を実行して準備は完了です。
これでフォームにバリデーションが設定されます。
まとめ
以上で、素のJavaScriptでフォームのバリデーションを実装する方法の解説は終了です。
クラス構文に馴染みが無いと理解するのがかなり大変だと思いますが、メソッドをひとつひとつ確認していけば理解できるはずです。
前述でも紹介しましたが、以下2つの記事を読めば、クラス構文(オブジェクト指向)がなんとなく理解できるようになるので、興味があるかたはぜひご一読ください。