【課題17】スライドショーの作成

【課題17】スライドショーの作成

2022年7月10日

もりけん塾のJSの課題17に取り組みました。
今までスライドショーはswiperやslickといったライブラリーを使用していましたが、
今回初めてvanilla JSで実装にチャレンジしました。

塾生の皆様に頂いたレビューが凄く勉強になったので、
その備忘録です。

今回実装したスライドショーはこちらです。

スライドショーの課題

今回のスライドショーの課題は以下になります。

画面遷移してから3秒後に解決されるPromiseが返すオブジェクトを元にimgタグを5つつくる。
以前使ったmy jsonを使う
それぞれは.z-indexで重ねた状態。矢印画像をクリックを押すとスライド画像が変わる
5枚中何枚目かを表示して、5/5の場合Nextの矢印はdisabledにする。1/5枚の時はBackボタンはdisabledにする

https://github.com/kenmori/handsonFrontend/blob/master/work/markup/1.md#17

個人的に今回特に苦労した箇所は、
・ページネーションのテキスト変更(画像枚数のカウント)
・1/5枚の時はBackボタンはdisabledに、5/5の場合Nextの矢印はdisabledにする。

です。1枚目、5枚目をどう判定するかで結構悩みました。

スライドを作成する関数を作成

function renderSlideArea() {
  const slideImageArea = createElementWithClassName(
    "div",
    "mainvisual__images__inner"
  );
  document.querySelector(".mainvisual__images").appendChild(slideImageArea);
}

function renderSlideImage(item) {
  const slideImageArea = document.querySelector(".mainvisual__images__inner");
  const slider = createElementWithClassName("ul", "slider");
  const fragment = document.createDocumentFragment();
  item.forEach((image, i) => {
    const sliderItem = createElementWithClassName("li", "slider__item");
    sliderItem.dataset.slideIndex = i + 1;

    const sliderImage = createElementWithClassName("img", "slider__image");
    sliderImage.src = image.image;
    sliderImage.width = image.width;
    sliderImage.height = image.height;

    i === 0 && sliderItem.classList.add("is-active");

    fragment.appendChild(sliderItem).appendChild(sliderImage);
  });

  slideImageArea.appendChild(slider).appendChild(fragment);
}

引数(jsonデータ)でjsonデータを受け取り、画像データと画像の枚数から画像を作成していきます。
このコードになるまでに2つ程レビューをいただいてリファクタリングできました。

①data属性の追加の仕方。

最初data属性を追加するのに下記のコードを書いていました。

sliderItem.setAttribute("data-slide-index", i + 1);

setAttributeを使ってdata属性を付与していましたが、レビューでdatasetを教えて頂きdatasetに変更しました。
こちらの方がdata属性を追加していると分かりやすいと思ったからです。

dataset は HTMLElement インターフェイスの読み取り専用プロパティで、要素に設定されたすべてのカスタムデータ属性 (data-*) への読み取…
developer.mozilla.org

② 条件分の省略

スライドが1枚目の時はアクティブにする条件分岐を下記のように書いていました。
if分で渡ってきたデータのindexが0だったら(true)、”is-active”のクラスを付与するというものでした。

   if (i === 0) {
      sliderItem.classList.add("is-active");
    }

レビューサイトで{}を省略できることをレビューでいただきました。
条件が1つだけの場合は{}を省略できるので早速取り入れました。

if (i === 0) sliderItem.classList.add("is-active");

その後もう1つレビューをいただきました。
論理演算子(&&)を使った書き方です。

i === 0 && sliderItem.classList.add("is-active");

個人的にifの{}を省略するより、&&を使ったほうがi===0の時にクラス付与を実行すると読みやすくなりました。個々の意味がちゃんと分かっていれば、すごく読みやすくなるとレビューを受けて感じました。
最初レビュー頂いたときは何がどうなっているのかわかりませんでした・・・・

スライドのボタンを作成する関数

function renderSlideButton() {
  let fragment = document.createDocumentFragment();
  const buttonArea = createElementWithClassName("div", "slider__button");

  const direction = ["previous", "next"];
  direction.forEach((element) => {
    const button = document.createElement("button");
    button.classList.add("arrow", `${element}`);
    button.id = `js-button_${element}`;
    button.textContent = `${element}`;
    button.setAttribute("aria-label", `${element}`);
    fragment.appendChild(button);
  });

  const slideArea = document.querySelector(".mainvisual__images");
  slideArea.appendChild(buttonArea).appendChild(fragment);
}

このボタンの関数作成もレビューですごくシンプルになりました。
はじめはprevとnextのをそれぞれ変数を用意して作成していました。
違いはprevとnextの違いしかなくやっている事(ID,クラスを付与したり)は全く同じでした。
レビューで違う部分を配列で用意し、配列処理で作成する方法を教えて頂きました。
配列の引数の部分はテンプレートリテラルで対応しています。
正直今までテンプレートリテラル苦手だったので、
やっと自分のものにできた気がします。すごく今更感ありましたが・・・・

同じようなものを作成するときは配列で処理することができる!と学んだ瞬間でした。

ページネーションを作成する関数

function renderPagenation(item) {
  const pagenation = createElementWithClassName("div", "slider__pagination");
  const fragment = document.createDocumentFragment();
  const current = document.createElement("span");
  current.id = "js-current";
  current.textContent = 1;
  fragment.appendChild(current);

  const separation = createElementWithClassName("span", "separation");
  separation.textContent = "/";
  fragment.appendChild(separation);

  const total = document.createElement("span");
  total.id = "js-total";
  total.textContent = item;
  fragment.appendChild(total);

  const textArea = document.querySelector(".mainvisual__textarea");
  textArea.appendChild(pagenation).appendChild(fragment);
}

ページネーションを作成する関数はあまり躓かずにコードを書くことができました。
画像の枚数を元にtotalの値が入るように引数を取るようにしています。

ページネーションのテキスト変更機能関数

function switchPagination(target) {
  const pagenationCurrent = document.getElementById("js-current");
  const pagenationTotal = Number(
    document.getElementById("js-total").textContent
  );
  let currentNum = Number(pagenationCurrent.textContent);

  const buttonPrev = target.getAttribute("aria-label") === "previous";
  const buttonNext = target.getAttribute("aria-label") === "next";

  if (currentNum < pagenationTotal && buttonNext) {
    pagenationCurrent.textContent = currentNum += 1;
  } else if (currentNum <= pagenationTotal && buttonPrev) {
    pagenationCurrent.textContent = currentNum -= 1;
  }
}

スライドが変わるごとにページネーションのテキスト(数字)も変更するので、
この関数を作りました。
ここで苦労したのは戻るボタンの最大枚数(今回は5枚目)の時の動作です。
初めはボタンのlabelを条件に含めず、画像の枚数で判断していました。1つの関数で実装できないか考えたのですが、最後の枚数の時数を増やす処理と減らす処理が被ってしまい悩みました。
その結果増やす関数と減らす関数を別々で作成しマジックナンバーを使って実装してしまいました。。。
下記が初期のコードです。

function pagenationCountUp() {
  const pagenationCurrent = document.querySelector(".current");
  const pagenationTotal = Number(document.querySelector(".total").textContent);
  let currentNum = Number(pagenationCurrent.textContent);

  if (currentNum <= pagenationTotal && currentNum !== 5) {
    pagenationCurrent.textContent = currentNum += 1;
  }
}

これがレビューでボタンの属性をもとに判断すると1つの関数にまとまれらることを教えて頂き1つの関数で処理できるようになりました!
クリックイベントを実行する際、クリックしたエレメントを引数で渡し、そのエレメントにpreviousかnextのラベルがあるか判断し関数を実行するように修正しました。
数の増減しか頭になかったので、もっと判断できる条件を探そうと思いました。

画像を変更する関数

function switchImg(target) {
  const active = document.querySelector(".is-active");
  active.classList.remove("is-active");
  active[target].classList.add("is-active");
}

ここも最初はこんなにシンプルに書けていませんでした。
ページネーションと同じように画像を進める関数と戻す関数をそれぞれ作成し実装していました。
違いは条件分岐のがif (prevSlideIndex === 1)かif (nextSlideIndex === slideImage.length)だけなので、
正直かなり冗長でした・・・・

function ImagePrevSlide(e) {
  const activeSlide = document.querySelector(".is-active");
  const prevSlide = activeSlide.previousElementSibling;
  const prevSlideIndex = Number(prevSlide.getAttribute("data-slide-index"));

  if (prevSlideIndex === 1) {
    prevSlide.classList.add("is-active");
    activeSlide.classList.remove("is-active");
    e.disabled = true;
  } else {
    e.nextElementSibling.disabled = false;
    prevSlide.classList.add("is-active");
    activeSlide.classList.remove("is-active");
  }
}


これもレビューを頂いたことで、すごくシンプルにまとめることができました。
それをクリックイベントを登録する際に、is-activeのエレメントを基準とし、
previousElementSiblingとnextElementSiblingを文字列で引数と渡すことで解決しました。

function addSwitchButtonEvent() {
  const prevButton = document.getElementById("js-button_previous");
  const nextButton = document.getElementById("js-button_next");
  prevButton.addEventListener("click", (e) => {
    switchPagination(e.currentTarget);
    switchImg("previousElementSibling");
    toggleButtonDisabled();
  });
  nextButton.addEventListener("click", (e) => {
    switchPagination(e.currentTarget);
    switchImg("nextElementSibling");
    toggleButtonDisabled();
  });
}

データを取得し、スライドを表示する関数を実行する関数

function callImageData() {
  return new Promise((resolve) => {
    setTimeout(() => {
      resolve(
        fetchImageData(
          //https://api.json-generator.com/templates/IZjWl012CAMD/data?access_token=b0154huvd1stffra1six9olbgg34r4zofcqgwzfl : image = 0
          //https://api.json-generator.com/templates/9tm12BO1y5Xx/data?access_token=b0154huvd1stffra1six9olbgg34r4zofcqgwzfl&status=503 : 503
          "https://api.json-generator.com/templates/9tm12BO1y5Xx/data?access_token=b0154huvd1stffra1six9olbgg34r4zofcqgwzfl"
        )
      );
    }, 3000);
  });
}

async function fetchImageData(URL) {
  const response = await fetch(URL);
  if (response.ok) {
    const json = await response.json();
    return json;
  } else {
    console.error(`${response.status}:${response.statusText}`);
    displayErrorMassage("サーバの通信に失敗しました");
  }
}

async function init() {
  showLoadingImage();
  renderSlideArea();
  let slideImageData;
  try {
    const json = await callImageData();
    slideImageData = json.slide;
  } finally {
    removeLoading();
  }

  if (slideImageData.length > 0) {
    renderSlideImage(slideImageData);
    renderPagenation(slideImageData.length);
    renderSlideButton();
    toggleButtonDisabled();
    addSwitchButtonEvent();
  } else {
    addNoimage();
  }
}

init();

ここでも1つレビューで学んだことがあります。
今までtryの中でスライドを表示する関数を実行していたのですが、
tryの中で実行してしまうと、
データの取得に失敗したのか、それともスライドを表示する関数に不備があるのか分からなくなってしまう問題がありました。
tryの処理が完了した後実行するように修正しました。
最初エラーが出て何が原因か掴めずかなり悩み込んでいました。
最初は実行順の問題なのかすごく悩みましたが、別の箇所で宣言した変数が空だったのが問題でした。

スライドボタンにイベントを登録

function addSwitchButtonEvent() {
  const prevButton = document.getElementById("js-button_previous");
  const nextButton = document.getElementById("js-button_next");
  prevButton.addEventListener("click", (e) => {
    switchPagination(e.currentTarget);
    switchImg("previousElementSibling");
    toggleButtonDisabled();
  });
  nextButton.addEventListener("click", (e) => {
    switchPagination(e.currentTarget);
    switchImg("nextElementSibling");
    toggleButtonDisabled();
  });
}

IDでボタンのエレメントを取得し、ページネーションと画像の変更関数、ボタンの非活性化の関数を登録します。
これでスライドショーの課題を実装することができました。

まとめ

今回もレビューで自分のよく分かっていなかった部分がわかり、
1つ1つの理解が深まりました。

特に引数の使い方を考えれば、
コードをシンプルに書けると分かったことが大きかったです。

日々コードを書いていけるように、
このままチャレンジし続けます!

もりけん塾

現在もりけん塾に参加し、JavaScriptの課題に取り組んでいます!
素敵な先生と塾生に囲まれて切磋琢磨な日々を送っています。
今回のJSON GENERATORも塾生の方に教えていただきました!

森田先生のBLOG:無骨日記
Twetter:@terrace_tech

毎日7時に「おはようございます」の記事で投稿しています。ブロガーです。 先輩芸人の運転手していた29歳の頃、芸人の中でも何か一芸を身に付けたいと思い、初海外単身…
kenjimorita.jp