嘘ついてすいませんでした
三部作と言っておきながら、三つ目の記事ではまだ問題があったのでヌケヌケと4つ目に入ります。
html-css-javascript.hatenadiary.com
前回でも言いましたが、コード的には特に問題はありませんでした。
コードはこんな感じです。
let candidates = JSON.parse(localStorage.getItem("candidates")) || []; const candidateList = document.getElementById("candidateList"); render(); document .getElementById("addCandidateForm") .addEventListener("submit", function (event) { event.preventDefault(); const inputName = document.getElementById("candidateName"); const inputAge = document.getElementById("candidateAge"); const name = inputName.value.trim(); const age = inputAge.value.trim(); inputName.value = ""; inputAge.value = ""; const newCandidate = { name, age, vote: 0, }; candidates.push(newCandidate); render(); }); function render() { candidateList.innerHTML = ""; const sortedCandidates = [...candidates].sort((a, b) => b.vote - a.vote); sortedCandidates.forEach((candidate) => { const listItem = document.createElement("li"); const infoSpan = document.createElement("span"); infoSpan.textContent = `名前: ${candidate.name}, 年齢: ${candidate.age}, 投票数: ${candidate.vote}`; const voteBtn = document.createElement("button"); voteBtn.textContent = "投票"; voteBtn.addEventListener("click", function () { candidate.vote += 1; localStorage.setItem("candidates", JSON.stringify(candidates)); render(); }); listItem.appendChild(infoSpan); listItem.appendChild(voteBtn); candidateList.appendChild(listItem); }); }
(あと、HTMLも必要なところだけここらで追加しておきます。)
<form id="addCandidateForm" action="#" method="post"> <label for="candidateName">候補者名:</label> <input type="text" id="candidateName" name="candidateName" placeholder="候補者" required /> <label for="candidateAge">候補者年齢:</label> <input type="number" id="candidateAge" name="candidateAge" min="0" max="150" placeholder="年齢" required /> <button id="addBtn" type="submit">追加</button> </form> <ul id="candidateList"></ul><!-- 候補者を追加していく部分。今色々手をくわえてるのはここです -->
しかし一回投票ボタンをクリックしてrender()を呼び出すと、候補者の分だけクリックイベントの発火装置をつくることになってしまい、流石に無駄が大きいです。
これをどうにかします。
どうしたらいいのかわからないのでChatGPT先生に聞いてみたところ、イベント委譲というのがいいらしいです。
イベント委譲
イベント委譲が何をするのかというと、注目する対象の要素をボタンより1つ上げます。
つまりvoteBtnの一つ上、candidateListを注目することになります。
実際にクリックするボタンはvoteBtnです。
しかしその処理を一つ上のcandidateListに任せる、つまり委譲することになります。
で、配列の要素、つまり子要素のボタンに番号を割り振って、candidateListが「◯◯番、お前がやれ」と指示するわけです。
こうすれば、指定されたボタン以外は動かなくて済むので、候補者の回数だけクリックイベントを作るという問題は防ぐことができます。
ではそういう感じで作っていきます。
まず番号の割り振りから。
voteBtn.dataset.index = candidates.indexOf(candidate);
これをforEachの中で使えば、変数candidateが何周目を回しているのかをindexとして取り出すことができます。
でもこれ、forEachの第二引数にindexと入れたら、同じことをやってくれるのでこっちでやったほうが速いですね。
なのでindexにはすでに順番を表す数値が入っているものとして考えます。
で、この何番目かを表す数値をボタンにタグ付けすれば、あとはcandidateListさんが指示したいボタンを指定することができます。
そのために必要なことは、ボタンに新しい属性を付与することです。
それにはdatasetを使います。
voteBtn.dataset.index = index;
これで、HTMLのボタンタグに以下のような属性が付与されます。
<button data-index="2">投票</button>
index="2"と書いてますが、これはforEachを回しているcndidateが二周目に入った状態ということです。
あと「どこのボタンだっけ?」と言う人(私)のために言っておくと、voteBtnは以下のように定義づけされている投票ボタンです。
const voteBtn = document.createElement("button");
JavaScriptでdataset.indexと書くと、htmlのタグの中では上述のようにdata-indexという属性が付与されます。
これはdatasetと書かないと機能しません。
たとえばheyset.indexと書いてもhey-indexみたいな面白いことにはならないということです。
dataset.indexと書いて、data-indexという属性が付与される、ということを覚えておきましょう。
さて、ここまでわかったのでこれを実際に入れるとなると、修正箇所はforEachということになりますね。
やることは3つ。
①forEachに第二引数indexを追加。
②buttonのタグにindexの属性を付与しbutton data-indexにする。
③クリックイベントをrender関数の外に出す。
結果が以下。
sortedCandidates.forEach((candidate,index) => { //第二引数を追加 const listItem = document.createElement("li"); const infoSpan = document.createElement("span"); infoSpan.textContent = `名前: ${candidate.name}, 年齢: ${candidate.age}, 投票数: ${candidate.vote}`; const voteBtn = document.createElement("button"); voteBtn.textContent = "投票"; voteBtn.addEventListener("click", function () { candidate.vote += 1; localStorage.setItem("candidates", JSON.stringify(candidates)); render(); }); // 子ボタンに番号を付ける voteBtn.dataset.index = index; listItem.appendChild(infoSpan); listItem.appendChild(voteBtn); candidateList.appendChild(listItem); });
さて、外に出したクリックイベントも記述しないといけませんね。
イベント委譲をするためにイベントは親要素で行うことになりましたので、以下のようになります。
candidateList.addEventListener("click", function () { candidate.vote += 1; localStorage.setItem("candidates", JSON.stringify(candidates)); render(); });
さて、これだと問題点があります。
candidateListの中にはbuttonだけではなくliやspanも存在します。
このままではそのliやspanにある文字をクリックした場合であってもクリックイベントが働いてしまいます。
なので「クリックしたところがbuttonであること」という条件を付ける必要があります。
candidateList.addEventListener("click", function(event) { const target = event.target; if (target.tagName === "BUTTON") {//ボタンを押したら処理を実行 const index = target.dataset.index; candidates[index].vote += 1; localStorage.setItem("candidates", JSON.stringify(candidates)); render(); } });
targetを定義せず、そのままevent.target.tagName==="BUTTON"としてもよかったのですが、見やすいということと、今後どこかで使う可能性もあるので、const target = event.target;としてtargetは定義しておくことにします。
ここではtargetは<button>投票</button>を指します。
なのでtarget.tagNameはbuttonを指すことになります。
で、以前言ったように、これがJavaScriptに渡されるときはなぜか大文字になりますので(仕様です)、"BUTTON"になります。
これでクリックした部分がbuttonだった場合は処理が行われ、それ以外の時は何もおこりません。
さて、イベント委譲を含んだクリックイベントをまとめます。
candidateList.addEventListener("click", function(event) { const target = event.target; if (target.tagName === "BUTTON") { const index = target.dataset.index; candidates[index].vote += 1; localStorage.setItem("candidates", JSON.stringify(candidates)); render(); } });
candidates.voteのところが変わっていますが、まあこれは細かい説明はいらないでしょう。
配列のindex番目の投票数に1を足しているだけです。
でもこれ、candidates[]だとソートをしていない初期の配列ということになります。
なのでここは
sortedCandidates[index].vote += 1;
にしておく必要があります。
ID方式
さて、ここまでindexによって配列の中のどの要素を指定するかを書いてきたわけですが、コードをChatGPT先生に見せたところ、「いや、まあそもそもindexを指標とするのってまずいんだけどね」とか抜かしやがってくださいました。
は?
先生「indexじゃなくてidを指標にして。」
なんで最初からそう言ってくれねぇんですか?
そこにはちゃんとした理由がありました。
まあ簡単にいうと、indexだけだと確実には順番どおりに紐づけできないので、それをIDにすることで指定する配列の要素を確実にすることが目的です。
今回はここまでにして、続きは次回に回します。