もりけん塾 JavaScript課題20 ユーザーデータをテーブルに出力する

もりけん塾 JavaScript課題20 ユーザーデータをテーブルに出力する

こんにちは!もりけん塾のJavaScript課題20にチャレンジしました。今回は、ユーザーデータのjsonを作成し、下記のようなテーブルに出力するという課題です。

ID名前性別年齢
1山田28
3斉藤48
5榎本45
2鈴木38
4遠藤50

課題20の仕様

仕様はこちら

  • こちらのようなテーブルを画面遷移してから3秒後に解決されるPromiseが返すオブジェクトを元に作ってください。
  • カラム名など(id, 名前等)もdataで表現して受け取り、フロント側で加工して表示すること
  • APIデータのidはリアルではない採番や、別idを付与しても良い
  • その時、idは要素の並び順でつけず以下のように順不同にしてください
ex.) 
[{id: 3}, {id: 2}, {id: 1}]
- 受け取った順不同のデータは並び替え不要です。そのまま表示してください

TailwindCSSを使ってみる

もりけん塾では、9月から TailwindCSSが必須となります。すでに使われている方もいるので、私も今回の課題からチャレンジしてみました!

TailwindCSSは、いわゆるCSSを書かずとも画面のレイアウトが作成できる、CSSのフレームワークです。簡単なものであればカスタマイズしなくても綺麗なUIを作成することができ、実装の手間を減らすことができます。

CSSを簡単に書きたいなら、Bootstrapでいいのでは?と思うかもしれませんが、BootstrapとTailwindCSSは性質が結構違います。Bootstrapの場合、ボタンのクラス名に、btnとつけるとボタン用のTailwindCSSは、ユーティリティファーストのCSSフレームワークと謳っており、HTML構造に影響されにくい性質を持っています。

また、メディアクエリや状態に応じたCSSクラスをHTMLに書くことができるという、自由度が高いものになっています。自分用のテンプレートを作ることも可能です。

HTMLでのTailwindCSSの記述はこんな感じです。

<div class="md:flex bg-glay rounded-lg p-6">
  <img class="h-16 w-16 md:h-24 md:w-24 rounded-full mx-auto md:mx-0 md:mr-6" src="avatar.jpg">
  <div class="text-center md:text-left">
    <h2 class="text-lg">Mai Nakagawa</h2>
    <div class="text-purple-500">Web Designer</div>
    <div class="text-gray-600">nakagawa@example.com</div>
    <div class="text-gray-600">(555) 765-4321</div>
  </div>
</div>

上記のようなクラスをHTMLに直接書くと、下記のようなレイアウトが実現できます。

PC時
スマホ時

TailwindCSSは冗長?

初めは、通常のCSSに比べると、TailwindCSSのクラス名の多さに驚くかも知れません。そして、通常のCSSでは、いわゆるダメなクラス名とされているようなクラス名が登場します。BEMなどのクラス名を重要視したCSSを使ってきた人にとっては、見た目の奇妙さ、クラスの多さに対する抵抗がかなり出そうな印象です。

TailwindCSSの場合は、通常のCSSを理解していいるレベルであれば、学習コストが低いです。クラス名=CSSのプロパティになっているので、直感的に使用することが可能です。

<div class="text-purple-500">Web Designer</div>

クラス名の命名から解放される喜び

TailwindCSSには、ほとんどのCSSプロパティに対応するCSSクラスが用意されているので、HTMLに集中することができます。新しい要素を追加するたびにクラス名を考えないといけない、という時間から解放されます。

CSSのクラス名を考える時間を減らすことができ、レイアウトを崩す心配もなくなる、結果的にスピーディな実装ができるという点でメリットが多いCSSフレームワークといえます。

使ってみた感想としては、CSSのクラス名を考える手間が少なくなって、使えば使うほど、便利だと感じています。仕事のCSSもTailwindCSSにしたい。

グローバルなスタイルの追加

TailwindCSSに独自のグローバルなベーススタイルを追加することもできますが、TailwindCSSのドキュメントでは、htmlやbodyにグローバルなスタイルを適用させたいだけの場合は、HTMLにclassを書くことをすすめています。

HTMLでクラスを使う

htmlまたはbody要素にグローバルスタイルを適用したいだけの場合は、新しいCSSを作成する代わりに、HTMLのこれらの要素に既存のクラスを追加することを検討してください。

tailwindCSS : https://tailwindcss.jp/docs/adding-base-styles

独自のCSSを追加したいわけではなく、グローバルなCSSを追加したいだけなら、こんな感じで直接既存のクラスを追加してくださいね、ということみたいです。

TailwindCSSの既存クラスで事足りる場合は、あえてベーススタイルを追加する必要はありません。

<!DOCTYPE html>
<html lang="ja" class="text-gray-900">
  <!-- ... -->
  <body class="min-h-screen bg-gray-100">
    <!-- ... -->
  </body>
</html>

独自のスタイルの追加

既存クラスのみでは、ちょっと難しい、複数の既存クラスをまとめておきたいという場合は、CSSに追加の記述をすること実現可能です。@applyに、複数の既存クラスをまとめておき、HTMLで使うこともできます。

@tailwind base;
@tailwind components;
@tailwind utilities;

@layer base {
  h1 {
    @apply text-2xl;
  }
  h2 {
    @apply text-xl;
  }
}

反映させたいファイルは、パスをtailwind.config.jsのcontentに書く

TailiwindCSSを反映させるためには、tailwind.config.jsにパスを書く必要があります。ファイルの拡張子もまとめて記述しておくと楽です。

module.exports = {
  content: [
    "./src/**/*.{html,js}", //ここを追加
  ],
  theme: {
    extend: {},
  },
  plugins: [],
}

JavaScriptでもTailwindCSSのクラス名を追加すれば、反映できるようになります。

valuesByAttributes:{
    class:"text-sm text-white px-6 py-4",

オブジェクトのキー、値を取得する

オブジェクトのキーと値は、Object.keys()とObject.values()で取得することが可能です。

Object.keys()は、指定されたオブジェクトが持つプロパティ名(キー)を、通常のループ処理と同じ順番で配列にして返します。

Object.values()は、指定されたオブジェクトが持つ列挙可能なプロパティの値を、 for...inと同じ順序で、配列にして返します。

const user = {
    "id"     : "user0001",
    "name"   : "なかがわ",
    "gender" : "女",
    "age"    : 35
}

//オブジェクトのプロパティ名(キー)を取得
const userKeys = Object.keys(user); 
console.log(userKeys); //["id", "name" , "gender", "age"];

//オブジェクトのプロパティの値を取得
const userValues = Object.values(user);
console.log(userValues) //["user0001", "なかがわ", "女", 35 ];

課題20で実装したテーブル

今回、実装したテーブルはこのようなものです。APIからデータを受け取り、表に出力しています。カラム名は、フロント側で作成しています。

オブジェクトの配列をHTMLに出力する

ユーザーデータのJSONはこのように、配列のオブジェクトになっています。

"data": [
  {"id": "3","name": "田中","gender": "男","age": 25},
  {"id": "5","name": "榎本","gender": "女","age": 36},
  {"id": "2","name": "佐藤","gender": "女","age": 18},
  {"id": "4","name": "安藤","gender": "女","age": 48},
  {"id": "1","name": "山田","gender": "男","age": 32}
]

配列の場合は入力した順に列挙されますが、オブジェクトの場合は、順番は保証されず、実行環境に影響をうけて、ランダムに列挙されることがあります。

["ID","名前","性別","年齢"] //配列 入れた順番通りに列挙される
{"id": "3","name": "田中","gender": "男","age": 25} //オブジェクト キーの順番は保証されない

このままだとテーブルのカラム通りの順番(id,name,gender,ageの順)に列挙されるとは限らないので、
・ユーザーデータを表示するテーブルのカラム用のオブジェクトを作る
・カラム用のオブジェクトのキーを、Object.keys()で配列にする
・カラム用のキーの配列の順番で、ユーザーデータの値をループ処理する
という処理にしました。

const UsersTableColumn = {
    "id"     : "ID",
    "name"   : "名前",
    "gender" : "性別",
    "age"    : "年齢"
}
const renderTableBody = (usersData) => {
    const tbody = document.createElement("tbody");

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

        Object.keys(UsersTableColumn).forEach((column) => {  //UsersTableColumnのキーを配列に
            const td = createAttributedElements({
                tag:"td",
                valuesByAttributes:{
                    class:"border border-gray-300 px-4 py-2"
                },
                str:user[column] // UsersTableColumnのキーによる配列の順番で出力
            })
            fragment.appendChild(tr).appendChild(td);
        });

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

このようにすることで、ユーザーの持つidの値が、期待通りの順番で列挙されました。

最終的なコード

最終的なコードはこちらです。

<!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>Lesson20</title>
  </head>
  <body class="font-body">
    <div class="m-10">
      <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 createErrorMessage = (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}`);
    createErrorMessage("Communication with the server is broken.", element);
    return;
  }
  const json = await response.json();
  return json.data;
};

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

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

const initUserData = async () => {
  loading.showLoading(table);
  try {
    const usersData = await fetchContentsData(
      endPointURL.usersData,
      table,
      3000
    );
    if (!usersData.length) {
      createErrorMessage("No user.", table);
      return;
    }
    renderTableContents(usersData);
  } catch (error) {
    console.error(error);
  } finally {
    loading.removeLoading(table);
  }
};

const renderTableContents = (usersData) => {
  renderTableHedaer();
  renderTabelBody(usersData);
};
const UsersTableColumn = {
  id: "ID",
  name: "名前",
  gender: "性別",
  age: "年齢"
};

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

  Object.values(UsersTableColumn).forEach((column) => {
    const th = createAttributedElements({
      tag: "th",
      valuesByAttributes: {
        class: "text-sm text-white px-6 py-4"
      },
      str: column
    });
    tr.appendChild(th);
  });

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

const renderTabelBody = (usersData) => {
  const tbody = document.createElement("tbody");

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

    Object.keys(UsersTableColumn).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);
  }
};

initUserData();

今回学んだこと

  • TailwindCSSの使い方
  • Object.keysとObject.valuesの使い方
  • mapとforEachの違い

次回は今回作ったテーブルにソート機能を実装するので、楽しみです。

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

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

https://kenjimorita.jp/

まい

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

JavaScriptカテゴリの最新記事