初めに
htmlのカスタムデータ属性( data-* )の学習のため、 data-* とTypeScriptを使い、フォームに文字を入力すると、リアルタイムにエラーかどうか判定するプログラムを作成しました。
カスタムデータ属性
htmlのタグにつけられる属性にはidやclass、hrefなど様々なものがありますが、カスタムデータ属性を使用するとオリジナルの属性をつけることができます。
属性名は先頭に**data-**がついていれば自由に記述できます。1
<div id="my" data-foo="bar"></div>
カスタムデータ属性は要素のdatasetプロパティからアクセスすることができます。
const myDiv = document.getElementById('my');
console.log(myDiv.dataset.foo); // bar
console.log('foo' in myDiv.dataset); // true
カスタムデータ属性を使ったバリデーション
カスタムデータ属性にバリデーションを記述します。
<input type="password" id="password" class="form-control"
data-required
data-pattern="^[0-9A-Za-z]+$"
data-maxlength="12"
data-minlength="6">
const rules = {
'required': el => {
return el.value !== '';
},
'pattern': el => {
return (new RegExp(el.dataset.pattern)).test(el.value);
},
// =====================================================
// 以下略
// =====================================================
};
HTML
bootstrapのCDNを利用しました。
inputタグのカスタムデータ属性にバリデーションのルールを記述します。
警告テキストをliタグで表示します。
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css" rel="stylesheet">
<link href="style.css" rel="stylesheet">
<title></title>
</head>
<body>
<div class="container">
<form class="form-horizontal">
<h3>Real time validation</h3>
<div class="form-group validation-target">
<label for="name" class="control-label col-xs-4">名前</label>
<div class="col-xs-8">
<input type="text"
id="name"
class="form-control"
data-required="true"
data-maxlength="8">
<ul>
<li class=" text-danger" data-required>名前は必須です。</li>
<li class="text-danger" data-maxlength>名前は8文字以内で入力してください。</li>
</ul>
</div>
</div>
<div class="form-group validation-target">
<label for="email" class="control-label col-xs-4">メールアドレス</label>
<div class="col-xs-8">
<input type="email"
id="email"
class="form-control"
data-required="true"
data-pattern="^[a-zA-Z0-9.!#$%&'*+/=?^_`{|}~-]+@[a-zA-Z0-9-]+(?:\.[a-zA-Z0-9-]+)*$">
<ul>
<li class="text-danger" data-required>メールアドレスは必須です。</li>
<li class="text-danger" data-pattern>メールアドレスは正しい形式で入力してください。</li>
</ul>
</div>
</div>
<div class="form-group validation-target">
<label for="password" class="control-label col-xs-4">パスワード</label>
<div class="col-xs-8">
<input type="password"
id="password"
class="form-control"
data-pattern="^[0-9A-Za-z]+$"
data-maxlength="12"
data-minlength="6"
data-required="true">
<ul>
<li class="text-danger" data-required>パスワードは必須です。</li>
<li class="text-danger" data-maxlength>パスワードは12文字以内で入力してください。</li>
<li class="text-danger" data-minlength>パスワードは6文字以上で入力してください。</li>
<li class="text-danger" data-pattern>パスワードは半角英数字で入力してください。</li>
</ul>
</div>
</div>
<div class="form-group validation-target">
<label for="age" class="control-label col-xs-4">年齢</label>
<div class="col-xs-8">
<input type="number"
id="age"
class="form-control"
data-min="0"
data-max="150"
data-pattern="^[0-9]*$">
<ul>
<li class="text-danger" data-pattern>年齢は整数で入力してください。</li>
<li class="text-danger" data-min>年齢は0歳以上で入力してください。</li>
<li class="text-danger" data-max>年齢は150歳以下で入力してください。</li>
</ul>
</div>
</div>
<div class="form-group validation-target">
<label for="program-language" class="control-label col-xs-4">好きなプログラミング言語</label>
<div class="col-xs-8">
<input type="text"
id="program-language"
class="form-control"
list="program-list"
data-required="tru"
data-pattern="JavaScript|Ruby|Python|PHP|Swift|Java|C#|その他">
<datalist id="program-list">
<option>JavaScript</option>
<option>Ruby</option>
<option>Python</option>
<option>PHP</option>
<option>Swift</option>
<option>Java</option>
<option>C#</option>
<option>その他</option>
</datalist>
<ul>
<li class="text-danger" data-required>プログラミング言語の選択は必須です。</li>
<li class="text-danger" data-pattern>プログラミング言語は選択肢から選択してください。</li>
</ul>
</div>
</div>
<div class="form-group">
<div class="col-xs-offset-4 col-xs-8">
<div class="checkbox">
<label>
<input type="checkbox">remember
</label>
</div>
</div>
</div>
<div class="form-group">
<div class="col-xs-offset-4 col-xs-8">
<button type="submit" class="btn btn-default" formnovalidate>送信</button>
</div>
</div>
</form>
</div>
<script type="text/javascript" src="script.js"></script>
</body>
</html>
以下はフォントサイズの調整と警告テキストをデフォルトで非表示にするCSSです。
.container {
font-size: 12px;
}
.text-danger {
display: none;
}
##JavaScript
静的型付けのAltJS2であるTypeScript3を使いました。
####バリデーションを行うクラスを作る
class AppModel {
// 検証対象の要素 --- (※1)
private _el: HTMLInputElement;
// 各カスタムデータ属性名をキーにし、対応した検証メソッドを格納したMapオブジェクト --- (※2)
private _Rules = new Map<string, Function>()
.set('required', (el: HTMLInputElement): boolean => {
return el.value.trim() !== '';
})
.set('maxlength', (el: HTMLInputElement): boolean => {
return el.value.length <= Number(el.dataset.maxlength);
})
.set('minlength', (el: HTMLInputElement): boolean => {
return el.value.length >= Number(el.dataset.minlength);
})
.set('pattern', (el: HTMLInputElement): boolean => {
const ptn = el.dataset.pattern;
return (ptn !== undefined) ? new RegExp(ptn).test(el.value) : true;
})
.set('max', (el: HTMLInputElement): boolean => {
return Number(el.value) <= Number(el.dataset.max);
})
.set('min', (el: HTMLInputElement): boolean => {
return Number(el.value) >= Number(el.dataset.min)
});
constructor(el: HTMLInputElement) {
this._el = el;
}
// 要素に対し各検証を実行し、エラーとなったキーの配列を返す。 --- (※3)
public validate(): string[] {
const errors: string[] = [];
for (let [key, func] of Array.from(this._Rules.entries())) {
if (!(key in this._el.dataset))
continue;
if (!func(this._el))
errors.push(key);
}
return errors;
}
}
_rules(※2)はhtml側のカスタムデータ属性名をキー、キーに対応した検証用の関数をバリューとしたMapオブジェクトです。
validateメソッド(※3)では対象の要素(※1)に対し検証メソッドを順に実行しエラーとなったキー(カスタムデータ属性名)の配列を返します。検証対象のカスタムデータ属性にないキーは実行されないようにしました。
_rulesをMapでなく連想配列にする場合インターフェイスが必要です。
(noImplicitAny / 暗黙のany型を許容しない場合)
インターフェイスを定義しないとブラケット表記法(Object[key] ← Object.keyと同じ)のキーに変数を指定した場合、「オブジェクト側のキーの型が定義されていない」とコンパイルエラーとなります。
// Mapを使わない場合
interface IRules {
[index: string]: Function // キーが文字列、バリューが関数と定義
}
class AppModel {
private _el: HTMLInputElement;
private _Rules: IRules = { // IRulesを実装
'required': (el: HTMLInputElement): boolean => {
return el.value.trim() !== '';
},
// 略
}
// 要素に対し各検証を実行し、エラーとなったキーの配列を返す。
public validate(): string[] {
return Object.keys(this._Rules).filter(key => { // filter関数でエラーとなったキーのみ抽出
return (key in this._el.dataset) // keyがカスタムデータ属性に含まれる場合検証
? !(this._Rules[key](this._el)) // interfaceを定義しない場合ここでエラー
: false;
})
}
}
画面表示を操作するクラスを作る
class AppView {
private _model: AppModel
// ユーザー入力要素
private _input: HTMLInputElement;
// 警告テキスト
private _list: NodeListOf<HTMLLIElement>;
constructor(
// クラス名 validation-target の div 要素
private _group: HTMLDivElement
) {
// グループ内の input 要素と li 要素の NodeList を取得
this._input = _group.getElementsByTagName('input')[0];
this._list = this._group.getElementsByTagName('li');
// input 要素に対し必要なイベントリスナーを登録
this._input.addEventListener('keyup', () => this.handleEvent(), false);
this._input.addEventListener('mouseup', () => this.handleEvent(), false);
this._model = new AppModel(this._input);
}
// イベントリスナーで登録されるメソッド
private handleEvent(): void {
const errors: string[] = this._model.validate();
// エラーがない場合に検証成功のメソッド、ある場合に検証エラー時のメソッドを呼び出す
(errors.length === 0) ? this.onValid() : this.onInvalid(errors);
}
// ユーザー入力値が正しいとき
private onValid(): void {
// success 表示
this._group.classList.add('has-success');
this._group.classList.remove('has-error');
// 全ての警告テキストを非表示
Array.from(this._list)
.forEach(li => li.style.display = 'none');
}
// ユーザー入力値エラーがあるとき
private onInvalid(errorKeys: string[]): void {
// error 表示
this._group.classList.add('has-error');
this._group.classList.remove('has-success');
// 全ての警告テキストを非表示
Array.from(this._list)
.forEach(li => li.style.display = 'none');
// エラーの警告のみ再表示
Array.from(this._list)
.filter(li => errorKeys.some(key => key in li.dataset))
.forEach(li => li.style.display = 'block');
}
}
メイン処理
window.addEventListener('DOMContentLoaded', () => {
// 検証する要素群を取得
const groups = <HTMLCollectionOf<HTMLDivElement>>document.getElementsByClassName('validation-target');
// 画面操作のインスタンスを生成
Array.from(groups).forEach(group => new AppView(group));
document.getElementsByTagName('form')[0].addEventListener('submit', (e) => {
// 送信時の動作、サンプルなので何もしません。
alert('デモ画面です。')
e.preventDefault();
}, false);
}, false);