
現在、もりけん塾にてJavaScriptを勉強させていただいております。その記録。
今回は課題17についての備忘録です。
考え方が間違っている箇所もあるかと思いますが、お気づきの際はご指摘いただければと思います。
課題17について
仕様
1.画面遷移してから3秒後に解決されるPromiseが返すオブジェクトを元にimgタグを5つつくる。
2. それぞれは.z-indexで重ねた状態。矢印画像をクリックを押すとスライド画像が変わる
3. 5枚中何枚目かを表示して、5/5の場合Nextの矢印はdisabledにする。1/5枚の時はBackボタンはdisabledにする
制作物
コードはこちらから(codesandbox)※エラーが出る場合がありますが、リロードすれば動きます。
実装内容
JSONデータ作成
まずはJSONデータを作成しました。今回はimgだけの取得で良いので、id、img、description(alt用)という構成で作成しました。
課題作成時に今まで使用していたmyjsonが使用できなくなってしまったので、新しく教えてもらった下記サイトで作りました。塾ではいろんな情報を共有してくれるので有り難いです。
Mock API for Development and Testing 自分が作成し、保存したJSONデータこれ3秒後に解決されるPromise/JSONデータの取得
Web上に保存したJSONデータの取得をします。
ここら辺は前回までの課題からほとんど一緒ですね。
今回は3秒後にPromiseでfetchでのデータ取得を解決させました。fetchでの取得に問題がなければ、取得したデータをrenderImgUiElement()へ渡します。
const getData = new Promise((resolve) => {
showLoadImg();
setTimeout(() => {
resolve(fetch('https://mocki.io/v1/09593e78-80ba-4765-940e-3c70fb46078c'));
}, 3000);
})
//Get json data
const callApi = async() => {
try {
const res = await getData;
if (!res.ok) {
throw new Error(`サーバーリクエストに失敗しました: ${res.status}`);
}
const json = await res.json();
return json.data;
}
catch(error) {
console.error(error);
}
finally {
removeLoadImg();
}
}
const init = async() => {
const imgData = await callApi();
if (imgData){
renderImgUiElement(imgData);
} else {
imgListsWrapper.textContent = "データの表示に失敗しました";
console.error('データの表示に失敗しました');
}
}
init();
データを受け取った後の処理
データを受け取ったrenderImgUiElement()は下記の事をします。
①次へのボタンを作成する(renderNextBtn())
②前へのボタンを作成する(renderPrevtBtn())
③ページナンバーを作成する(renderPageNumElement())
④イメージ要素を作成し追加、表示(getImgListsFragment()を実行し、imgLists配下へ追加)
const renderImgUiElement = (imgData) => {
renderNextBtn(imgData);
renderPrevBtn(imgData);
renderPageNumElement(imgData);
imgLists.appendChild(getImgListsFragment(imgData));
}
取得したデータから画像を表示する
JSONデータにある数だけ、for文でimgタグを作成し、src、altを挿入していきます。一番目の要素には、.is-show というクラスをつけました。今回は「z-indexで重ねた状態」という仕様があるので、is-showがついているimgタグをz-indexで上に表示させ、ボタンをクリックする度に、is-showが切り替わるというイメージで考えました。
ここでできたDOMツリー(fragmentImglists)をimgLists配下へappendchildします。
//get Img Elements
const getImgListsFragment = (imgData) => {
const fragmentImglists = document.createDocumentFragment();
for (let i = 0; i < imgData.length; i++) {
const imgList = createElementWithClassName("li", "imgList");
const img = createElementWithClassName("img", `img_0${[i + 1]}`);
img.src = imgData[i].img;
img.alt = imgData[i].description;
i === 0 && imgList.classList.add("is-show");
fragmentImglists.appendChild(imgList).appendChild(img);
}
return fragmentImglists;
};
ボタンの作成・クリックイベントで画像の切り替えをする
ここが今回悩んだ箇所かなと思います。
imgNumという変数をグローバルで宣言し、次へのボタン・前へのボタンが押される度に1ずつ増減させページのindexを取得し、その箇所に.is-showクラスを付与させページの切り替えを行いました。
また、このクリックイベントが行われる度に、下記関数も実行されます。(また別で説明します)
・switchDisableForBtn()
→最初と最後のページの際に、ボタンをdisableにする
・setNumberOfPage()
→ページナンバー表示の切り替え
let imgNum = 0;
const imgList = document.getElementsByClassName("imgList");
const renderPrevBtn = (imgData) => {
const prevBtn = createElementWithClassName("button", "prev");
prevBtn.id = 'js-prevbtn';
prevBtn.textContent = '◀︎';
imgListsWrapper.appendChild(prevBtn);
prevBtn.disabled = true;
prevBtn.addEventListener("click", function() {
imgNum -= 1;
document.querySelector(".is-show").classList.remove('is-show');
imgList[imgNum].classList.add('is-show');
switchDisableForBtn(imgData);
setNumberOfPage(imgData);
});
}
const renderNextBtn = (imgData) => {
const nxtBtn = createElementWithClassName("button", "next");
nxtBtn.id = 'js-nextbtn';
nxtBtn.textContent = '▶︎';
imgListsWrapper.appendChild(nxtBtn);
nxtBtn.addEventListener("click", function() {
imgNum += 1;
document.querySelector(".is-show").classList.remove('is-show');
imgList[imgNum].classList.add('is-show');
switchDisableForBtn(imgData);
setNumberOfPage(imgData);
});
}
レビューPOINT①
ここでの引数を当初下記としてました。
switchDisableForBtn(imgNum,imgData);
setNumberOfPage(imgNum,imgData);
imgNumの増減はクリックイベントによって変動するため、ローカル変数と思い込んでいました。が、グローバルで宣言しているため、引数に渡す必要はなかった。思い込みってすごいな。
レビューPOINT②
クリックされた際に、.is-showのついた要素を取得する必要があるのですが、ここを最初、下記を使用しグローバルで定数宣言していました。
const isShowElement = document.getElementsByClassName("is-show");
ですが、ここはquerySelectorを使用してはどうかとレビューしていただきました。
is-showは1つしか存在しないので、クラスに一致する最初の要素を取得するこちらの方が適切だと思い、変更させていただきました。しかし、そのままグローバルの位置で変更すると、ページ読み込み時に要素が取得できずnullが返ってくるため、定数には入れずクリックイベントの中で直接使用することにしました。
ボタンのdisable化
もし最初のページだった場合前へのボタンは無効化、最後のページだった場合は次へのボタンは無効化。
const switchDisableForBtn = (imgData) => {
const prevBtnElement = document.getElementById("js-prevbtn");
const nxtBtnElement = document.getElementById("js-nextbtn");
const lengthImg = imgData.length;
prevBtnElement.disabled = imgNum === 0;
nxtBtnElement.disabled = imgNum === lengthImg - 1;
};
レビューPOINT
最初のコードがこれ。
//最初のプルリク
const switchDisableForBtn = (num, imgData) => {
const prevBtnElement = document.getElementById("js-prevbtn");
const nxtBtnElement = document.getElementById("js-nextbtn");
const lengthImg = imgData.length;
if (num !== 0) {
prevBtnElement.disabled = false;
} else {
prevBtnElement.disabled = true;
}
if (num !== lengthImg - 1) {
nxtBtnElement.disabled = false;
} else {
nxtBtnElement.disabled = true;
}
};
ながあ。。
.disabled = の後ろにはtrueかfalseが判断できればいいので、そこに条件式を書いてあげればいいのだとわかりました。
こことってもスッキリして嬉しかった。
ページ何枚目かの表示
画像が切り替わる度に何枚目かを切り替える。
デフォルトで1/imgData.length(今回は5)を表示させ、クリックイベントが起こるたびに更新されるimgNumを取得し、表示させる。
const renderPageNumElement = (imgData) => {
const numberOfPage = createElementWithClassName("p", "pageNumber");
imgListsWrapper.appendChild(numberOfPage);
numberOfPage.id = "js-number";
numberOfPage.textContent = `1 / ${imgData.length}`;
}
const setNumberOfPage = (imgData) => {
document.getElementById("js-number").textContent = `${imgNum + 1} / ${imgData.length}`;
}
その他レビュー箇所
関数について、ほとんどがアロー関数で書かれているのに対し、数カ所関数宣言で書かれている箇所があったので、統一しました。課題を続けていると、前に書いたものをそのまま使用していたりするので確認不足ですね。
後記
いつも冗長なコードになってしまうので、機能ごとに関数でまとめるのを意識しました。リファクタリングしている最中に動かなくなったり、理解不足も感じましたが、レビューいただきコードがスッキリしたので嬉しいです。
c.sakyou