よくユーザーの操作で任意個数の入力をできるようにすることがあります。ReactやVueの様な宣言的UIならば配列に格納してループで配列内各要素のパラメーターに応じたHTMLを表示、追加ボタンが押されたら配列にパラメーターを追加、削除ボタンが押されたらindexに合わせて要素を削除、とするのみです。しかしながらやんごとなき理由で宣言的UIのライブラリが使えず、自前の素のJavaScriptのみでどうにかしなくてはならない時もありますそういった時に使うコードのテンプレ―トを紹介します。
コード例とデモは次です。
<button class="add-btn">追加</button> <div class="item-box"> </div>
document.querySelector('.add-btn').addEventListener('click', appendItem);
/** 要素を追加する関数 */
function appendItem(){
/** 追加要素につけるID */
const randomId = `id-${Math.random()}`;
/**
* 入力欄のインデックス。getNextIndex 関数内で空いている最小のインデックスを取得する。
* これをすると次の様な中抜けの補完ができる。
* [0, 1, 2] -削除-> [0, 2] -追加-> [0, 1, 2]
*/
const nextIndex = getNextIndex();
/**
* 追加する要素のHTML。ここで先程作ったIDとインデックスを使う。
* randomIdを用いることでピンポイントで要素を削除できるようにする。
* item[nextIndex][任意キー]とすることで、
* サーバー側で受け取ったリクエストでは item[index] 以下の連想配列に意味のある一セットが集まる様にする
*/
const template = `
<button type="button" class="btn-remove" onclick="document.getElementById('${randomId}').remove()">削除</button>
<label>入力欄のラベル</label>
<div class="input-box">
<input type="text" name="item[${nextIndex}][name]">
<input type="text" name="item[${nextIndex}][email]">
</div>
`;
// ↑で定義した要素を中身に持つ要素オブジェクトを作る
const el = document.createElement('div');
el.id = randomId;// 削除用ランダムID
el.dataset.index = nextIndex;// この要素がどのインデックスを持つか読み取りやすくする
el.innerHTML = template;// innerHTML で中身を丸ごと渡す。わりと高速な処理
// 出来上がった要素を入れるべき場所に追加
document.querySelector('.item-box').appendChild(el);
}
/** 追加する要素がどのインデックスを取るべきか教える関数 */
function getNextIndex(){
// 既存要素が使用中のインデックスを列挙
const usedIndex = Array.from(document.querySelectorAll('.item-box > *')).map(el => +el.dataset.index);
usedIndex.sort();
// 抜け番探し
let prev = usedIndex[0];
// 0 が存在しないならば、0が抜け番
let missingNo = (prev != null && prev !== 0) ? 0 : null;
// 順々に見て行ってずれが1出なくなった場合、それが抜け番
if (missingNo === null) {
for (let current of usedIndex.slice(1)) {
if (prev + 1 !== current) {
missingNo = prev + 1;
break;
}
prev = current;
}
}
// 抜け番が見つかればそれを返す。見つからなければ現在の最大値+1を返す
return missingNo !== null ? missingNo : usedIndex.slice(-1) + 1
}
React 等を使っている時と決定的に違うのはデータを保持する変数を JavaScript 上に持たないことです。代わりにHTML上を常に参照する様にしています。これを行うことで何が良いかというと状態管理とそれをUIに反映する手間から解放されることが良いです。そういった宣言的UIらしいことを素の JavaScript で実現しようとすると大変手間ですので、なるべく状態を持たず読みやすさを保った方が結果的に高速かつ改修しやすくなりやすいです。
あんまり巨大な処理になってくると処理結果でどういうUIが出来上がるのか把握できなくなり、にっちもさっちもいかなくなります。そういった時こそ React 等の宣言的UIを使いやすいライブラリの出番です。抜本的な解決ができます。