もりけん塾 JS課題16に挑戦

もりけん塾 JS課題16に挑戦

もりけん塾にて、JavaScriptの課題に挑戦しています。こちらで挑戦した課題を備忘録としてまとめていきます。

今回挑戦したのは、課題16です。

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

今回追加した機能

これはyahooのトップページを模したUIですが、これをデータとしてどのようなものがサーバーがから返ってくれば実現できそうか、データ構造から考えて、静的なデータ(前回までのようなresolveされると返されるベタ書きのデータ)を作って、画面表示させてみてください。

  • それぞれのカテゴリタブを開くことができてそれぞれのジャンルに応じた記事が4つ表示できる。(記事のタイトル名は適当)
  • それぞれのカテゴリにはそれぞれ固有の画像が入る(右側四角。画像は適当)
  • 記事にはnewという新着かどうかのラベルがつく(どこの記事にそれが入るかは適当でいいです)
  • 記事にはそれぞれコメントがあり、0件なら表示しない、1以上ならアイコンと共に数字が表示される
  • カテゴリタブは切り替えられる。面倒なら2つのカテゴリだけでよいです。その場合ニュースと経済だけにします
  • どのカテゴリタブを初期表示時に選んでいるかはデータとして持っている
  • htmlはulだけ作ってあとはcreateElementで作る
  • try-catchでエラー時はulの中に「ただいまサーバー側で通信がぶっ壊れています」みたいなテキストを画面内に表示すること
  • CSSはなしで良い。上記機能要件だけ満たしていればいい

yahooトップページ

今回悩んだこと

・JSONデータの構造

・タブ切り替えの時、コンテンツを切り替えるのをどうするか

コード

JSONデータについて

作成したJSONデータです

{
    "data": [
        {
            "isInitialDisplay": false,
            "field": "ニュース",
            "img": "https://i.postimg.cc/67dS3c2K/news-img.jpg",
            "contents": [
                {
                    "id": "3a1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6a3",
                    "title": "news記事タイトル1",
                    "date": "2022-04-15",
                    "comments": [
                        {
                            "id": "3a1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6a3-c124",
                            "data": "2022-03-11",
                            "name": "Chris",
                            "comment_content": "Chrisのコメント"
                        },
                        {
                            "id": "3a1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6a3-c110",
                            "data": "2022-03-11",
                            "name": "Zelma",
                            "comment_content": "Zelmaのコメント"
                        }
                    ]
                },
                {
                    "id": "4a1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6a4",
                    "title": "news記事タイトル2",
                    "date": "2022-04-11",
                    "comments": []
                },
                {
                    "id": "1a1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6a1",
                    "title": "news記事タイトル3",
                    "date": "2022-04-10",
                    "comments": [
                        {
                            "id": "1a1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6a1-c021",
                            "data": "2022-03-11",
                            "name": "Alex",
                            "comment_content": "Alexのコメント"
                        },
                        {
                            "id": "3a1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6a3-c100",
                            "data": "2022-03-15",
                            "name": "Zelma",
                            "comment_content": "Zelmaのコメント"
                        }
                    ]
                },
                {
                    "id": "2a1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6a2",
                    "title": "news記事タイトル4",
                    "date": "2022-04-15",
                    "comments": [
                        {
                            "id": "2a1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6a2-c125",
                            "data": "2022-03-15",
                            "name": "Dana",
                            "comment_content": "Danaのコメント"
                        },
                        {
                            "id": "3a1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6a3-c090",
                            "data": "2022-03-15",
                            "name": "Zelma",
                            "comment_content": "Zelmaのコメント"
                        },
                        {
                            "id": "4a1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6a4-c102",
                            "data": "2022-03-12",
                            "name": "Pat",
                            "comment_content": "Patのコメント"
                        },
                        {
                            "id": "3a1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6a3-c153",
                            "data": "2022-03-11",
                            "name": "Chris",
                            "comment_content": "Chrisのコメント"
                        },
                        {
                            "id": "1a1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6a1-c171",
                            "data": "2022-03-11",
                            "name": "Alex",
                            "comment_content": "Alexのコメント"
                        }
                    ]
                }
            ]
        },
        {
            "isInitialDisplay": false,
            "field": "経済",
            "img": "https://i.postimg.cc/bDp52nkt/economy-img.jpg",
            "contents": [
                {
                    "id": "2b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6b2",
                    "title": "経済記事タイトル1",
                    "date": "2022-04-01",
                    "comments": []
                },
                {
                    "id": "1b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6b1",
                    "title": "経済記事タイトル2",
                    "date": "2022-04-15",
                    "comments": [
                        {
                            "id": "1b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6b1-c225",
                            "data": "2022-03-15",
                            "name": "Jamie",
                            "comment_content": "Jamieのコメント"
                        },
                        {
                            "id": "2b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6b2-c201",
                            "data": "2022-03-17",
                            "name": "Hunter",
                            "comment_content": "Hunterのコメント"
                        }
                    ]
                },
                {
                    "id": "4b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6b4",
                    "title": "経済記事タイトル3",
                    "date": "2022-04-05",
                    "comments": [
                        {
                            "id": "4b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6b4-c220",
                            "data": "2022-03-15",
                            "name": "Morgan",
                            "comment_content": "Morganのコメント"
                        },
                        {
                            "id": "1b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6b1-c024",
                            "data": "2022-03-25",
                            "name": "Jamie",
                            "comment_content": "Jamieのコメント"
                        },
                        {
                            "id": "2b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6b2-c011",
                            "data": "2022-03-17",
                            "name": "Hunter",
                            "comment_content": "Hunterのコメント"
                        }
                    ]
                },
                {
                    "id": "3b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6b3",
                    "title": "経済記事タイトル4",
                    "date": "2022-04-12",
                    "comments": [
                        {
                            "id": "3b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6b3-c222",
                            "data": "2022-03-15",
                            "name": "Robin",
                            "comment_content": "Robinのコメント"
                        }
                    ]
                }
            ]
        },
       //以下省略
    ]
}

参考にさせて頂いたサイトがこちらです

JSONデータをどう作成するか…かなり悩んだのですが、

データの集まりを表現するみたいな形で配列を使って書く

dataという塊の中に、記事の内容のデータの塊があって、さらにその中に記事に対するコメントの塊があるという形で作成しました。

"data": [ 
 
    "contents": [ 

      "comments": [ 

        ]
    ]
]

関係性が違うデータを分ける

isInitialDisplay
field
img
contents

関連したデータはできる限りまとめ、関連していないものは分ける

contents内の
id
title
date
comments

さらにcomments内では、
id
date
name
comment_content

JSONデータで設定したidについて

idは、実務ではハッシュ値を使用しているとのことでそれに近いものとしました。
(ハッシュ値について)

isInitialDisplayは、Boolean型にしたこと

dateの日付のスタイルはyear-month-dayの並びとしたこと

できたのが下記です。

{
    "data": [
        {
            "isInitialDisplay": false,
            "field": "ニュース",
            "img": "https://i.postimg.cc/67dS3c2K/news-img.jpg",
            "contents": [
                {
                    "id": "3a1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6a3",
                    "title": "news記事タイトル1",
                    "date": "2022-04-15",
                    "comments": [
                        {
                            "id": "3a1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6a3-c124",
                            "data": "2022-03-11",
                            "name": "Chris",
                            "comment_content": "Chrisのコメント"
                        },
                        {
                            "id": "3a1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6a3-c110",
                            "data": "2022-03-11",
                            "name": "Zelma",
                            "comment_content": "Zelmaのコメント"
                        }
                    ]
                },
                {
                    "id": "4a1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6a4",
                    "title": "news記事タイトル2",
                    "date": "2022-04-11",
                    "comments": []
                },
                {
                    "id": "1a1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6a1",
                    "title": "news記事タイトル3",
                    "date": "2022-04-10",
                    "comments": [
                        {
                            "id": "1a1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6a1-c021",
                            "data": "2022-03-11",
                            "name": "Alex",
                            "comment_content": "Alexのコメント"
                        },
                        {
                            "id": "3a1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6a3-c100",
                            "data": "2022-03-15",
                            "name": "Zelma",
                            "comment_content": "Zelmaのコメント"
                        }
                    ]
                },
                {
                    "id": "2a1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6a2",
                    "title": "news記事タイトル4",
                    "date": "2022-04-15",
                    "comments": [
                        {
                            "id": "2a1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6a2-c125",
                            "data": "2022-03-15",
                            "name": "Dana",
                            "comment_content": "Danaのコメント"
                        },
                        {
                            "id": "3a1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6a3-c090",
                            "data": "2022-03-15",
                            "name": "Zelma",
                            "comment_content": "Zelmaのコメント"
                        },
                        {
                            "id": "4a1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6a4-c102",
                            "data": "2022-03-12",
                            "name": "Pat",
                            "comment_content": "Patのコメント"
                        },
                        {
                            "id": "3a1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6a3-c153",
                            "data": "2022-03-11",
                            "name": "Chris",
                            "comment_content": "Chrisのコメント"
                        },
                        {
                            "id": "1a1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6a1-c171",
                            "data": "2022-03-11",
                            "name": "Alex",
                            "comment_content": "Alexのコメント"
                        }
                    ]
                }
            ]
        },
       //以下省略
    ]
}

初めてなので不安な部分もありましたが、一旦よしとしました。

ただ、作成したJSONデータを有効にコードで使用できているかについてはもう少し見直しが必要かなと思っています。

DOM生成のHTMLについて

yahooのトップページでは、タブや記事のコンテンツ(タブパネル)はWAI-ARIA(ウェイ エリア)で作成されていました。

WAI-ARIA(ウェイ エリア):HTMLに専用のタグを使用することでアクセシビリティ向上が測れる

WAI-ARIA(ウェイ エリア)とは?初めて制作する方向けに解説!
ARIA: tab ロール
WAI-ARIAを学ぶときに整理しておきたいこと

なので、今回はWAI-ARIAを使用してみました。

JSについて

もう一度調べ直したこととして、
指定したクラス名だけ削除するときどうするのかについて調べ直しました。

classListプロパティremoveメソッドを利用すればできました。

JavaScriptで特定のクラス名があるかどうかを判別して条件分岐する方法はclassNameを使用すればできました!!

前回のJSONデータ取得関数についての変更

前回のコード(getData()のところ)では、JSONデータを取得する際には、fetchしたURLがfetchできなかったり、202以外のレスポンスを返した場合(サーバーエラー)には、try/catch節でエラーを外部のcatch節に渡していたのですが、

・throw Errorをここでした場合、サーバーからのエラーとリファレンスエラーのようなエラーとの処理が分けられていない問題もあること

・throw Errorで実行側のcatch節にerror処理を委譲した場合、apiを呼び出す側は毎回statusコードに対して同じ処理を書かなくてはならないといけないので辛くなること
(これはあくまで実際にfetchを呼び出す関数がこのように使いまわせること前提の考えです)

・今後の課題では、別ページへ飛ばすことも考えていること

以上の指摘を先生から受けたので、下記のコードに修正しました。

const request = async () => {
  const response = await fetch(REQUEST_URL, {
    headers: {
      "Content-Type": "application/json"
    }
  });
  if (!response.ok) {
    displayErrorMessage(`${response.status}:${response.statusText}`);
  } else {
    return response.json();
  }
};

・try/catch節を削除

・新たにdisplayErrorMessage関数を作成

知らなかったので調べたのですが…
fetch関数の引数 headers: { }の中身の“Content-Type”は、処理を行う前にコンテンツの中身が正しいか判別しているようでした。
Fetchの使用

また、下記が先生が考えていたコードのようです。

async function fetchData(endpoint) {
  const response = await fetch(endpoint);
  if (response.ok) {
      const res = await response.json();
      return res
  } else {
   window.location.href = `your/path/${statusCode}.html` or run display function here
  }
}

タブやタブパネル作成について

・初期表示(ページを読み込んだときに最初に表示するニュース 今回であれば、”スポーツ”)をする場合カテゴリーは決まっていることから、JSONデータに記述してあるisInitialDisplayがtrueであれば、初期表示をする

・タブを表示する関数としてrenderNewsTab関数を作成しました。
 まとめて、要素を追加するのに、createDocumentFragmentを使用しています。

・コンテンツを表示する関数としてrenderNewsContent関数を作成しました。
 こちらも同様にcreateDocumentFragmentを使用して、メソッドチェーンでコードの記述量を減らして要素を追加しています。

DocumentFragment
DocumentFragment – 親を持たないドキュメント

メソッドチェーンについては、さえさん(@sae_prog)に教えていただきました。

 この関数の中で、
 ・記事を作成する(createArticle関数)
 ・Sectionタグを作成する(createSection関数)→これは、ページを読み込んだ際にのみ動く(これは後述するクリックイベントで再利用できたかも…)
 ・それぞれのトピックのメイン画像を作成する(createTopicImg関数)

記事を作成する(コメントのアイコン)

・createArticle関数では、JSONデータを取得した際にコメントが0以上であれば、fontawasomeで設定したアイコンが表示されるようにcreateCommentIcon関数を作成しました。
 fontawasomeでアニメーションが使用できるとのことだったので勉強として作成してみました。

下記をheadタグに追加しました。

    <link
      rel="stylesheet"
      href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome-animation/0.0.10/font-awesome-animation.css"
      type="text/css"
      media="all"
    /> // Font Awesome Animation の利用

    <script
      defer
      src="https://use.fontawesome.com/releases/v5.1.0/js/all.js"
      integrity="sha384-3LK/3kTpDE/Pkp8gTNp2gR/2gOiwQ6QaO7Td0zV76UFJVhqLl4Vl3KL1We6q6wR9"
      crossorigin="anonymous"
    ></script> //Font Awesome のアイコンフォントを表示する

特定のクラスを付与すれば、アイコンとアニメーションを付与できたのですが、これをDOM操作で出来るのかわからなかったので、今回試してみて動いたので一つ勉強になりました。

fontawasomeの使用の仕方について

Font Awesome 5 の基本的な使い方

Font Awesome の使い方(ver5.9以降)

Font Awesome 5 の使い方(SVG と JavaScript 編)

Layering Text & Counters (official)

アイコンのアニメーション

Layering Text & Counters (official)

2021年最新版、Font Awesome アイコンの使い方と便利な機能のまとめ

【保存版】Font Awesomeの使い方:Webアイコンフォントを使おう

記事を作成する(コメントのアイコン)

isLatestArticles関数で3日前までの記事については、newアイコンをつけるようにしました。

ここの関数を作成する際に、date-fnsというライブラリを使用しました。
date-fns : date-fnsは、ブラウザーNode.jsJavaScriptの日付を操作するための、最も包括的でありながらシンプルで一貫性のあるツールセット

実際の現場では、使用することがあるそうです。

使い方は、
npm install date-fns --save (npm 5.0.0以降からは–save不要)
 npm install時に「–save」オプションはいらない
・node installでdate-fnsを使用する場合は、該当jsファイルに
 import { format, formatDistance, formatRelative, subDays } from 'date-fns'を記入する
 赤色マーカーのところは、使用するdate-fnsのライブラリの関数を記述します。
・今回は指定した日付の差を返却するため(日付の引き算)date-fnsの関数としてdifferenceInCalendarDays を使用しました。

const isLatestArticles = (newsArticleData) => {
  const PERIODOFLATESTARTICLES = 3; //何日までnewをつけるか→今回は、3日前まで
  const newsArticleDate = newsArticleData.date; //記事がいつ投稿されたかをJSONデータから取得

  const nowDate = new Date(); //今日の日付を取得
  const postDate = new Date(newsArticleDate);

  const periodOfDays = differenceInCalendarDays(nowDate, postDate); //今日の日付から記事が投稿された日付の差を取得
  const result = periodOfDays <= PERIODOFLATESTARTICLES;  //取得した日付が3日以内かの記事かBoolean型で返す

  return result;
}

date-fnsについての記事

date-fns (公式)
【超便利】Javascript日付ライブラリ・date-fn
JavaScriptの日付ライブラリdate-fnsでよく使うメソッドのまとめ
Javaエンジニア、React+Firebaseでアプリを作る
date-fns 使い方あれこれ

JSの関数についての記事

Date() コンストラクター

タブをクリックした時の処理

当初、タブをクリックした際に、どのタブをクリックしたのか判別する際にループ処理を行う必要があると考えていました。

・for文で回す

・forEachで回す

forEachで回す場合、querySelectorAllは、NodeListを返すためforEachは使用できませんでした。

なので、一旦配列に直す必要がありました。(NodeList : 配列みたいなオブジェクト。厳密には配列ではない)

その時に使用を考えたのが、Array.from()でした。

Array.from() : 配列のようなオブジェクトや反復可能オブジェクトから、コピーされた新しい Array インスタンスを生成します。

これでは、コードが冗長になると考えたので、for文で回すことを考えました。

 const tabTopics = document.querySelectorAll(".tabTopics");
    for (let i = 0; i < tabTopics.length; i++) {
        tabTopics[i].addEventListener("click", () => {...}

その後、コードレビューの際にさえさん(@sae_prog)からスプレッド構文を教えていただきました。

const clickedTabEvent = (newsDataArray) => {
  const tabTopics = [...document.querySelectorAll(".tabTopics")];
  tabTopics.forEach((tab) => {...}

コードが見やすくなったので教えていただきすごく勉強になりました!!

スプレッド構文
【JavaScript】スプレッド構文の便利な使い方まとめ
どうして!?document.querySelectorAll(selector).addEventListener()が動かないわけ
EventTarget.addEventListener()

後から調べたら、for...of 構文であれば配列状オブジェクトでも使用できたようでした。勉強になった

for…of

for…in 構文もあったなと思い出したのですが、あまり使用しないようでした。(配列状オブジェクトでは使用できない!)

for…in

その後の処理で、クリックされたタブが何番目のタブなのか調べるために、

const clickedTabIndex = tabTopics.indexOf(event.currentTarget);

indexOfを使用しました。

event.currentTargetでクリックした要素を取得して、それがtabTopics内で何番目の要素のなのか取得できるようになっています。

indexOf
JavaScriptで配列を検索する4つのメソッド

また、イベントハンドラのthis,event.target,event.currentTargetについて何が違うのか疑問に思ったので調べてみました。

currentTargetプロパティは、Element.addEventListerner()のElementの要素(イベントハンドラを登録した要素)のみ

targetプロパティは、イベントが発生した要素(親にイベントハンドラを登録していた場合でも、子をクリックした場合は子が取得される)

両者の違いは結構重要ですね。

thisについてはthis はハンドラを設置したオブジェクトになるので、文句なしに正しいでしょう。

イベントハンドラの this

と書かれており、console.logを使用して確認してみましたが、

Event.currentTargetプロパティとEvent.targetプロパティの違い

イベントハンドラの this と event.target, +α

最後に

今回は御二方にレビューいただきました。

まい(@mai2022web )さん
さえ(@sae_prog)さん

ありがとうございます。

少しずつ、前に進んでいるぞー


もりけん塾について

自分がお世話になっているもりけん塾の先生についてです。 基本から応用までのJavaScript、モダンな技術の共有をしてくださいます。 また、色々な相談にも乗っていただけます。

もりけん先生ツイッター(@terrace_tec)

もりけん先生ブログ↓https://kenjimorita.jp/

タイトルとURLをコピーしました