【もりけん塾】JavaScript課題21 -Vanilla JSでテーブルを実装② (ソート機能を追加)-

JavaScript

Vanilla JSを使用して、JSONデータをもとにテーブルを実装をしています。今回はソート機能を追加する課題に挑戦しました。

こんにちは。Webコーダーのはるです。

現在、所属している「もりけん塾」でハンズオン課題 に取り組み、レビューをいただいています。

今日は、課題21 についてアウトプットをしていきます。
(前回までのアウトプットは、こちらです。)

実装する過程で学んだことを、学習ノートとして記録していきます。

認識の間違えている箇所がありましたら、お問い合わせからご指摘いただけるとうれしいです。

Vanilla JSでテーブルを実装②

今回、取り組んだ課題はこちらです。

(もりけん先生のJavaScriptハンズオン課題より)

前回の課題はこちらです。

作成したJSONデータ

こちらにまとめています。

ソートボタンの作成

ソートボタンの素材は配布されていたので、それぞれ標準→昇順→降順の3種類をrenderしました。

背景画像にしてしまうとアクセシビリティ的にボタンの意図が伝わらないと考えて画像を選択しました。(aria-labelとか使えばよかったのかも…?要勉強…)

ボタンの情報はconfigとしてオブジェクトにまとめました。

const buttonConfig = [
  { src: "../img/icon-both.svg", alt: "idを昇順に並び替える", dataSet: "default"},
  { src: "../img/icon-asc.svg", alt: "idを降順に並び替える", dataSet: "asc"},
  { src: "../img/icon-desc.svg", alt: "idを順不同に並び替える", dataSet: "desc"}
 ]

この数だけループで<li><button><img>をそれぞれ作成していきました。

それぞれのボタンには、data-button-status属性で default / asc / desc を付与してソートの種類を管理するようにしました。

const createSortButtons = () => {
 const ul = createElementWithClassName("ul", "sort-buttons");
 ul.id = "js-sort-buttons";


 const buttonConfig = [
  { src: "../img/icon-both.svg", alt: "idを昇順に並び替える", dataSet: "default"},
  { src: "../img/icon-asc.svg", alt: "idを降順に並び替える", dataSet: "asc"},
  { src: "../img/icon-desc.svg", alt: "idを順不同に並び替える", dataSet: "desc"}
 ]


 const fragment = document.createDocumentFragment();
 buttonConfig.forEach(item => {
  const li = createElementWithClassName("li", "sort-buttons__item js-sort-button-item");
  const button = createElementWithClassName("button", "sort-buttons__btn");
  const img = createElementWithClassName("img", "sort-buttons__img");


  li.dataset.buttonStatus = item.dataSet;
  img.src = item.src;
  img.alt = item.alt;
  fragment.appendChild(li).appendChild(button).appendChild(img);
 })
 ul.appendChild(fragment);
 return ul;
};

ソートボタンを表示

今回はID順でソートする課題なので、thの1番目に対してソートボタンをつける実装をしていました。

しかし、レビューを受けて、要素の順番に左右される実装は良くないと気付きました。

IDが一番目以外になることもありうるからです。

最終的に、カラム名がIDの時はソートボタンをrenderするという実装にしました。

const renderSortButton = () => {
 const sortTarget = [...document.querySelectorAll(".js-table-title")].filter(el => el.textContent === "ID");

 if(sortTarget) {
  sortTarget.forEach(el => {
   el.classList.add("is-target");
   el.insertAdjacentElement('beforeend', createSortButtons());
  })
 }
};

sortTargetとして、”ID”でフィルターをかけています。

const sortTarget = [...document.querySelectorAll(".js-table-title")].filter(el => el.textContent === "ID");

ここで、

(省略).filter(el => el.textContent === "ID" || el.textContent === "年齢");

とすることで、ボタン数を増やすことができます。(次の課題..)

ボタンの初期表示

defalutのボタンが表示されるように設定しました。

data-button-status="default"属性のついているボタンにis-activeクラスをつけました。

const setButtonForInitDisplay = () => {
 const defaultButtons = [...document.querySelectorAll('[data-button-status="default"]')];
 defaultButtons.forEach(button => {
  button.classList.add("is-active");
 })
}

querySelectorAllを使用しているのは、IDソート以外にもソートの種類が増えた時のことを考えたからです。

(今回はそこまで考えなくてよかったな…)

ソートされたデータを作成

クリックされたカラム名を取得→そのカラム名でデータをソートして返す

という方法を考えました。

今回はカラム名は固定ですが、他のカラム名の場合にも成り立つからです。

 

まず、カラム名をクリックしたことでactiveになる<button>を取得、

そこからclosest()メソッドを使って祖先要素の<th>を取得し、textContentすることでカラム名が取得できます。

  const activeButton = document.querySelector(".is-active");
 const currentColumn = activeButton.closest(".js-table-title");
 const currentColumnName = currentColumn.textContent;

次に、そのカラム名をJSONから取得したデータのキーに変換します。

const key = Object.keys(tableTitlesData).find(key => tableTitlesData[key] === currentColumnName);

ここで出てくるtableTitlesDataとは、JSONから取得したデータのキーを、カラム名に変換するオブジェクトです。

const tableTitlesData = {
 "userId": "ID",
 "name": "名前",
 "gender": "性別",
 "age": "年齢"
}

ここから、カラム名で検索した「ID」に該当するキー「userId」を検索しました。

最後に「userId」キーで昇順・降順のデータにソートしていきます。

const currentButtonStatus = activeButton.dataset.buttonStatus;

if (currentButtonStatus === "asc") {
return [...data].sort((firstEl, secondEl) => firstEl[key] - secondEl[key]);
}
if (currentButtonStatus === "desc") {
return [...data].sort((firstEl, secondEl) => secondEl[key] - firstEl[key]);
}
return data;
元のメンバーデータをスプレッド構文でシャローコピーしてsort()しました。

sort()メソッドは元の配列を壊してしまうためコピーが必要です。

もともとslice()を使用していましたが、スプレッド構文が使えるとレビューで教えていただきました!

 /* 最初の書き方 */
 const copyDataForAsc = data.slice();
 const copyDataForDesc = data.slice();

シャローコピー・ディープコピーについても学びました。

今回は、オブジェクトの1階層までのコピーで問題なかったのでシャローコピーであるスプレッド構文を使用することができました。

2階層以下まで完全なコピーが必要な場合は、スプレッド構文は使用できません。(2階層以下は、元のオブジェクトを参照しているため)

//

また、if ~ elseif文で書くのは好ましくないという点もレビューいただき、修正しています。

実際の挙動としてはelse文の中でさらにif節とelse節のネストが生成されているのと等しい
(引用:すっきり書きたいJavaScriptの条件分岐

ソードボタンをクリックすると昇順→降順→標準に変化させる

ボタンをクリックすると以下のように変化するようにします。

元のコードではif文を使用していましたが、レビューでswitch文を知りました。

const switchSortButtons = () => {
 const activeButton = document.querySelector(".is-active");
 const sortStatus = activeButton.dataset.buttonStatus;

 activeButton.classList.remove("is-active");

 switch (sortStatus) {
  case "default":
   document.querySelector("[data-button-status='asc']").classList.add("is-active");
   break;
  case "asc":
   document.querySelector("[data-button-status='desc']").classList.add("is-active");
   break;
  default:
   document.querySelector("[data-button-status='default']").classList.add("is-active");
   break;
 }
};
switch( )の式が、case: と等しいかを上から順に調べて、当てはまらなかったらdefault: が実行されます。
ケースがいくつかあるときは if文で書くよりも可読性が上がると思いました。

クリックイベント

最後にボタンをクリックした時のイベントを作成しました。

3つのボタンを持つ親 <ul> = js-sort-buttonsクラスに対してイベントを定義します。

<ul>をクリックすると、

switchSortButtons()
② それぞれのメンバーの<tr>を取得
③ <tr>の数だけchangeTableContents()で<td>を書き換える

ロジックを考えました。

const addEventListenerForSortButtons = (data) => {
 const sortButtons = document.getElementById("js-sort-buttons");

 sortButtons.addEventListener("click", () => {
  switchSortButtons();

  const trArray = [...document.querySelectorAll(".js-table-row")];
  const sortData = createSortingData(data);

  trArray.forEach((tr, index) => {
   const tdItems = tr.children;
   changeTableContents(tdItems, sortData[index]);
  })
 });
};

changeTableContents()には、それぞれのメンバーの<td>とソート済みのデータを渡します。

const changeTableContents = (items, data) => {
 const titleKeys = Object.keys(tableTitlesData);

 for(let i = 0; i < titleKeys.length; i++){
  items[i].textContent = data[titleKeys[i]];
 }
};

 

今回のコード

//
学習に使用している本は、もりけん先生推奨の”JavaScript本格入門”です。

あとがき

さえさん(@sae_prog)、まいさん(mai2022web)レビューいただきありがとうございました!

switch文やスプレッド構文を使用したデータのコピーなど、新しい知識をたくさん得ることができました。

お時間を割いていただきありがとうございました!

今日は以上です。

//

【もりけん塾で勉強しています】

もりけん先生(@terrace_tec)のHPはこちら