学習の備忘録です。タスク管理アプリを作っています。
登録画面と編集画面のモーダルを開いて表示する挙動について、重複する部分が多々あったのでまとめられない? となったのでまとめました。
今回のポイントは以下です。
- イベントデリゲートで一括管理
- クリック要素から対象モーダルを判定
- 登録モーダルのみリセット処理を分岐
本記事は個人の学習記録です。内容の正確性を保証するものではありませんので、あらかじめご了承ください。
統一前の登録画面のjs
※全体を囲う$(function () {});は省略してます。
//タスク登録画面表示
function addTask() {
$(document).on('click', '.task-create-btn, #close-add-modal, #add-task-modal', function (e) {
//送信ボタンが押されたときはそのまま送信させる
if ($(e.target).closest('button[type="submit"]').length) {
return;
}
//送信ボタン以外なら、htmlの標準動作を止める
e.preventDefault();
//クリックされた瞬間にモーダルを探す
const addTaskModal = document.getElementById('add-task-modal');
if (!addTaskModal) return;
//クリックされたのが「登録ボタン」ならモーダル表示
if ($(this).hasClass('task-create-btn')) {
addTaskModal.classList.add('is-open');
}
//キャンセルボタン、または欄外が押されたら閉じる
else if ($(this).is('#close-add-modal') || e.target === addTaskModal) {
//まず非表示にする
addTaskModal.classList.remove('is-open');
//3秒後に中身をリセットする
setTimeout(function() {
//2行目以降(最初の行以外)をすべて削除
$('#input-container .input-row').not(':first').remove();
//残った1行目を取得
const $FirstRow = $('#input-container .input-row').first();
//1行目の中身をリセット
$FirstRow.find('input[type="text"]').val('');
$FirstRow.find('.priority-select').val('2').removeClass('p-1 p-3').addClass('p-2');
}, 300);
}
});
}
統一前の編集画面のjs
//タスク編集画面表示
function editTask() {
$(document).on('click', '.task-edit-btn, #close-edit-modal, #edit-task-modal', function (e) {
//送信ボタンが押されたときはそのまま送信させる
if ($(e.target).closest('button[type="submit"]').length) {
return;
}
//送信ボタン以外なら、htmlの標準動作を止める
e.preventDefault();
const editTaskModal = document.getElementById('edit-task-modal');
if (!editTaskModal) return;
//クリックされたのが「編集ボタン」ならモーダル表示
if ($(this).hasClass('task-edit-btn')) {
editTaskModal.classList.add('is-open');
}
//キャンセルボタン、または欄外が押されたら閉じる
else if ($(this).is('#close-edit-modal') || e.target === editTaskModal) {
//まず非表示にする
editTaskModal.classList.remove('is-open');
}
});
}
登録画面と編集画面のコードを統一
function manageTaskModal() {
//登録ボタンや編集ボタン、閉じるボタン、モーダル背景を一括監視
$(document).on('click', '.task-create-btn, .task-edit-btn, #close-add-modal, #close-edit-modal, #add-task-modal, #edit-task-modal', function(e) {
//送信ボタンなら何もしない
if ($(e.target).closest('button[type="submit"]').length) return;
e.preventDefault();
//ターゲットとなるモーダルを特定する
//ボタンのクラス名やIDから、操作すべきモーダルを自動判別
let targetModalId = '';
if ($(this).hasClass('task-create-btn') || $(this).is('#add-task-modal, #close-add-modal')) {
targetModalId = 'add-task-modal';
}else if ($(this).hasClass('task-edit-btn') || $(this).is('#edit-task-modal, #close-edit-modal')) {
targetModalId = 'edit-task-modal';
}
const $modal = $('#' + targetModalId);
if (!$modal.length) return;
//表示・非表示の切り替え
if ($(this).hasClass('task-create-btn') || $(this).hasClass('task-edit-btn')) {
//開く処理
$modal.addClass('is-open');
} else if ($(this).is('#close-add-modal, #close-edit-modal') || e.target.id === targetModalId) {
//閉じる処理
$modal.removeClass('is-open');
//リセット処理(登録モーダルかつ閉じた時のみ実行)
if (targetModalId === 'add-task-modal') {
setTimeout(function() {
const $container = $('#add-input-container');
$container.find('.input-row').not(':first').remove();
const $FirstRow = $container.find('.input-row').first();
$FirstRow.find('input[type="text"]').val('');
$FirstRow.find('.priority-select').val('2').removeClass('p-1 p-3').addClass('p-2');
}, 300);
}
}
});
}
manageTaskModal()
分解して解説
イベントデリゲートにてクリック検知
イベントデリゲートで、クリックを検知させたい要素を全部監視します。
$(document).on('click', '.task-create-btn, .task-edit-btn, #close-add-modal, #close-edit-modal, #add-task-modal, #edit-task-modal', function(e) {・・・
submitで送信させる場合と、それ以外で挙動を分岐させる
モーダル内にある「登録」や「更新」ボタンを押した時はそのまま通常の送信処理が動くようにして(jsの処理を終了して)、e.preventDefault();で通常のHTMLの送信処理を停止させます。
//送信ボタンなら何もしない
if ($(e.target).closest('button[type="submit"]').length) return;
e.preventDefault();
ターゲットとなるモーダルを特定する
ここが今回の改修の一番のポイントです!
//ターゲットとなるモーダルを特定する
//ボタンのクラス名やIDから、操作すべきモーダルを自動判別
let targetModalId = '';
if ($(this).hasClass('task-create-btn') || $(this).is('#add-task-modal, #close-add-modal')) {
targetModalId = 'add-task-modal';
}else if ($(this).hasClass('task-edit-btn') || $(this).is('#edit-task-modal, #close-edit-modal')) {
targetModalId = 'edit-task-modal';
}
まずlet targetModalId = ”;で更新可能な変数を定義します。この中に、指定したいモーダルのIDが入ります。
if ($(this).hasClass('task-create-btn') || $(this).is('#add-task-modal, #close-add-modal')) {
targetModalId = 'add-task-modal';
}
$(this).hasClass(‘task-create-btn’)…クリックした要素が「task-create-btn」クラスを持っているか確認します。
$(this).is(‘#add-task-modal, #close-add-modal’)…クリックした要素が「add-task-modal」または「close-add-modal」というIDを持っているか確認します。(hasClass()メソッドはあるのにhasId()メソッドはないんですよね。)
targetModalId = ‘add-task-modal’;…ifの条件を満たしたら、変数に’add-task-modal’を入れます。
edit-task-modalについても同じ流れです。
操作するモーダルが実在するか確認する
特定したtargetModalIdを使って、jQueryオブジェクト$modalを生成します。
const $modal = $('#' + targetModalId);
if (!$modal.length) return;
ここでは、直前の条件分岐で特定したIDが、実際にHTML内に存在するかを確認しています。もしtargetModalIdが空だったり、想定外の値が入って要素が見つからなかったりした場合は、$modal.lengthが0になります。
if (!$modal.length) return;は、そういった予期しない要素が入った時にエラーを吐かないように、「もし一つ上の処理でlength = 0が返ってきたら処理を終了する」という保険を掛けています。
const $modal = $(‘#’ + targetModalId);なんだからtargetModalId = ”でも$modal = #が入ってlength = 0にならないんじゃ? と思いましたが、$(‘#’)というのは「id = “”という要素を探して」という命令なので、「そんなものはない(length = 0)」で結果が返ってくるとのこと。
表示・非表示の切り替え
//表示・非表示の切り替え
if ($(this).hasClass('task-create-btn') || $(this).hasClass('task-edit-btn')) {
//開く処理
$modal.addClass('is-open');
} else if ($(this).is('#close-add-modal, #close-edit-modal') || e.target.id === targetModalId) {
//閉じる処理
$modal.removeClass('is-open');
//リセット処理(登録モーダルかつ閉じた時のみ実行)
if (targetModalId === 'add-task-modal') {
setTimeout(function() {
const $container = $('#add-input-container');
$container.find('.input-row').not(':first').remove();
const $FirstRow = $container.find('.input-row').first();
$FirstRow.find('input[type="text"]').val('');
$FirstRow.find('.priority-select').val('2').removeClass('p-1 p-3').addClass('p-2');
}, 300);
}
細かく分解します。
if ($(this).hasClass('task-create-btn') || $(this).hasClass('task-edit-btn')) {
//開く処理
$modal.addClass('is-open');
}
もしクリックした要素が「task-create-btn」または「task-edit-btn」のクラスを持っているなら、$modal(指定したIDの要素、つまりは、登録画面か編集画面のモーダル)にis-openというクラスを付け足します。
該当箇所のHTMLとCSSは以下のようになっているので、それぞれ対応したモーダルが開きます。
<!-- タスクの登録、編集、変更ボタン -->
<div class="btn-area">
@if (now()->format('Y/m/d') == $displayDate)
@if ($tasks -> whereNull('master_id') -> isEmpty())
<div class="task-create-area">
<button type="button" class="btn-primary task-create-btn">タスクを登録する</button>
</div>
@else
<div class="use-change-ticket">
<button type="button" class="btn-primary change-ticket-btn">タスクを変更する</button>
</div>
@endif
@elseif (\Carbon\Carbon::parse($displayDate)->lt(today()))
<div class="btn-primary placeholder" style="visibility: hidden;"></div>
@elseif ($tasks -> whereNull('master_id') -> isNotEmpty())
<div class="task-edit-area">
<button type="button" class="btn-primary task-edit-btn">タスクを編集する</button>
</div>
@else
<div class="task-create-area">
<button type="button" class="btn-primary task-create-btn">タスクを登録する</button>
</div>
@endif
</div>
<div id="add-task-modal" class="modal-overlay">
@include('tasks._create_modal', ['tasks' => $tasks, 'displayDate' => $displayDate])
</div>
<div id="edit-task-modal" class="modal-overlay">
@include('tasks._edit_modal', ['tasks' => $tasks, 'displayDate' => $displayDate])
</div>
.modal-overlay {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, 0.5);
/* 背景を暗く */
z-index: 1000;
display: flex;
justify-content: center;
align-items: center;
opacity: 0;
visibility: hidden;
transition: opacity 0.3s ease, visibility 0.3s ease;
}
.modal-overlay.is-open {
opacity: 1;
visibility: visible;
}
.modal-content {
background: white;
padding: 36px;
border-radius: 15px;
width: 70%;
height: 60%;
display: flex;
flex-direction: column;
transform: translateY(-20px);
transition: transform 0.3s ease;
}
.modal-overlay.is-open .modal-content {
transform: translateY(0);
}
閉じる処理は、is-openクラスをremoveするまでは共通
else if ($(this).is('#close-add-modal, #close-edit-modal') || e.target.id === targetModalId) {
//閉じる処理
$modal.removeClass('is-open');
キャンセルした時に編集内容を初期化する処理は登録画面のみでif分岐させます。
//リセット処理(登録モーダルかつ閉じた時のみ実行)
if (targetModalId === 'add-task-modal') {
setTimeout(function() {
const $container = $('#add-input-container');
$container.find('.input-row').not(':first').remove();
const $FirstRow = $container.find('.input-row').first();
$FirstRow.find('input[type="text"]').val('');
$FirstRow.find('.priority-select').val('2').removeClass('p-1 p-3').addClass('p-2');
}, 300);
}
setTimeout(function() {(処理)}, 300);…時間指定して処理を動かします。この場合、「0.3秒待ってから、処理を実行する」というタイマー機能です。
const $container = $(‘#add-input-container’);…指定のIDを含む要素を取得します。
$container.find(‘.input-row’).not(‘:first’).remove();…一番最初の行以外を全部削除します。
$FirstRow.find(‘input[type=”text”]’).val(”);…一番最初の行の値を初期化します。
$FirstRow.find(‘.priority-select’).val(‘2’).removeClass(‘p-1 p-3’).addClass(‘p-2’);…このアプリのタスクには優先度を設けているのですが、デフォルト値が2なのでそれを指定します。併せて、cssも初期化するためにHTMLのクラスを付け替えます。
まとめ
クリック判定を一括管理→ターゲットとなるモーダルを判別→判別結果を基に処理を分岐させる。
とすることで登録画面の処理と編集画面の処理を統一してみました。「似た処理が2つある」時は、今回みたいにまとめられないか考えるとよさそうです。
コードがすっきりすると、次の開発へのモチベーションも上がりますね。引き続きアプリ制作を頑張ります!

コメント