【Rails】Ajaxを使ってタグ切り替え（めも）

某フリマサイトのコピー制作でAjaxを勉強したくて以下のような機能を実装しました。
振り返りをかねて、メモしておきます。

書き方が間違っている、コードが汚いなど、色々とご指摘をいただけると嬉しいです。
(javascriptの変数はvarにしてます。。）

# 完成イメージ
それぞれのタブを押すたびにAjaxが発火して、タブが切り替わるイメージです。
[![Image from Gyazo](https://i.gyazo.com/963e52b452258d0f6fda7a77ba66514d.png)](https://gyazo.com/963e52b452258d0f6fda7a77ba66514d)

# ポイント整理
自分なりに挑戦しようとしたところは以下。
## JQueryのオブジェクト指定を正規表現で行う
 今回は、指定する条件(評価種別)が異なるだけで、全く同じコンテンツを表示させるものでした。
 そのため、オブジェクト指定に正規表現をかけ、出来るだけ省力化をはかったつもり。。。
 前方、後方、部分一致などが使えるようなので、書き方例を以下に例示します。

```sample.js

// ①前方一致　※今回私が使った方法
 $("[id^='rating-tab-']").on('click',function(e){});

// ②後方一致
 $("[id$='-rating-tab']").on('click',function(e){});

// ③部分一致
 $("[id*='rating-tab']").on('click',function(e){});
```

その他にも沢山指定方法があるみたいです。
参考：[JQueryのセレクタに正規表現・属性フィルタを使う方法](https://www.sejuku.net/blog/34426#id)

## カスタムデータ属性を使って評価種別を判別する
 カスタムデータ属性に評価判別のための値を仕込みました。
 ただ、hamlで書くの初めてで少し戸惑いました。下記のように書くとうまく行くみたいです。'-'で表現していたところを：(コロン)と{}(中括弧)で表現する模様。

```sample.html.haml
  %li.rating__nav--all.selected-tab#rating-tab-all{ data: {rating: '0'}}
```
上述のカスタムデータ属性は、js内で以下のようにすれば取得できます。

```sample.js
  var target = $(this).data('rating');
```
※thisはイベントの発火対象を指します。
　それ以外なら、id又はクラスでオブジェクト指定してあげてください。

参考：[haml に HTML5 のカスタムデータ属性(data-*) を定義する方法](https://qiita.com/k6i/items/e2f2675dd6e3c2212c1b)

# コーディング例
railsのcontroller部分は割愛して、載せています。
JavaScriptメインで参考にしていただければと思います。。

##JavaScriptファイル
正規表現などを利用して、出来るだけコンパクトにまとめられるように努めました。
html部分は、Ajaxじゃないページを作って、ブラウザ検証ツールで引っこ抜いてくる感じです。
一番楽チンかなと。

```sample.js
$(function(){
  // ul作成用のHTMLBuildメソッド
  function UlBuildHTML(){
    var html = `<ul id="rating-result-list">`
    return html;
  }

  //li作成用のHTMLBuildメソッド
  function ListBuildHTML(data){
    var html = `<li class="rating__contents__content">
                  <a href="/users/${data.rater_id}/show_profile">
                    <img class="rating__contents__content__image" height="48px" width="48px" src="${data.rater_avatar}">
                    <div class="rating__contents__content__text">
                      <i class="fas ${data.rate}"></i>
                        ${data.stance}
                      <p>${data.rater_nickname}</p>
                      <p>${data.comment}</p>
                      <div class="rating__contents__content__text__time">
                        <i class="far fa-lg fa-clock"></i>
                        <p>${data.created_at}</p>
                      </div>
                    </div>
                    <div class="rating__contents__content__link-icon">
                      <i class="fas fa-lg fa-angle-right"></i>
                    </div>
                  </a>
                </li>`
    return html;
  }

  //データが何も無い時用のHTMLBuildメソッド
  function NoneListBuildHTML(message){
    var html = `<div class="rating__contents__none" id="rating-result-none">
                　<h3>
                  　${message}
                　</h3>
              　</div>`
    return html;
  }
  
  //Ajaxの本体
  //idに特定のキーワードが含まれた要素をクリックした時に発火する
  $("[id^='rating-tab-']").on('click',function(e){
    //"rating-tab-"に前方一致するidをもつ全ての要素から"selected-tab"クラス削除
    $("[id^='rating-tab-']").removeClass('selected-tab');
    //clickされたタブに"selected-tab"クラスを追加
    $(this).addClass('selected-tab');

    //カスタムdata属性に仕込んだ検索対象の評価種別(all/normal/good/bad)を取得する
    var target = $(this).data('rating');

    //ajaxを発火させる
    //検体条件となる評価種別は明示的にdataとして埋め込む
    //ユーザ情報も必要だけど、私はdeviseを使っているのでサーバー側でcurrent_user.idを使うから不要
    $.ajax({
      url:     '/ratings/index_api',
      type:    "GET",
      data:     {rating: target},
      dataType:'json',
    })
    .done(function(list){
      // Ajaxが正常に戻ってきたので一旦リストを空にする
      var result_all  = $('#rating-result-all');
      result_all.empty();
      // 取得データの状況によって表示内容を切り替える
      if ( list.length > 0 ){
        //データがあった場合はリスト表示開始
        //まずulタグをビルド
        var ul_html = UlBuildHTML();
        result_all.append(ul_html);
        var result_list = $('#rating-result-list');
        //生成したulタグにデータ数分のliを追加
        list.forEach(function(data){
          var html = ListBuildHTML(data);
          result_list.append(html);
        });
      }else{
        //データがない場合は無いよとメッセージを出してあげる
        var message ='評価はまだありません'
        var html = NoneListBuildHTML(message);
        result_all.empty();
        result_all.append(html);
      }
    })
    .fail(function(){
      alert('データ取得に失敗しました');
    });
  });
});
```

##Jbuilderファイル
view上でrubyを使って整形している箇所は、全てJbuilderで吸収してあげる必要あり。
フォーマットヘルパーを使うなら、ここで。

```sample.json.jbuilder
#複数結果が取得できる可能性があるため、繰り返し処理を行う
json.array! @ratings do | rate |
  json.rater_id            rate.rater_user_id
  json.rater_nickname      rate.rater_user.nickname

  # イメージ有無で仕込むデータを切り替え。
  # 本番環境はフィンガープリントがくっつくので、それも付加してファイル名記載・・・（多分もっとスマートなやり方あります）
  if rate.rater_user.avatar.attached?
    json.rater_avatar        url_for(rate.rater_user.avatar)
  else
    json.rater_avatar        "/assets/common/member_photo_noimage_thumb-224a733c50d48aba6d9fdaded809788bbeb5ea5f6d6b8368adaebb95e58bcf53.png"
  end

  json.stance              stance_check(rate) #独自ヘルパーメソッド 
  json.comment             rate.comment
  #javascript内でrubyのフォーマットヘルパーが使えないので、jbuilder内で事前に整形。
  json.created_at          rate.created_at.strftime("%m月%d日 %H:%M")

  # font-awesomeの指定をここで割り当てる。
  # 普通に文字列として指定したらいけました。ヘルパーメソッド にしてあげたほうが親切かも。。
  case rate.rate
  when 1
    json.rate 'fa-smile good'
  when 2
    json.rate 'fa-meh normal'
  when 3
    json.rate 'fa-frown bad'
  end

end

```

##html.hamlファイル
部分テンプレート部分だけを抜粋しました
どうしてもBEM表記がスマートに書けない病です・・・

```sample.html.haml
.rating
  .rating__title
    %h2
      評価一覧
  %ul.rating__nav
    //hamlの場合、属性名に'-'を使うと文法エラーになるため下記の表記。
    %li.rating__nav--all.selected-tab#rating-tab-all{ data: {rating: '0'}}
      %h3
        全て
    %li.rating__nav--good#rating-tab-good{ data: {rating: '1'}}
      = icon 'fas fa-lg', 'smile', class: "good"
      %h3
        良い
    %li.rating__nav--normal#rating-tab-normal{ data: {rating: '2'}}
      = icon 'fas fa-lg', 'meh', class: "normal"
      %h3
        普通
    %li.rating__nav--bad#rating-tab-bad{ data: {rating: '3'}}
      = icon 'fas fa-lg', 'frown', class: "bad"
      %h3
        悪い
  .rating__contents#rating-result-all
    - if @ratings&.present? && @ratings.size > 0
      %ul#rating-result-list
        - @ratings.each do | rate |
          %li.rating__contents__content
            = link_to show_profile_user_path(rate.rater_user_id) do
              - if rate.rater_user.avatar.attached?
                = image_tag rate.rater_user.avatar, class:'rating__contents__content__image', height: '48px', width: '48px'
              - else
                = image_tag 'common/member_photo_noimage_thumb.png', class:'rating__contents__content__image', height: '48px', width: '48px'
              .rating__contents__content__text
                = rating_check(rate) #独自ヘルパーメソッド
                = stance_check(rate) #独自ヘルパーメソッド
                %p
                  = rate.rater_user.nickname
                %p
                  = rate.comment
                .rating__contents__content__text__time
                  = icon 'far fa-lg','clock'
                  %p
                    = rate.created_at.strftime("%m月%d日 %H:%M")
              .rating__contents__content__link-icon
                = icon 'fas fa-lg','angle-right'
    -else
      .rating__contents__none#rating-result-none
        %h3
          評価はまだありません
```
##CSSファイル
全くレスポンシブじゃあ、ありません。。
mixinを利用することで短くなりそうですが、非常にプレーンに記述しています。

```sample.scss
.wrapper{
  .rating{
    //タイトル（評価一覧）
    &__title{
      height:72;
      padding-left:16px;
      background-color:#fafafa;
      h2{
        line-height:72px;
        font-size:16px;
        font-weight:bold;  
      }
    }
    //タブ
    &__nav{
      display:flex;
      width:700px;
      height:74px;
      background-color:#eee;
      cursor: pointer;
      h3{
        font-weight:bold;
      }
      &--all{
        display:flex;
        justify-content: center;
        text-align: center;
        align-items: center;
        width:175px;
      }
      &--good{
        display:flex;
        justify-content: center;
        text-align: center;
        align-items: center;
        width:175px;
        i{
          margin-right:10px;
          color:#ef5185;
        }
      }
      &--normal{
        display:flex;
        justify-content: center;
        text-align: center;
        align-items: center;
        width:175px;
        i{
          margin-right:10px;
          color:#fba933;
        }
      }
      &--bad{
        display:flex;
        justify-content: center;
        text-align: center;
        align-items: center;
        width:175px;
        i{
          margin-right:10px;
          color:#6ab5d8;
        }
      }
      //選択されたタブのみデザインを変えられる用に別だし
      .selected-tab {
        border-top: 2px solid #dd0e0e;
        background-color: #fff;
      }
    }
    //評価リスト
    &__contents{
      &__content{
        box-sizing: border-box;
        padding:16px;
        background-color: #ffffff;
        border-bottom:solid 1px #ccc;
        a{
          display:flex;
          align-items:center;
          text-decoration: none;
          width:668px;
          color:#333;
          &__image {
            width:48px;
            height:48px;
            display:inline-block;
          }
          &__text {
            display:inline-block;
          }
          &__link-icon{
            display:inline-block;
          }
        }
        &__text {
          margin: 0 40px 0 20px;
          width:85%;
          font-size:13px;
          color:#333;
          .good{
            color:#ef5185;
          }
          .normal{
            color:#fba933;
          }
          .bad{
            color:#6ab5d8;
          }
          &__time{
            display:flex;
            align-items:center;
            font-size:12px;
            i{
              display:inline-block;
              margin:0 5px;
            }
          }
        }
      }
      &__content:hover{
        background-color:#f5f5f5;
      }
      &__content:last-child{
        border:none;
      }
      //表示するリストが存在しない場合のメッセージ表示用
      &__none{
        box-sizing: border-box;
        height:236px;
        padding: 160px 0 60px;
        background-color: #ffffff;
        background:#fff image-url('logo-gray-icon.svg') no-repeat;
        background-position:center 50px;
        background-size:78px 85px;
        h3{
          font-size:16px;
          font-weight:600;
          color:#ccc;
          text-align:center;
        }
      }
    }
  }
}

```

# まとめ
今回は特にないのですが、もう少し正規表現やフィルタなどを駆使すれば、スマートに分かりやすいJavaScriptやJQueryが書けそうだなぁ。というのが正直な感想です。（要するに、まだまだ勉強不足）

初心者から抜け出すためにも、引き続き頑張ります。

# 参考
[haml に HTML5 のカスタムデータ属性(data-*) を定義する方法](https://qiita.com/k6i/items/e2f2675dd6e3c2212c1b)
[JQueryのセレクタに正規表現・属性フィルタを使う方法](https://www.sejuku.net/blog/34426#id)
[Railsガイド：アセットパイプライン](https://railsguides.jp/asset_pipeline.html#%E4%B8%BB%E8%A6%81%E3%81%AA%E6%A9%9F%E8%83%BD)