<body>
<form id="form" novalidate>
<p class="wrapper" data-wrapper>
<label>
<span>必須項目: </span>
<input type="text" required>
</label>
<span class="errorMessage"></span>
</p>
<p class="wrapper" data-wrapper>
<label>
<span>number: </span>
<input type="number" required>
</label>
<span class="errorMessage"></span>
</p>
<p class="wrapper" data-wrapper>
<label>
<span>最小5, 最大10文字: </span>
<input type="text" required minlength="5" maxlength="10">
</label>
<span class="errorMessage"></span>
</p>
<p class="wrapper" data-wrapper>
<label>
<span>[banana]or[cherry]: </span>
<input type="text" title="「banana」もしくは「cherry」を入力してください。" required pattern="[Bb]anana|[Cc]herry">
</label>
<span class="errorMessage"></span>
</p>
<p class="wrapper" data-wrapper>
<label>
<span>email: </span>
<input type="email" name="email" required maxlength="10">
</label>
<span class="errorMessage"></span>
</p>
<p class="wrapper" data-wrapper>
<label>
<span>URL: </span>
<input type="url" required>
</label>
<span class="errorMessage"></span>
</p>
<p class="wrapper" data-wrapper>
<label>
<span>カスタムルール("1"のみ許可): </span>
<input type="text" data-custom-rule="test" title="1を入力してください。">
</label>
<span class="errorMessage"></span>
</p>
<p class="wrapper" data-wrapper>
<label>
<span>ラジオ1</span>
<input type="radio" name="radio" value="1" required>
</label>
<label>
<span>ラジオ2</span>
<input type="radio" name="radio" value="2" required>
</label>
<label>
<span>ラジオ3</span>
<input type="radio" name="radio" value="3" title="必須項目です。" required>
</label>
<span class="errorMessage"></span>
</p>
<p class="wrapper" data-wrapper>
<label>
<span>チェックボックス: </span>
<input type="checkbox" name="check" value="1" title="必須項目です。" required>
</label>
<span class="errorMessage"></span>
</p>
<p class="wrapper" data-wrapper>
<label>
<span>select: </span>
<select name="select" required>
<option selected disabled></option>
<option value="01">選択肢01</option>
<option value="02">選択肢02</option>
<option value="03">選択肢03</option>
<option value="04">選択肢04</option>
</select>
</label>
<span class="errorMessage"></span>
</p>
<p>
<button>送信</button>
</p>
</form>
</body>
.errorMessage {
display: none;
color: red;
font-size: 12px;
}
.wrapper.is-error .errorMessage {
display: block;
}
.wrapper.is-error :is(label, span) {
color: red;
}
.wrapper.is-error :is(input, select) {
border-color: red;
background-color: rgba(200, 0, 0, 0.1);
}
(() => {
class Validate {
/** @type {HTMLFormElement} */
#form;
/** @type {Array<HTMLInputElement | HTMLSelectElement | HTMLTextAreaElement>} */
#formElms;
/** @type {Array<HTMLInputElement | HTMLSelectElement | HTMLTextAreaElement>} */
#customRuleFormElms;
/** @param {HTMLFormElement} form */
constructor(form) {
this.#form = form;
this.#formElms = [...this.#form.elements].filter(
(elm) => elm instanceof HTMLInputElement || elm instanceof HTMLSelectElement || elm instanceof HTMLTextAreaElement
);
this.#customRuleFormElms = this.#formElms.filter((elm) => elm.hasAttribute('data-custom-rule'));
}
/**
* @summary `#getElms`メソッドの返り値の型を定義
* @typedef {Object} ReturnElms
* @property {Element} wrapper - 入力項目ラッパー要素
* @property {Element} errorMessage - エラーメッセージ要素
*/
/**
* 引数から受け取った入力項目に関連する要素を取得する
* @param {HTMLInputElement | HTMLSelectElement | HTMLTextAreaElement} input 入力項目
* @returns {ReturnElms}
*/
#getElms(input) {
const wrapper = input.closest('[data-wrapper]');
const errorMessage = wrapper?.querySelector('.errorMessage');
if (!wrapper || !errorMessage) {
return console.warn(new Error());
}
return { wrapper, errorMessage };
}
/**
* 入力項目下部にエラーメッセージを表示
* @param {HTMLInputElement | HTMLSelectElement | HTMLTextAreaElement} input 入力項目
* @param {string} message エラーメッセージ
*/
#displayErrorState(input, message) {
const { wrapper, errorMessage } = this.#getElms(input);
wrapper.classList.add('is-error');
errorMessage.textContent = message || input.validationMessage;
}
/**
* 引数から受け取った入力項目のエラー状態をクリア
* @param {HTMLInputElement | HTMLSelectElement | HTMLTextAreaElement} input 入力項目
*/
#resetErrorState(input) {
const { wrapper, errorMessage } = this.#getElms(input);
wrapper.classList.remove('is-error');
errorMessage.textContent = '';
input.setCustomValidity('');
}
/**
* カスタムルール
* @param {HTMLInputElement | HTMLSelectElement | HTMLTextAreaElement} input 入力項目
*/
checkCustomValidation(input) {
if (input.getAttribute('data-custom-rule') === 'test') {
if (input.value !== '' && input.value !== '1') {
input.setCustomValidity(input.title);
} else {
input.setCustomValidity('');
}
}
}
/**
* 引数に渡された要素にバリデーションを実行する
* @param {HTMLInputElement | HTMLSelectElement | HTMLTextAreaElement} input 入力項目
*/
#validateTargetInput(input) {
this.#resetErrorState(input);
// カスタムルールの対象の場合
if (this.#customRuleFormElms.includes(input)) {
this.checkCustomValidation(input);
}
input.checkValidity();
}
/**
* `invalid`時のハンドラ関数
* - エラーメッセージを表示
* @param {Event} e
*/
#handleInvalid(e) {
/** @type {HTMLInputElement | HTMLSelectElement | HTMLTextAreaElement} */
const input = e.currentTarget;
if (input.validity.valueMissing) {
// input.setCustomValidity('必須入力項目です。');
this.#displayErrorState(input, input.validationMessage);
} else if (input.validity.patternMismatch) {
input.setCustomValidity(input.title);
this.#displayErrorState(input, input.validationMessage);
} else {
this.#displayErrorState(input, input.validationMessage);
}
}
/**
* フォーム送信時のハンドラ関数
* @param {Event} e
*/
#handleSubmit(e) {
e.preventDefault();
this.#formElms.forEach((formElm) => this.#validateTargetInput(formElm));
const isValid = this.#form.checkValidity();
if (!isValid) {
const firstInvalidInput = this.#formElms.find((formElm) => !formElm.validity.valid);
firstInvalidInput.focus();
return;
}
// 以降validの場合
alert('valid!');
}
/** @summary 各種ハンドラの登録 */
#registerEventListener() {
this.#formElms.forEach((formElm) => {
// チェックボックス・ラジオボタン・セレクトボックスの場合は`change`イベント時に再検証発火
if (formElm.type === 'radio' || formElm.type === 'checkbox' || formElm instanceof HTMLSelectElement) {
formElm.addEventListener('change', (e) => this.#validateTargetInput(e.currentTarget));
} else {
formElm.addEventListener('blur', (e) => this.#validateTargetInput(e.currentTarget));
}
formElm.addEventListener('invalid', (e) => this.#handleInvalid(e));
});
this.#form.addEventListener('submit', (e) => this.#handleSubmit(e), { passive: false });
}
init() {
if (!this.#formElms.length) return;
this.#registerEventListener();
}
}
window.addEventListener('DOMContentLoaded', () => {
/** @type {HTMLFormElement} */
const form = document.getElementById('form');
const validateInstance = new Validate(form);
validateInstance.init();
});
})();
This Pen doesn't use any external CSS resources.
This Pen doesn't use any external JavaScript resources.