【もりけん塾】JavaScript課題16 -Vanilla JSでタブコンテンツを作る①-

JavaScript

もりけん塾のJavaScript課題16 で、Vanilla JSを使用したタブコンテンツ作りにチャレンジしています。今回は第一弾として、最低限の機能を実装したのでアウトプットしていきます。

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

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

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

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

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

【もりけん塾】JavaScript課題16 -Vanilla JSでタブコンテンツを作る①

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

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

 

【今回は、下記の仕様を実装しました】

  • それぞれのカテゴリタブを開くことができてそれぞれのジャンルに応じた記事が4つ表示できる。(記事のタイトル名は適当)
  • カテゴリタブは切り替えられる。面倒なら2つのカテゴリだけでよいです。その場合ニュースと経済だけにします
  • htmlはulだけ作ってあとはcreateElementで作る
  • CSSはなしで良い。上記機能要件だけ満たしていればいい

JSONのデータ構造を考える

仕様を満たすタブコンテンツを作成するために、どのようなデータがあればコンテンツが完成するのかを自分で考えてJSONデータを作りました。

タブのカテゴリーは、

  • news
  • economy
  • entertainment
  • domestic

の4つ。

それぞれにarticlesとして、タブコンテンツに表示させる記事データを入れました。

{ 
 "data": [
  { 
   "id": "81f55329-9212-41b6-a71b-c7c02bcdee54",
   "category": "news",
   "display": true,
   "img": "/img/img-news.png", 
   "articles": [ 
    { 
     "id": "89e779dd-a096-4ef4-b810-9a8f0a80ef18",
     "date": "2021-10-30", 
     "title": "news title 01",
     "comments": [ ] 
    }, 
    { 
     "id": "89e779dd-a096-4ef4-b810-9a8f0a80ef19",
     "date": "2021-11-30", "title": "news title 02",
     "comments": [ 
       { "id": "6b999dda-f0cb-443a-abe1-9158cc537ad9", 
        "name": "takeda", 
        "text": "It's so hard, my head is going to explode." 
       }, 
       { 
        "id": "6b999dda-f0cb-443a-abe1-9158cc537ad8", 
        "name": "yamada", 
        "text": "It's so hard, my head is going to explode." 
       }, 
       { 
        "id": "6b999dda-f0cb-443a-abe1-9158cc537ad7",
        "name": "takahashi", 
        "text": "It's so hard, my head is going to explode." 
       } 
      ] 
     }, 
    {
      "id": "89e779dd-a096-4ef4-b810-9a8f0a80ef20", 
     "date": "2021-12-30", 
     "title": "news title 03", 
     "comments": [ ] 
    }, 
    { 
     "id": "89e779dd-a096-4ef4-b810-9a8f0a80ef21", 
     "date": "2022-1-3", 
     "title": "news title 04", 
     "comments": [ ] 
    } 
   ] 
  }, 
  { 
   "id": "91f55329-9212-41b6-a71b-c7c02bcdee54", 
   "category": "economy", 
   "display": false, 
   "img": "/img/img-economy.png", 
   "articles": [ 
    { 
     "id": "99e779dd-a096-4ef4-b810-9a8f0a80ef18", 
     "date": "2021-10-30", 
     "title": "economy title 01", 
     "comments": [ ] 
    }, 
    { 
     "id": "99e779dd-a096-4ef4-b810-9a8f0a80ef19", 
     "date": "2021-11-30", 
     "title": "economy title 02", 
     "comments": [ 
      { "id": "5a999dda-f0cb-443a-abe1-9158cc537ad9", 
       "name": "takeda", 
       "text": "It's so hard, my head is going to explode." 
      }, 
    { 
          ・
      ・
          ・
          ・
          ・

このような感じで作成しました。これを作るだけでも調べながらすごい時間がかかって、先に進まれた塾生さん達すごい・・・となりました。

dateは、2021/11/30のように書いていたのですが、もりけん先生からDateオブジェクトにするように助言をいただき、2021-11-30に直しました。(jsファイル側でDate()関数を使用して日付オブジェクトに変換するという理解だったけれど、どうやらこれも違ったらしい・・・ちょっと保留でまた調べなければとなっている・・・)

HTML

今回は、仕様に”htmlはulだけ作ってあとはcreateElementで作る”とあったため、<ul> だけの状態からスタートです。

<!DOCTYPE html>
<html lang="ja">
 <head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <meta http-equiv="X-UA-Compatible" content="ie=edge">
  <link rel="stylesheet" href="./css/reset.css" type="text/css">
  <link rel="stylesheet" href="./css/style.css" type="text/css">
  <script src="./index.js" defer></script>
  <title>TerraceTechフロントエンドエンジニア養成所</title>
 </head>
 <body>
  <ul class="tab__nav-list" id="js-tabNav"></ul>
 </body>
</html>

普段の実装では、htmlに元々要素がある状態での実装になると思うのですが、今回はDOMを扱う練習になりました!

Array.map()

今回は上に書いたような階層の深いJSONデータを作成したため、下層のデータにアクセスするのにとても苦労しました。

調べている中でArray.map()メソッドにたどり着きました。

MDNによると

map() メソッドは、与えられた関数を配列のすべての要素に対して呼び出し、その結果からなる新しい配列を生成します

これを使用して、下層データを配列化してみようと思いつきました。

まず、JSONデータをfetchして、dataという名前のついた配列オブジェクトを返す関数を作ります。

async function fetchData() {
 const api = "https://myjson.dit.upm.es/api/bins/6u5z";
 const response = await fetch(api);
 const json = await response.json();
 return json.data;
}

これを変数data として、mapメソッドを使用して下層データのarticles部分を集めた配列を作成していきます。

async function fetchArticleData() {
 const data = await fetchData();
 const articles = data.map(value => value.articles);
 return articles;
}

mapを初めて使用してみました。
ここで、mapの引数にはcallback関数が入ります。

 const articles = data.map(value => value.articles);

MDNには

map は、与えられた callback 関数を配列の順番通りに、各要素に対して一度ずつ呼び出し、その結果から新しい配列を生成します。

とあり、

4カテゴリーの記事の内容をそれぞれ抜き出して(=JSONの下層データ)、新しい配列を作成しました。

変数articlesをconsole.logで表示させてみると、4つの配列が抜き出せており、それぞれの中にarticlesの記事情報が4つ入っている状態です。

Array0〜3の中身は、articlesの記事情報であることが確認できました。

これで、変数articlesというデータの中から、JSONの下層データにアクセスが可能になりました。

上層データは、data.articlesのように簡単にアクセスできるのに対して、例えばさらに下の階層のarticlesの中のtitleにアクセスしようとするととても大変だということを知りました・・・。(他に方法があるのだろうか・・・)

タブコンテンツに表示する記事タイトルを作成する

タブのコンテンツは、選択したタブカテゴリーの記事(タイトル)を4つずつ表示します。

データは先ほどのfetchArticleData関数から取ってきます。

async function fetchArticleData() {
 const data = await fetchData();
 const articles = data.map(value => value.articles);
 return articles;
}

記事タイトルを作成する関数を作成してみました。中身がかなり冗長になってしまった・・・

ここを考えるのに、禿げるほど時間を費やしました・・私の考えたコードは以下です。

async function createArticleTitle() {
 const values = await fetchArticleData();
 const tabContents = document.getElementById("js-tabContents");
 const tabContentsInner = document.getElementById("js-tabContentsInner");

 //①記事データの数だけulを作成
 for(let i = 0; i < values.length; i++){
  const ul = document.createElement("ul");
  const articleTitle = values[i].map(value => value.title);

  ul.id = "js-tabContentsList"+`${i+1}`;
  ul.classList.add("tab__contents-list");

  const fragment = document.createDocumentFragment();

  //②記事タイトルの数だけliとaを作成
  for(let i = 0; i < articleTitle.length; i++) {
   const li = document.createElement("li");
   const a = document.createElement("a");

   li.classList.add("tab__contents-item");
   a.classList.add("tab__contents-link");
   a.href = "#";
   a.insertAdjacentHTML("beforeend", articleTitle[i]);

   li.appendChild(a);
   fragment.appendChild(li);
  }
  tabContents.appendChild(tabContentsInner).appendChild(ul).appendChild(fragment);
 }

 //今回のPRで「どのカテゴリタブを初期表示時に選んでいるかはデータとして持っている」を実装していないため、以下を記述
 const tabContentsItem = document.getElementById("js-tabContentsList1");
 tabContentsItem.classList.add("is-show");
}

createArticleTitle()関数の中では、

①articlesという配列データの数だけ、タブのコンテンツ(<ul>)を作成し
②articlesの中の要素数(=記事タイトルの数)だけ、記事タイトルを表示させる <li>・<a>を作成しました。

まず、①です。

//①記事データの数だけulを作成
 for(let i = 0; i < values.length; i++){
  const ul = document.createElement("ul");
  const articlesTitle = values[i].map(value => value.title);

  ul.id = "js-tabContentsList"+`${i+1}`;
  ul.classList.add("tab__contents-list");

ここでいう valuesはconst values = await fetchArticleData();
いわゆる下層データ(JSONのarticles: 以下)を集めた配列データのことを指しています。

この配列データの数だけfor文を回して、<ul> を作ります。

変数articlesTitleで定義しているのは、タイトルを取得して新たに作った配列です。
次の②で使うためにここに書きました(ここに書くのは唐突すぎて微妙だなと思いつつも・・)

const articlesTitle = values[i].map(value => value.title);

次に②です

const fragment = document.createDocumentFragment();

  //②記事タイトルの数だけliとaを作成
  for(let i = 0; i < articlesTitle.length; i++) {
   const li = document.createElement("li");
   const a = document.createElement("a");

   li.classList.add("tab__contents-item");
   a.classList.add("tab__contents-link");
   a.href = "#";
   a.insertAdjacentHTML("beforeend", articleTitle[i]);

   li.appendChild(a);
   fragment.appendChild(li);
  }
  tabContents.appendChild(tabContentsInner).appendChild(ul).appendChild(fragment);
 }

 //TODO 今回のPRで「どのカテゴリタブを初期表示時に選んでいるかはデータとして持っている」を実装していないため、以下を記述
 const tabContentsItem = document.getElementById("js-tabContentsList1");
 tabContentsItem.classList.add("is-show");
}

先ほど考えたArticlesTitleの数だけfor文を回して<li>と<a> を作り、

以下で<a> にタイトル名を追加しました。

a.insertAdjacentHTML("beforeend", articleTitle[i]);

タブの内容を切り替える

最後にタブ内容を切り替えるコードを考えました。

//タブの内容を切り替える
tabNav.addEventListener("click", (e) => {
 const activeTabItem = document.getElementsByClassName("is-active")[0];
 const activeTabContent = document.getElementsByClassName("is-show")[0];
 const tabNavItem = document.getElementsByClassName("tab__nav-button");
 const tabContents = document.getElementsByClassName("tab__contents-list");
 const selectItem = e.target;
 const arrayTabs = Array.prototype.slice.call(tabNavItem);
 const index = arrayTabs.indexOf(selectItem);

 activeTabItem.classList.remove("is-active");//①
 selectItem.classList.add("is-active");//②
 activeTabContent.classList.remove("is-show");//①
 tabContents[index].classList.add("is-show");//②
})

①is-activeクラスとis-showクラスがついているタブ・コンテンツを取得してクラスを外す。

②選択したタブ・コンテンツにis-activeクラスとis-showクラスを付与する。

という内容です。

②の選択したタブを取得するところに苦戦しました。

 const selectItem = e.target;
 const arrayTabs = Array.prototype.slice.call(tabNavItem);
 const index = arrayTabs.indexOf(selectItem);

e.targetで選択した<button>を取得し、

const selectItem = e.target;

何番目のbuttonなのかを知るindexを取得するためにindexOfを使用してみることにしました。(ここは、のちにレビューをいただき、もっと簡略化される・・・!)

const index = arrayTabs.indexOf(selectItem);

indexOf()メソッドは、MDNによると

indexOf() メソッドは引数に与えられた内容と同じ内容を持つ最初の配列要素の添字を返します。存在しない場合は -1 を返します。

とあります。上記コードだと、e.targetと同じタブのindex番号を返します。

arrayTabs.indexOf(selectItem);

ここのarrayTabsには、タブの<button>を配列にして入れてあります。

しかし、最初に以下を配列に入れたときにうまくindexOf()メソッドが機能しませんでした。

const tabNavItem = document.getElementsByClassName("tab__nav-button");

これは、取得したtabNavItemはNodeListであり、これらを配列にしたものは配列風オブジェクトと呼ばれ、配列特有のメソッドを使用できないからでした。

配列オブジェクトを、配列に変換するためにArray.prototype.slice.call()を使用しました。

const arrayTabs = Array.prototype.slice.call(tabNavItem);

slice メソッドを呼び出すことで、配列状オブジェクトやコレクションを新しい配列に変換することができます。(MDNより)

[].slice.call()という短縮の書き方もあるようです。

最初のPRコード

レビュー:同じデータを引数で渡す

もりけん先生(@terrace_tec)にレビューいただきました。

元々2つの関数で、同じデータをfetchしていました(fetchData()

async function fetchArticleData() {
 const jsonData = await fetchData();
 const articleData = jsonData.map(value => value.article);
・
・
・
}

async function createTabNav() {
 const fragment = document.createDocumentFragment();
 const values = await fetchData();
・
・
・
}
これらを引数として渡すことで、データの取得を1度にすることができます。
async function fetchArticleData(values) {
 //const jsonData = await fetchData();
 const articleData = jsonData.map(value => value.article);
・
・
・
}

async function createTabNav(data) {
 const fragment = document.createDocumentFragment();
 //const values = await fetchData();
・
・
・
}

async function addTabContents() {
 const data = await fetchData(); //ここで1度だけfetch

 createTabNav(data);//引数に入れる
 createTabContainer();
 createTabContents();
 createArticleTitle(data);//引数に入れる
}
addTabContents();

レビュー:data属性にindexを指定する

クリックした<button>(タブ)のindexを取得するときに、下記のコードを書きました。

 const selectItem = e.target;
 const arrayTabs = Array.prototype.slice.call(tabNavItem);
 const index = arrayTabs.indexOf(selectItem);

ここのDOMにdata属性でindexを設定してdataset.indexで取得するともっとわかりやすい”とレビューいただきdata属性を勉強しました。

data属性をMDNで調べると

data-* グローバル属性 はカスタムデータ属性と呼ばれる属性の組を作り、HTML と、スクリプトによる DOM 表現との間で、固有の情報を交換できるようにします。

とあります。

<button>要素(タブ)に対して、datasetプロパティでdata-indexという属性をつけました。

以下をfor文の中で使用したので、タブに対して順番にdata-index=”0″ ~”3″が設定されています。

const button = document.createElement("button");
button.dataset.index = `${i}`;

この状態で、先ほどのクリックした<button>(タブ)のindexを取得する場合、以下のようにとても簡潔なコードで表すことができました!

const clickedTabIndex = e.target.dataset.index;

改めてみるとレビュー前のコード↓は、何をやっているのかとてもわかりにくいです・・!

 const selectItem = e.target;
 const arrayTabs = Array.prototype.slice.call(tabNavItem);
 const index = arrayTabs.indexOf(selectItem);

最終的なApproveコード

他にも、変数名やJSONデータについてなどいくつもレビューをいただきました。ありがとうございます。

最終的なコードは以下です。

※使用しているAPI作成ツールのmyjsonがたまにエラーになるため、CodeSandboxもエラーになっていることがありますが、そのままのコードで掲載します。

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

あとがき

今回レビューをしてくださったもりけん先生(@terrace_tec)ありがとうございました!!

最初にPRしたコードがただ動いているだけのかなりの荒さだったのですが、先生の丁寧なレビューに沿ってリファクタリングしていくことでとてもわかりやすいコードになりました。

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

今日は以上です。

//

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

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