もりけん塾 JavaScript課題22 JavaScriptで作成したテーブルに複数の要素のソート機能をつける

もりけん塾 JavaScript課題22 JavaScriptで作成したテーブルに複数の要素のソート機能をつける

こんにちは。もりけん塾で、JavaScriptを勉強中のまいです。今回は、もりけん塾のマークアップエンジニアの方がフロントエンドエンジニアになる為の課題22にチャレンジしたので、学習した内容を記録に残します。

課題の仕様

前回までの仕様は下記です。

課題21の仕様

1.テーブルを画面遷移してから3秒後に解決されるPromiseが返すオブジェクトを元に作り、 idがソートできる機能を作ってください

2.ソートは通常時はidが適当でもよく
3.ソートが昇順の場合は上矢印がアクティブ、下矢印がdisabled、1,2,3,4,5の順番で表示され、降順の場合はその逆、通常時の矢印クリック(クリッカブル領域は2つの矢印です。
上下別々のクリッカブル領域でではなく)を押すと画像のように変化します

課題22の仕様

・同じことを年齢でもやってください。

コードレビュー

最初、私はソートする要素を配列で指定していました。

const sortCategories = ["id", "age"];

しかし、ソートする要素が増える度に、書き換える必要があるため、下記のようなtableColumnConfigのオブジェクトを作成して管理する方法を教えていただきました。全ての要素が、同じソートに関わるプロパティを持っているので、一目瞭然で、管理が楽です。

const tableColumnConfig = {
  id: {
    value: "ID",
    hasSort: true
  },
  name: {
    value: "名前",
    hasSort: false
  },
  gender: {
    value: "性別",
    hasSort: true
  },
  age: {
    value: "年齢",
    hasSort: false
  }
};

どちらかのソートボタンが押されたら、もう一方のソートボタンをデフォルトの戻す動きを実装しました。セレクタのあたりなどが、やや冗長になったので、もう少しいい方法があるかもしれません。

const addIsNotClickedForSortButtonsBox = (currentTarget) => {
    const sortButtons = document.querySelectorAll(".js-sortButtons-Box");
    for(const button of sortButtons){
        button.classList.add("is-not-clicked");
    }

    currentTarget.classList.remove("is-not-clicked");
}

const resetStatusForSortButton = () => {
    const sortButtons = document.querySelectorAll(".is-not-clicked > .js-sortButtons-Item");
    sortButtons.forEach((button) => {
        const state = button.getAttribute("data-state");
        (state !== "both") ? button.classList.add("hidden") : button.classList.remove("hidden");
    });
}

ここでは、mapを使ってtableColumnConfigを配列オブジェクトにし、フィルタリングしています。
が、ちょっと面倒な気がします。

const filterSortCategories = () =>{
    const column = Object.entries(tableColumnConfig).map(([key,value])=>({key, value}));
    const sortCategories = column.filter((a) => a.value.hasSort );
    return sortCategories;
}

もっといい方法があれば、ぜひ教えてください、と聞いてみたところ、

ソートカテゴリかどうかだけ分かればいいのであれば、フィルタ処理を入れるのではなく、createSortButtonsでクリックイベントを追加してはいかがでしょうか?
私が実装した時も同じ問題がありました。

という回答をいただきました。そこで、フィルター処理はやめて、ソートボタンを作成する関数の引数にいれることにしました。

ボタンクリックでソートする関数は、下記のようになりました。

const addClickEventToSortTableBody = (sortButtonsBox,category,usersData)  => {
    sortButtonsBox.addEventListener("click", (e) => {
        if(e.currentTarget === e.target) return;

        addIsNotClickedForSortButtonsBox(e.currentTarget);
        resetStatusForSortButton();

        const currentState = e.target.getAttribute("data-state");
        const nextState  = changeState(currentState);
        const nextButton = sortButtonsBox.querySelector(`[data-state=${nextState}]`);

        toggleHiddenClassForSortButton(e.target,nextButton);
        sortUsersData(category, usersData, nextState);
    });
} 

usersDataという引数を、いろいろな関数で引き回してしまっていたので、バケツリレーのような状態になっていました。

そこで、下記のように最初にtableConfigのオブジェクトに、usersDataを設定しておくことで、バケツリレーを防ぐ方法を教えていただきました。

const tableConfig = {
  column: {
    id: {
      value: "ID",
      hasSort: true
    },
    name: {
      value: "名前",
      hasSort: false
    },
    gender: {
      value: "性別",
      hasSort: false
    },
    age: {
      value: "年齢",
      hasSort: true
    }
  },
  usersData: null  //here!!
};

最終的なコード

最終的なコードはこちらです。これで、idもageもソートができるようになりました。

<!DOCTYPE html>
<html lang="ja" class="text-gray-500">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Lesson22</title>
  </head>
  <body class="font-body">
    <div class="m-5">
      <table id="js-table" class="min-w-full text-center">
      </table>
    </div>
    <script type="module" src="./js/main.js"></script>
  </body>
</html>
import '../css/style.css'
import * as loading from "./module/loading";
import { createAttributedElements} from "./utils/createAttributedElements";

const endPointURL = {
    usersData: "https://myjson.dit.upm.es/api/bins/i55y"
}

const renderErrorMessage = (error,element) => {
    const errorMessage = createAttributedElements({
        tag:"p",
        valuesByAttributes:{
            class:"error-message"
        },
        str:error
    });
    element.appendChild(errorMessage);
}

const fetchData = async (endpointURL,element) => {
    const response = await fetch(endpointURL);
    if(!response.ok){
        console.error(`${response.status}:${response.statusText}`);
        renderErrorMessage("Communication with the server is broken.",element);
        return;
    }
    return await response.json();
}

const fetchContentsData = (endPointURL,element,ms) => new Promise(resolve => setTimeout(() => resolve(fetchData(endPointURL,element)),ms));

const table = document.getElementById("js-table");

const initUsersData = async () => {
    loading.showLoading(table);
    try{
        const json = await fetchContentsData(endPointURL.usersData,table,500);
        const usersData = json.data;

        if (!usersData.length) {
            renderErrorMessage("No user.",table);
            return;
        }
        tableConfig.usersData = usersData;
        renderTableElements();
    }catch(error){
        console.error(error);
    }finally{
       loading.removeLoading(table);
    }
}

const renderTableElements = () => {
    renderTableHeader();
    renderTableBody(tableConfig.usersData);
}

const tableConfig = {
    column: {
      id: {
        value: "ID",
        hasSort: true
      },
      name: {
        value: "名前",
        hasSort: false
      },
      gender: {
        value: "性別",
        hasSort: false
      },
      age: {
        value: "年齢",
        hasSort: true
      }
    },
    usersData: null
};

const renderTableHeader = () => {
    const thead = document.createElement("thead");
    const tr = document.createElement("tr");
    thead.className = "bg-slate-500";

    for (const [columnKey, columnValue] of Object.entries(tableConfig.column)) {
        const th = createAttributedElements({
            tag:"th",
            valuesByAttributes:{
                class:"text-sm text-white px-6 py-4",
            },
            str:columnValue.value
        });

        tr.appendChild(th);

        if (columnValue.hasSort) {
            th.appendChild(createSortButtons(columnKey));
        }
    };

    table.appendChild(thead).appendChild(tr);
}

const renderTableBody = usersData => {
    const tbody = document.createElement("tbody");
    tbody.id = "js-usersTableBody";

    for(const user of usersData){
        const fragment = document.createDocumentFragment();
        const tr = document.createElement("tr");

        Object.keys(tableConfig.column).forEach((column) => {
            const td = createAttributedElements({
                tag:"td",
                valuesByAttributes:{
                    class:"border border-gray-300 px-4 py-2"
                },
                str:user[column]
            })
            fragment.appendChild(tr).appendChild(td);
        });

        table.appendChild(tbody).appendChild(fragment);
    }
}

const sortButtonAttributes = [
    {state:"asc",src:"../img/asc.svg", alt:"asc-image"},
    {state:"desc",src:"../img/desc.svg",alt:"desc-image"},
    {state:"both",src:"../img/both.svg",alt:"both-image"}
]

const createSortButtons = (columnKey) => {
    const sortButtonsBox = createAttributedElements ({
        tag:"div",
        valuesByAttributes:{
            id:`js-${columnKey}Buttons-Box`,
            class: "w-10 inline-block align-middle js-sortButtons-Box"
        }
    })

    const fragment = document.createDocumentFragment();

    for(const button of sortButtonAttributes){
        const sortButton = createAttributedElements({
            tag:"button",
            valuesByAttributes:{
                class: `js-sortButtons-item`,
                'data-state':button.state
            }
        });

        button.state !== "both" && sortButton.classList.add("hidden");

        const sortButtonImage = createAttributedElements({
            tag:"img",
            valuesByAttributes:{
                class:"pointer-events-none",
                src:button.src,
                alt:button.alt
            }
        })

        fragment.appendChild(sortButton).appendChild(sortButtonImage);
    }

    sortButtonsBox.appendChild(fragment);
    addClickEventToSortTableBody(sortButtonsBox,columnKey);

    return sortButtonsBox;
}

const toggleHiddenClassForSortButton = (currentButton,nextButton) => {
    currentButton.classList.add("hidden");
    nextButton.classList.remove("hidden");
}

const updateTableBody = usersData =>  {
    document.getElementById("js-usersTableBody").remove();
    renderTableBody(usersData);
};

const sortUsersData = (category, state) => {
    const cloneUsersData = [...tableConfig.usersData];

    switch (state) {
        case "asc":
            cloneUsersData.sort((a, b) => a[category] - b[category]);
            updateTableBody(cloneUsersData);
            break;
        case "desc":
            cloneUsersData.sort((a, b) => b[category] - a[category]);
            updateTableBody(cloneUsersData);
            break;
        default:
            updateTableBody(tableConfig.usersData);
    }
}

const changeState = (currentState) => {
    switch (currentState) {
      case "asc":
        return "desc";
      case "desc":
        return "both";
      default:
        return "asc";
    }
}

const addIsNotClickedForSortButtonsBox = (currentTarget) => {
    const sortButtons = document.querySelectorAll(".js-sortButtons-Box");
    for(const button of sortButtons){
        button.classList.add("is-not-clicked");
    }

    currentTarget.classList.remove("is-not-clicked");
}

const resetStatusForSortButton = () => {
    const sortButtons = document.querySelectorAll(".is-not-clicked > .js-sortButtons-item");
    sortButtons.forEach((button) => {
        const state = button.getAttribute("data-state");
        (state !== "both") ? button.classList.add("hidden") : button.classList.remove("hidden");
    });
}

const addClickEventToSortTableBody = (sortButtonsBox,category)  => {
    sortButtonsBox.addEventListener("click", (e) => {
        if(e.currentTarget === e.target) return;

        addIsNotClickedForSortButtonsBox(e.currentTarget);
        resetStatusForSortButton();

        const currentState = e.target.getAttribute("data-state");
        const nextState  = changeState(currentState);
        const nextButton = sortButtonsBox.querySelector(`[data-state=${nextState}]`);

        toggleHiddenClassForSortButton(e.target,nextButton);
        sortUsersData(category, nextState);
    });
} 


initUsersData();

今回も根気よくレビューしていただいたみなさんに感謝です!

次回の課題は、会員登録画面を作成する課題です。

私が所属しているフロントエンドエンジニアを目指す方のための塾 「もりけん塾」の森田賢二先生のTwitterはこちら!

先生のブログ「武骨日記」はこちら!

https://kenjimorita.jp/

まい

Webサービス制作会社で、Wordpressのテーマ開発や2Dシミュレーターの開発、JavaScriptを使用したフロントエンド周りの実装を担当しています。 JavaScriptが好きです。 最近は、3D Model Configuratorの制作にチャレンジしています。

JavaScriptカテゴリの最新記事