【もりけん塾】JS課題34 – Vanilla JSでお気に入り追加・削除機能の作成 –

JavaScript

練習のためローカルストレージを使用し、お気に入り追加ボタンを押した記事をマイページに表示させたり、削除する機能の実装に挑戦しました。

こんにちは。はるです。

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

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

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

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

JavaScript課題34の仕様

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

    JavaScript課題34の学習記録

    制作物

    ニュース個別ページを動的に表示する

    個別ページ表示用に article.html を作成。

    個別ページにリンクを貼る際に、/article.html?id=** のようにURLパラメータで記事固有のidを付与して、動的に記事が表示されるように実装しました。

    URLパラメータのidから記事の情報を特定する

    URLパラメータのidと、jsonデータのidが一致しているデータを返す関数を作成。

    引数のdataにはfetchしたjsonデータが渡ってきます。
    ( fetchの一連のコードは前回の解説にあるので割愛 )

    const urlParameter = Object.fromEntries(new URLSearchParams(window.location.search));
    
    const getArticleData = data => {
     const articles = data.map(item => item.articles);
     let targetData;
     articles.forEach(article => {
      article.forEach(item => {
       if(item.id === urlParameter.id) targetData = item;
      });
     });
    return targetData;
    }

    特定したデータから記事内容を作成

    タイトル、サムネイル、お気に入り追加ボタン、本文をjsonデータから取得して記事を作成します。

    特に特記することがないのでそのまま掲載します。

    const setTitle = data => document.querySelector('title').textContent = `${data.title}`;
    
    const createCategoryLabel = data => {
     const categoryLabel = createElementWithClassName('p', 'category');
     const articleId = urlParameter.id;
     categoryLabel.textContent = findCategoryById(data, articleId);
     return categoryLabel;
    }
    
    const createArticleHead = data => {
     const articleHead = createElementWithClassName('div', 'article__head');
     const title = createElementWithClassName('h1', 'article__title');
     const favoriteButton = createElementWithClassName('button', 'article__btn');
     const favoriteImg = document.createElement('img');
    
     title.textContent = data.title;
     favoriteButton.type = "button";
     favoriteButton.id = "js-favorite-button";
     favoriteImg.src = '/assets/img/icon-star.png';
     favoriteImg.alt = 'お気に入りに追加';
     favoriteButton.appendChild(favoriteImg);
    
     articleHead.appendChild(title).after(favoriteButton);
     return articleHead;
    };
    
    
    const createArticleInfo = data => {
     const articleInfo = createElementWithClassName('div', 'article__info js-article-info');
     const date = createElementWithClassName('time', 'article__date');
    
     date.textContent = `${data.date}`;
     articleInfo.appendChild(date);
     return articleInfo;
    }
    
    const createArticleContents = data => {
     const articleContents = createElementWithClassName('p', 'article__text');
     articleContents.textContent = `${data.content}`;
     return articleContents;
    }
    
    const createThumbnail = data => {
     const thumbnailWrapper = createElementWithClassName("picture", "article__thumbnail");
     const thumbnailWebp = document.createElement("source");
     const thumbnailJpg = document.createElement("img");
     const noImgSrc = "/assets/img/no-img.jpg";
    
     thumbnailJpg.alt = "";
     thumbnailJpg.src = data.img || noImgSrc;
     thumbnailWrapper.appendChild(thumbnailJpg);
    
     if(data.webp){
      thumbnailWebp.srcset = data.webp;
      thumbnailWrapper.insertAdjacentElement("afterbegin", thumbnailWebp);
     }
     return thumbnailWrapper;
    };

    記事をrenderする

    const renderArticle = data => {
     const targetData = getArticleData(data);
     const articleElement = document.getElementById('js-article');
    
     setTitle(targetData);
     articleElement.appendChild(createArticleHead(targetData)).after(createArticleInfo(targetData));
     articleElement.appendChild(createArticleContents(targetData)).after(createThumbnail(targetData));
     renderCategory(data);
    }

    ここまででURLパラメータのidから判別した個別記事が作成→renderされます。

    htmlは1つですが、jsonデータにある16記事分のデータを動的に表示することができます。

    お気に入りに追加する

    お気に入りデータはローカルストレージに保存されるようにします。
    ※ローカルストレージは課題上、危険性を理解した上で練習のために使用しています。

    登録済みのお気に入りデータを取得する

    JSON.parse(json)を使用することでJSON文字列をオブジェクトに変換することができます。

    引数に入った値がjson形式ではなかった場合は、例外が投げられます。そのため、try…catchで例外処理をするのが安全ということを知りました。

     また、実際のアプリケーションでJSONを扱うのは、外部のプログラムとデータを交換する用途がほとんどです。 外部のプログラムが送ってくるデータが常にJSONとして正しい保証はありません。 そのため、JSON.parseメソッドは基本的にtry...catch構文で例外処理をするべきです。
    js-primer

    該当コードです。jsonパースした結果をリターンしています。

    const getRegisteredFavoriteData = () => {
     let registeredFavoriteData = null;
     try {
      registeredFavoriteData = JSON.parse(localStorage.getItem('registeredFavoriteData'));
     } catch (error) {
      console.log(`jsonパースでエラーが発生しました: ${error}`);
      displayInfo(articleWrapper,'エラーが発生しました');
     }
     return registeredFavoriteData;
    }

    新しいお気に入りデータの作成

    引数には、お気に入りボタンを押した個別記事のデータが渡ってきます。

    記事のidやタイトルなど必要な情報をオブジェクトに格納します。

    const createFavoriteData = data => {
     const favoriteData = {
      'id': data.id,
      'date': data.date,
      'title': data.title,
      'img': data.img,
      'webp': data.webp
     }
    

    先ほどのjsonパースした登録ずみお気に入りデータがあれば、それに新しいデータを追加。
    登録されたデータがなければ、favoriteDataをそのまま返すようにします。

     const registeredFavoriteData = getRegisteredFavoriteData();
     const newFavoriteData = registeredFavoriteData !== null ? [...registeredFavoriteData, favoriteData] : [favoriteData];
     return newFavoriteData;
    }

    ローカルストレージに新しいデータを保存

    最後に、localStrage.setItemを使って、createFavoriteData()で作成した新しいお気に入りデータを保存します。

    const saveArticleData = data => {
     const targetData = getArticleData(data);
     localStorage.setItem("registeredFavoriteData", JSON.stringify(createFavoriteData(targetData)));
    }

    お気に入り追加ボタンを押すと実行

    スターの色が変わり、記事のデータがローカルストレージに保存されます。

    const addEventListenerForFavoriteButton = data => {
     const favoriteButton = document.getElementById('js-favorite-button');
    
     favoriteButton.addEventListener('click', (e) => {
      if(e.target.disabled) return;
     
      changeButtonDisabled(e.target);
      saveArticleData(data);
     })
    }

     

    お気に入り一覧ページ

    マイページに移動すると、ローカルストレージに保存データがあるか確認して、あれば一覧として表示します。

    const getFavoriteData = () => {
     let favoriteData = null;
     try {
      favoriteData = JSON.parse(localStorage.getItem('registeredFavoriteData'));
     } catch (error) {
      console.log(`jsonパースでエラーが発生しました: ${error}`);
      displayInfo(articleWrapper,'エラーが発生しました');
     }
     return favoriteData;
    };
    
    const init = () => {
     const favoriteData = getFavoriteData();
     if (favoriteData.length === 0) {
      displayInfo(articleWrapper,'お気に入り記事がありません。');
      return;
     }
     renderArticleList(favoriteData);
     addEventListenerForRemoveFavoriteButton();
    };

    お気に入りデータがない場合には、「お気に入り記事がありません」というテキストを出力します。

    お気に入り削除

    お気に入り削除ボタンを押すと、ターゲットの記事カード(ローカルストレージのデータも一緒に)を削除します。

    const addEventListenerForRemoveFavoriteButton = () => {
     const articleList = document.querySelector(".js-article-list");
    
     articleList.addEventListener("click", (e) => {
      deleteArticleData(e.target);
    
      if (getFavoriteData().length === 0) {
       displayInfo(articleWrapper,'お気に入り記事がありません。');
      }
     });
    };

    お気に入り削除ボタンを押すと、e.targetの情報がdeleteArticleData関数に渡ります。

    ローカルストレージの配列から該当データを検索するために、お気に入りカードのURLにあるURLパラメータからidを取得します。
    const getArticleId = target => {
     const targetArticle = target.closest(".js-article");
     const targetArticleLink = targetArticle.querySelector(".js-article-link").href;
     const regex = /id=([^&]+)/;
     const articleID = targetArticleLink.match(regex)[1];
     return articleID;
    }
    
    match(regex)で指定した正規表現に対する照合の結果が返されます。
    ここから配列 index1のidの中身を使用しました。targetArticleLink.match(regex)[1];
    このidをもとに、deleteAtricleData関数で一覧カードとローカルストレージのデータを削除します。
    
    const deleteArticleData = target => {
     let registeredFavoriteData = getFavoriteData();
     const targetIndex = registeredFavoriteData.findIndex(item => item.id === getArticleId(target));
    
     registeredFavoriteData.splice(targetIndex, 1);
     localStorage.setItem("registeredFavoriteData", JSON.stringify(registeredFavoriteData));
     target.closest(".js-article").remove();
    }

    このidと、ローカルストレージに保存されているデータのidが一致するものを検索します。

    今回は、一致するローカルストレージのデータのIndexを調べました。

    const targetIndex = registeredFavoriteData.findIndex(item => item.id === getArticleId(target));

    ローカルストレージのデータ配列を新しいものにするため、splice(start, deleteCount);を使用しました。

    spliceは、配列の要素を取り除く・追加する・変更するなどの配列操作を行えるメソッドです。

    splice(start, deleteCount)でstart位置からdeleteCountの数だけ削除します。

    registeredFavoriteData.splice(targetIndex, 1);

    お気に入り削除機能の挙動

     

    コード

    lesson34のリンクから実際の挙動を確認できます。

    Sign up → Login → ニュース記事一覧へリンク

    article.jsとmypage.jsが主なコードです。

     

    //

    学習に使用している本は、JavaScript本格入門・独習JavaScriptです。

      

    あとがき

    もりけんさん(@terrace_tec)、さえさん(@sae_prog)、takakoさん(@wattwattwatt) 、hasegawaさん(@starsurferz)お忙しい中レビューや動作確認をいただきありがとうございました!

    今回も、全8回の細かい粒度に分けてのPRに挑戦しました。

    最初にしっかり実装の計画を立てる必要があるので、その練習になったと思います。

    教えていただいたことを生かして、次の課題に進みたいと思います!

    今日は以上です。

    //

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

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