

上記の悩みを改善します。
例えば、タイムスケジュールを登録するときに元々時間帯が確定しているものであれば不要ですが、その日、そのイベントごとに時間帯が異なる場合は、入力できるフォームを利用者が自由に増減できると機能的に柔軟性がありユーザー側も便利です。
本記事はそんな機能を実装する上で参考になるであろう、テーブルに行の追加・削除機能を実装する方法を解説します。
完成形
まずは完成形をご覧ください。

テーブルはシンプルに開始時間と終了時間、プログラム内容の項目を用意しています。
+ボタンを押下すると行が追加され、-ボタンが押下されると押下したテーブルの行が削除されるようになっています。
コード解説
それではコードの解説です。
注意ポイント
見た目はBootstrap4で構成されているので、コピペする際はBootstrap4に対応しているアプリケーションに利用してください。
HTML
HTMLのコードは以下の通りです。
<div class="d-flex mb-3">
<label for="" class="mb-0 form-label" style="width: 200px">プログラム</label>
<div class="flex-grow-1">
<table id="table-management" class="table table-striped mb-4 border">
<thead>
<tr>
<th>開始時間</th>
<th></th>
<th>終了時間</th>
<th>プログラム内容</th>
<th>操作</th>
</tr>
</thead>
<tbody>
<tr>
<td><input type="time" name="" id="" class="form-control" placeholder="" value="" /></td>
<td style="width: 3%" class="text-center align-content-center p-0">~</td>
<td><input type="time" name="" id="" class="form-control" placeholder="" value="" /></td>
<td><input type="text" name="" id="" class="form-control" placeholder="" value="" /></td>
<td>
<div class="btn btn-danger font-weight-bold rounded-circle remove-btn">-</div>
</td>
</tr>
</tbody>
</table>
<div class="d-flex justify-content-center">
<div id="add-btn" class="btn btn-primary d-flex justify-content-center font-weight-bold rounded-circle">+</div>
</div>
</div>
</div>

CSS
Bootstrapだけでは補えない部分を別途CSSで対応しています。(主にフォームに関するCSSです)
input[type="date"],
input[type="time"] {
position: relative;
&::-webkit-calendar-picker-indicator,
&::-webkit-calendar-picker-indicator {
position: absolute;
top: 0;
left: -7.5px;
width: 100%;
height: 100%;
opacity: 0;
cursor: pointer;
}
}
デフォルトだとinput type="time"
は、フォームの中に時計のアイコンが表示され、アイコンを押下しないと時間を設定することができないため、input全体を押下できる領域としています。
left: -7.5px;の指定について
input type="date"
であれば特に問題ないのですが、type="time"
の場合はなぜかleft: 0;
だとフォームの左端あたりまで押下できる範囲が広がり切らなかったため指定しています。数値は目視で確認した値を指定していますが、他にいい方法があるかもしれません...。
JavaScript
テーブル行の追加、削除機能の実装を行っている部分になります。
document.addEventListener("DOMContentLoaded", function () {
const tableBody = document.querySelector("#table-management tbody");
const addBtn = document.getElementById("add-btn");
const removeBtn = document.querySelectorAll(".remove-btn");
addBtn.addEventListener("click", function () {
const addTargetTableRow = tableBody.querySelector("tr:first-child");
const addTableRow = addTargetTableRow.cloneNode(true);
addTableRow.querySelectorAll("input").forEach((input) => (input.value = ""));
tableBody.appendChild(addTableRow);
});
tableBody.addEventListener("click", (event) => {
if (event.target.classList.contains("remove-btn") && tableBody.rows.length > 1) {
event.target.closest("tr").remove();
}
});
});
ポイントなる箇所だけ解説したいと思います。
行追加ボタンの処理について
addBtn
を押下すると、まずコピーする要素(addTargetTableRow
)を取得しています。
要素を取得したら、addTargetTableRow.cloneNode(true);
で要素の子要素ごとコピーしています。

cloneNode
の引数をtrue
とすることでその要素と子要素全てをコピー対象とし、false
とするとその要素のみをコピーの対象とします。コピーした要素(addTableRow
)に含まれるinput
に値が入っていてほしくないため、すべてinput
を取得(querySelectorAll("input")
)して、それぞれにinput.value = ""
の処理(forEach((input) => (input.value = "")
)を実行しています。
value
を空にしたら、行を最後に追加する処理(tableBody.appendChild(addTableRow);
)を行い、行の追加処理は完了です。
行削除ボタンの処理について
削除ボタンは各行テーブルそれぞれに配置されているため、どの削除ボタンが押下されたかをイベント側に情報を渡してあげる必要があります。
まずは、渡されたイベント情報に削除ボタンが含まれているか確認(event.target.classList.contains("remove-btn")
)し、現在の行数が2以上かどうかを確認(tableBody.rows.length > 1
)しています。

rows
関数は、table
またはtbody
に対して使用できるプロパティで、テーブル内のすべての tr
(行)を取得するために使います。tableBody.rows.length > 1
は、最低でも1つ行を残しておく必要があるため条件式に含めています。
これらの条件をクリアすることで行の削除が実行されます。
まとめ
ここまで読んでいただきありがとうございました。
テーブルの行を追加・削除する必要がある要件の場合に役立つ記事かと思いますので、ぜひ参考にしていただけると幸いです!
また今回は実装していませんが、行ごとに移動ができるとなお良いですね!