2019.01.28

入門書の次にチャレンジ!JavaScript実践クイズ〜もっと見る編〜


この記事では、入門書などを読んで JavaScript の文法をひととおり知ったあとに、より実践的に JavaScript の活用法を学べるクイズを紹介します。応用が効くように、実際の開発で「ありがち」な実装パターンを扱っていきたいと思います。

問題

もっと見る機能を作ろう

画面が小さい場合は右上の「EDIT ON CODEPEN」をクリックすると CodePen のサイトにジャンプして大きな画面で見られます。

See the Pen JavaScript実践クイズ〜GIFガチャ編〜 by Masahiro Harada (@MasahiroHarada) on CodePen.

後述する仕様の通りに動作するように JavaScript を編集しましょう。
穴埋めになっているので /* Insert code here... */ の箇所にコードを足してください。

HTML と CSS は出来ているので変更しません。

今回使用する API(ruddy-mail.glitch.me)は Glitch で作成しました。コードは以下の通りです。
(前回「GIFガチャ編」で使用した API のコードも含まれています。)

前提条件

  • jQuery などのライブラリは使用せずに実装してください。
  • 対応ブラウザは Google Chrome 最新版とします。
  • 解答例では ES2015 以降の文法も使用します。
  • 非同期通信には Axios というライブラリを用います。
    (CodePen には事前に読み込んであります。)

仕様

  • 最初は「もっと見る」ボタンだけが表示されています。
  • 「もっと見る」ボタンをクリックすると、猫のGIFが一行に三枚表示されます。
  • 表示できる GIF がなくなったら「もっと見る」ボタンを非表示にします。

GIF 画像取得 API(/api/list)は以下の形式の JSON を返却します。
返却できる GIF がなくなると、lasttrue になります。

{
  "items": [
    { "url": string },
    { "url": string },
    { "url": string }
  ],
  "last": boolean
}

「もっと見る」をクリックしたあと、このような HTML を構築してください。

<div id="output" class="gifs">
  <div class="columns">
    <img class="column is-one-third" src="..." alt="" />
    <img class="column is-one-third" src="..." alt="" />
    <img class="column is-one-third" src="..." alt="" />
  </div>
</div>

「もっと見る」がさらにクリックされたときは、このように要素を追加します。

<div id="output" class="gifs">
  <div class="columns">
    <img class="column is-one-third" src="..." alt="" />
    <img class="column is-one-third" src="..." alt="" />
    <img class="column is-one-third" src="..." alt="" />
  </div>
  <div class="columns">
    <img class="column is-one-third" src="..." alt="" />
    <img class="column is-one-third" src="..." alt="" />
    <img class="column is-one-third" src="..." alt="" />
  </div>
</div>

🤔

🤔

🤔

回答例

const moreButton = document.getElementById('more');
const output = document.getElementById('output');

const API_URL = 'https://ruddy-mail.glitch.me/api/list';

let page = 1;

moreButton.addEventListener('click', function() {
  axios.get(API_URL + '?page=' + page)
    .then(response => {
      // HTMLを追加する
      const row = buildHTML(response.data.items);
      output.appendChild(row);
      // 最後のページであれば「もっと見る」ボタンは隠す
      if (response.data.last) {
        moreButton.classList.add('hidden');
      }
    });
  page += 1;
});

/**
 * 一行分の要素を生成する
 *
 * @param {Array} items
 */
function buildHTML(items) {
  const row = document.createElement('div');
  row.className = 'columns';
  let html = '';
  items.forEach(item => {
    html += '<img class="column is-one-third" src="' + item.url + '" alt="" />';
  });
  row.innerHTML = html;
  return row;
}

解説

前回のGIFガチャ編に引き続き Promise を利用した非同期処理がポイントの一つですが、それに加えて今回は「HTML を追加する」パターンが登場しています。

今まではクラスを利用して要素を出したり消したりしていただけでしたが、今回は HTML そのものを JavaScript で作って既存の要素の中に追加する実装を行います。

まず API からはレスポンスとして以下のようなデータを受け取ります。

{
  "items": [
    { "url": "https://media.giphy.com/media/vFKqnCdLPNOKc/giphy.gif" },
    { "url": "https://media.giphy.com/media/33OrjzUFwkwEg/giphy.gif" },
    { "url": "https://media.giphy.com/media/l0MYNB04rBb51QNtC/giphy.gif" }
  ],
  "last": false
}

このうち HTML 作成に使うのは items 配列です。
last は後ほど「もっと見る」ボタンの表示判定に使用します。

そして以下の HTML を作成します。

<div class="columns">
  <img class="column is-one-third" src="..." alt="" />
  <img class="column is-one-third" src="..." alt="" />
  <img class="column is-one-third" src="..." alt="" />
</div>

この処理を受け持つのが buildHTML 関数です。つまり buildHTML 関数は、レスポンスの items 配列を引数にとり、上記の HTML 要素を返却します。

buildHTML 関数ではまず外側の <div> 要素を createElement メソッドで作成しています。

const row = document.createElement('div');

createElement メソッドの返却値は getElementById などと同じ HTML 要素です。
そのため、className プロパティからクラスを指定できます。

row.className = 'columns';

さて createElement は HTML 要素を作成するメソッドでしたが、ある要素の中に別の要素を追加する方法はいくつかあり、今回は以下の二つを使用しています。

elem.innerHTML = "<img src="..." alt="..." />";
elem.appendChild(childElem);

innerHTML プロパティを用いると、文字列で表される HTML 要素が子要素に追加されます。子要素全体が上書きされ置き換わるのがポイントです。

appendChild メソッドを用いると、引数で与えた HTML 要素が子要素の末尾に追加されます。

問題コードに戻りましょう。

buildHTML 関数は、引数にとったレスポンスの items 配列をループさせて配列要素の数だけ <img> 要素を文字列の形で変数 html に書き込みます。そして createElement で作成した <div> 要素(変数 row)の innerHTML に追加します。

let html = '';
items.forEach(item => {
  html += '<img class="column is-one-third" src="' + item.url + '" alt="" />';
});
row.innerHTML = html;

これで buildHTML 関数は完成です。
一行分の GIF 画像の一覧を表す HTML 要素を作成する関数ができたので、then の中で呼び出し、結果を appendChildoutput の子要素の末尾に追加します。

axios.get(API_URL + '?page=' + page)
  .then(response => {
    // HTMLを追加する
    const row = buildHTML(response.data.items);
    output.appendChild(row);
  });

「もっと見る」ボタンをクリックするたびに GIF 画像が増えていく必要があるので innerHTML ではなく appendChild を使用します。

最後にレスポンスデータの lasttrue であれば、つまり最後のページであれば「もっと見る」ボタンは隠します。

// 最後のページであれば「もっと見る」ボタンは隠す
if (response.data.last) {
  moreButton.classList.add('hidden');
}

今回のポイントは JavaScript による HTML の作成でした。実は React や Vue などのフレームワークを使用したモダンな開発手法ではあまり今回のように直接 HTML 要素を作成するコードは書かなくなるのですが、それでもフレームワークの裏側では間違いなく HTML 要素の作成や追加は行われているわけで、HTML 要素の操作は JavaScript の存在意義そのものと言えるでしょう。


以上、今回はもっと見る機能を実装するクイズを紹介しました。