BootstrapとJavascriptでQiita検索アプリを作る

FIRST PLAN株式会社のフロントエンドエンジニアtakeです。

**ダイエット頑張ります！！**

さて、入社後の課題をやったので発表したいと思います。
お時間があったら見ていってください。

# こんなやつ
[![Image from Gyazo](https://i.gyazo.com/6c8935828b1e34435dfdd8883a2de243.gif)](https://gyazo.com/6c8935828b1e34435dfdd8883a2de243)

# 簡単な設計(JavaScript)
##api部分
①フォームに入力した単語を取得。
`document.querySelector(element).value`
↓
②それを`get`のパラメーターの`query`にセット、そしてget。
`axios.get('https://qiita.com/api/v2/items', params: {query: ①})`
↓
③帰ってきたJSONの`data`プロパティを変数に代入。
`const response = axios.get('https://qiita.com/api/v2/items', params: {query: ①})`
`const fugafuga = response.data`
↓
③その配列に対して、検索結果をリストレンダリング。
`fugafuga.foreach(fuga => document.createElement(foo)～`

##ページネーション部分
①ページ番号部分をクラス内のプロパティで管理、初期値は1。
`this.currentPage = 1`;
②「進む」ボタンを押すと`currentPage`を+1、「戻る」場合は-1する。
③そしてその`currentPage`を`get`の`page`部分にセット、そして`get`。
`axios.get('https://qiita.com/api/v2/items', params: {page: this.currentPage})`
④最後にapi部分で行っている③、④をこちらに行う。

# コード
##HTML
マークアップはbootstrapを使いcssでは書かずに実装しました。

動きとしては`<tbody>`にJavaScript側で`createElement`された

```html:
<tr>
  <td>記事のタイトル</td>
  <td>ライク数</td>
  <td>コメント数</td>
</tr>
```

が挿入されていくイメージです。


```html:HTML
<div class="container p-5">
  <div class="row flex-column">
    <h1>
      <span class="text-success">Qiita</span>記事検索
    </h1>

    <form class="mb-3">
      <div class="form-group">
        <label for="search-box">キーワードで検索</label>
        <input
          type="text"
          class="form-control"
          id="search-box"
          placeholder="キーワードを入力"
        />
      </div>
      <button type="button" class="btn btn-success search-button">検索</button>
    </form>
  </div>

  <div class="search-result row d-none">
    <div class="table-responsive">
      <table class="table table-striped">
        <thead>
          <tr>
            <th scope="col">タイトル</th>
            <th scope="col">LGTM</th>
            <th scope="col">コメント数</th>
          </tr>
        </thead>
        <tbody>
          <!-- ここに挿入される-->
        </tbody>
      </table>
    </div>
  </div>

  <div class="search-result row d-none">
    <nav class="my-0 mx-auto" aria-label="page-list">
      <ul class="pagination">
        <li class="page-item return-button">
          <a
            class="page-link"
            aria-label="Previous"
          >
            <span aria-hidden="true">&laquo;</span>
            <span class="sr-only">Previous</span>
          </a>
        </li>

        <li class="page-item">
          <a class="page-link page-number" href="#!">1</a>
        </li>

        <li class="page-item next-button">
          <a class="page-link" href="#!" aria-label="Next">
            <span aria-hidden="true">&raquo;</span>
            <span class="sr-only">Next</span>
          </a>
        </li>
      </ul>
    </nav>
  </div>
</div>
```

##JavaScript
http通信ではaxiosを使いました。
「簡単な設計」で書いた事を今回はあえてフルスクラッチで実装しています。

`renderingResults`関数の部分ではReactの`map`、Vueの`v-for`のような事を力技で実装しています。
それと少し表示が寂しいので、アラート部分ではsweetalert2を導入しました。

※まだまだコードを綺麗にできると思うので、リファクタリングして更新します。

```javascript:JavaScript
const $ = element => document.querySelector(element);

class ApiSearch {
  constructor() {
    //検索結果のJSONを格納
    this.results = '';
    //現在のページ数
    this.current_page = 1;
  }

  renderingResults() {
    //表示前に以前のDOMを削除
    while ($('tbody').firstChild) $('tbody').removeChild($('tbody').firstChild);

    this.results.forEach(result => {
      //追加するテーブルのエレメントを作成して中身を追加
      const TR = document.createElement('tr');
      TR.style.cursor = 'pointer';
      TR.addEventListener('click', () => {
        window.open(result.url, '_blank');
      });

      const TITLE = document.createElement('td');
      TITLE.textContent = result.title;

      const LGTM = document.createElement('td');
      LGTM.textContent = result.likes_count;

      const COMMENT = document.createElement('td');
      COMMENT.textContent = result.comments_count;

      //tbodyに順に子要素を追加
      $('tbody').appendChild(TR);
      TR.appendChild(TITLE);
      TR.appendChild(LGTM);
      TR.appendChild(COMMENT);
    });
  }
  pageTransition(identifier) {
    //検索前は実行しない
    if (!this.results) return;

    //戻るか進むかの処理分岐
    if (identifier === 'next') {
      this.current_page++;
    } else {
      if (this.current_page <= 1) return;
      this.current_page--;
    }

    $('.page-number').textContent = this.current_page;
    this.searchResults();
  }
  async searchResults() {
    //キーワードが空の場合はアラート
    if (!$('#search-box').value) {
      Swal.fire({
        title: 'Caution!',
        text: 'キーワードを入力してください',
        icon: 'warning'
      });
      return;
    }

    //apiを叩く
    try {
      const PARAMS = {
        page: this.current_page,
        per_page: 20,
        query: $('#search-box').value
      };
      const RESPONSE = await axios.get('https://qiita.com/api/v2/items', {
        headers: {
          Authorization:
            'Bearer アクセストークン'
        },
        params: PARAMS
      });
      //JSONを配列に格納
      this.results = RESPONSE.data;
    } catch {
      //エラーの場合はalert
      Swal.fire({
        title: 'Error!',
        text: '通信エラーです',
        icon: 'error'
      });
    }

    //テーブルとページネーションを表示。
    document.querySelectorAll('.search-result').forEach(result => {
      if (result.classList.contains('d-none')) result.classList.remove('d-none');
    })

    this.renderingResults();
  }
}

//クラスをインスタンス化
const APISEARCH = new ApiSearch();

//イベントを紐づけ
$('#search-box').addEventListener('keydown', e => {
  //デフォルトの動作をキャンセルしてsearchResultsを発火する。
  if (e.key === 'Enter') {
    e.preventDefault();
    APISEARCH.searchResults();
  }
});
$('.search-button').addEventListener('click', () => {
  APISEARCH.searchResults();
});
$('.next-button').addEventListener('click', e => {
  e.preventDefault();
  APISEARCH.pageTransition('next');
});
$('.return-button').addEventListener('click', e => {
  e.preventDefault();
  APISEARCH.pageTransition('return');
});
```