Help us understand the problem. What is going on with this article?

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

More than 1 year has passed since last update.

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

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

完成イメージ

それぞれのタブを押すたびにAjaxが発火して、タブが切り替わるイメージです。
Image from Gyazo

ポイント整理

自分なりに挑戦しようとしたところは以下。

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のセレクタに正規表現・属性フィルタを使う方法

カスタムデータ属性を使って評価種別を判別する

カスタムデータ属性に評価判別のための値を仕込みました。
ただ、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-*) を定義する方法

コーディング例

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-*) を定義する方法
JQueryのセレクタに正規表現・属性フィルタを使う方法
Railsガイド:アセットパイプライン

ozackiee
「モノが作れる」エンジニアを目指して学習中です。 学習の軌跡として、まとめやバグなどでハマったことなどを書いて行こうと思います。 <勉強中の言語・フレームワーク・ツール>  ・Ruby,Ruby on Rails  ・HTML/CSS
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away