社会人1年目エンジニアのブログ

if you can't explain it simply, you don't understand it well enough.

RailsとAjaxでいいね機能実装してみた

Ajaxでいいね機能を実装したので復習かねてメモ
挙動:
f:id:chonesu:20160311014834g:plain
画面遷移しないでいいね付ける。
もうちょっと押した感ほしいけど。
js.erbやらjs.hamlやらrender jやらよくわかんなかったからJSテンプレートは使わなかった

参考にしたのはこの記事

Ruby on RailsのAjax処理のおさらい - Qiita

流れとしては
1. いいねボタンクリック
2. createまたはdestroyアクションにajaxでリクエスト
3. createだったらDBに保存、destroyだったらDBから削除
4. jsonレスポンスをフロントに返す
5. 成功(done)したらすでにいいねされてるかされてないかで条件分岐
6. icon-fav-off,icon-fav-offというクラスをremoveしたりaddする
7. レスポンスで帰ってきたlikes_countでいいね総数を表示

モデル

※いいねするユーザー(ここではdevise使って生成されたuser)いいねする対象(ここではarticle)、いいね自体(ここではarticlelike) 3つのモデルがあること 前提。

user.rb

  has_many :articlelikes # 1対多
  has_many :articles # 1対多

article.rb

  belongs_to :user #1対1
  has_many :articlelikes, dependent: :destroy #1対多

dependent: :destroy を指定してあげるとuserがいなくなったとき自動的にいいねも削除してくれる

articlelike.rb

  belongs_to :article, counter_cache: :likes_count #1対1
  belongs_to :user #1対1

counter_cacheに自分で追加したarticleテーブルのlikes_countカラムを指定してあげると articlelikeが生成されたり削除されたりするたびにその数を自動的に保持してくれる。
articlelike.findarticleid.countするより高速

2.ビュー(Haml)

.article-likes.js-article-likes
  -if user_signed_in?
    -if current_user.articlelikes.find_by(article_id: article.id)
      %i.fa.fa-heart.icon-fav-on.js-likes-button{data: {id: article.id}}
    -else
      %i.fa.fa-heart.icon-fav-off.js-likes-button{data: {id: article.id}}
  -else
    = link_to new_user_session_path do
      %i.fa.fa-heart.icon-fav-off
  %span.js-likes-count
    =article.likes_count

まずユーザーがログインしてるかしてないかで分岐して さらにいいね済みかどうかで分岐 ハートはFont Awesome(

Font Awesome, the iconic font and CSS toolkit


iタグにマウスオーバーしたときポインタがリンクになるようにするにはcsscursor: pointerを指定

3.コントローラー

  def create
    @article = Article.where(id: params[:article_id])
    articlelike = current_user.articlelikes.build(article_id: params[:article_id])
    if articlelike.save
      render json: @article, only:[:likes_count]
    end
  end

  def destroy
    @article = Article.where(id: params[:article_id])
    articlelike = current_user.articlelikes.find_by(article_id: params[:article_id])
    if articlelike.destroy
      render json: @article, only:[:likes_count]
    end
  end

4.JS(Coffee)

$->
  class FavArticle
    constructor: ($el) ->
      # インスタンス作成したときの引数をコンストラクタで受け取ってjqueryobjにして$el変数に格納
      @$el = $($el)
      # いいねボタン探して$likesButton変数に格納
      @$likesButton = @$el.find('.js-likes-button')
      # いいね総数表示要素を取得して$likesCount変数に格納
      @$likesCount = @$el.find('.js-likes-count')
      # イベントリスナー呼び出し
      @setEventListener()

    setEventListener: ->
      # ボタンをクリックした際のイベントリスナー
      @$likesButton.on 'click', (e) =>
        # Ajax呼び出し
        @_setLikesAjax(e)

    _setLikesAjax: (e)->
      $this = $(e.currentTarget)
      # 記事idを取得
      articleId = $this.data('id')
      # destroyアクションへのリクエストURL
      unLikeURL = '/articleunlike/' + articleId
      # createアクションへのリクエストURL
      likeURL = '/articlelike/' + articleId
      # もしクリックした要素がいいねされてなかったら
      if $this.hasClass('icon-fav-off')
        $.ajax({
          # createアクションにリクエスト飛ばす
          url: likeURL
          # POSTメソッドで
          type: 'POST'
          # キャッシュは保持しない
          cache: false
          # 記事idを送る
          data: {
            'article_id': articleId
          }
          # 帰ってくるデータはjson形式で
          datatype: 'json'
        })
        # Ajax通信が成功した場合
        .done (data) =>
          # 灰色ハートのクラスを削除し赤いハートのクラスを付与
          $this.removeClass('icon-fav-off').addClass('icon-fav-on')
          # いいね総数を表示
          @$likesCount.text(data[0].likes_count)
      else
        $.ajax({
          url: unLikeURL
          type: 'DELETE'
          cache: false
          data: {
            'article_id': articleId
          }
          datatype: 'json'
        })
        .done (data) =>
          $this.removeClass('icon-fav-on').addClass('icon-fav-off')
          @$likesCount.text(data[0].likes_count)

    # いいねボタンといいね総数をwrapしている.js-article-likesをすべて取得
    favButtons = document.querySelectorAll('.js-article-likes')
    # ループ回して一個一個インスタンス生成
    for favButton in favButtons
      new FavArticle(favButton)

こっからajax。最初のdom取得以外全部jquery インターンでCoffee勉強しているからクラス作って書いたけど自信はない。

.js-article-likesを全部取得してインスタンス生成
コンストラクタでイベトリスナー呼び出し
イベントリスナーで_setLikesAjax()を呼び出し。
プライベートだから明示的にプレフィックス_をつける

@$likesButton.on 'click', (e) =>がアロー->じゃなくてファットアロー=>の理由
アローだと_setLikesAjaxについてるthis(coffeeでは@)がjqueryオブジェクト、ここではクリックしたa要素 を指してしまう。
だけどCoffeeだとファットアローにすればコンテクストを維持してくれるので、ここでのthisはインスタンス自身つまりFavArticleになる。 つまり前者でのthis._setLikeAjax$(eventObj)._setLikeAjaxになっちゃってエラーが帰ってくる。
後者ではFavArticle._setLikeAjaxと同義になってちゃんと処理が進む。(理解間違ってたらごめんなさい)
_setLikeAjaxのなかではいいねクリックした記事id,いいねURL、いいね削除URLを それぞれ変数に格納

もしクリックした要素が icon-fav-off(灰色のハート)クラスを持ってたら 「article likesコントローラーのcreateアクションにpostメソッド、帰ってくる型はjson」でajaxリクエストを出す。
dataは記事のid。

そしてうまくajax通信ができたらrails側からフロントにレスポンスが帰ってくるのでその処理をdone()の中に書く
処理がうまくいったということはいいねが作成されたということだからこんどはicon-fav-offをremoveしてicon-fav-onクラスをadd。
最後に返ってきたデータ(いいねの総数)を表示させておわり

クリックしたいいねボタンがicon-fav-on(いいね済みの赤いハート)のクラスを持ってたときはその逆

以上

つまったとこ

data-methodが切り替わらない

最初remote: :trueRailsの力を多大に借りて実装しようと思い実際にかなり最後のほうまで実装したのはよかったけど 最後の最後で、data-methodをattrしても要素の名前が変わるだけで リクエスト上ではメソッドが切り替わらないという自体が発生してしまって結局やめた。
.data(‘method’, ’NameofMethod')すればいけるって記事みたけど無理だった

記事のidをどうやってJSに渡すのかわからない

調べたら独自data属性を定義してそこに書いたのをdomで取得すればいいらしいということで今回はそれを実践。data-idという属性に記事idをいれてます。 content_tagを用いる方法もあるっぽい。 ( #324 Passing Data to JavaScript - RailsCasts

) ちなみに独自data属性をどうやってhamlで書くのかわからなくてこれもぐぐった (

haml に HTML5 のカスタムデータ属性(data-*) を定義する方法 - Qiita

) ちょいちょいhamlシンタックスエラーでてイラッとする。
ちなみにfontawesomeをlink_toで指定したaタグのなかに埋め込む場合はdoでブロックにする必要性がある

あとはいろいろあったけどもう忘れちゃった

謎なこと

railsから帰って来るjsonデータの構造

0: Object
likes_count: num

???

data.likes_countで出力させたいのに data[0].likes_count ってやらないととれない。なぞなぞー

最後に

すごく勉強になった。 でもいろいろ構造的に問題ある気がする。closest('a')とかあんまよくない気がして、 マークアップ構造変わったら終わる笑
処理がかぶってるところも多いからその辺まとめられたらもっといいかな。

あとはエラー処理できてないのが欠点だけどその辺は分岐させたりfailメソッド使えば ちょちょいといけそうな気がスル(なめてる)

次は投票グラフを得票数に応じて動的に変化させるっていうのにチャレンジしたいけどできない気しかしない

そのほか

インターン先のデザイナーさんにアマゾンが開発したダッシュボタン(

アマゾンの「ダッシュ・ボタン」、エイプリル・フールではない - WSJ

)ってサービスを教えてもらって感動した。 WEBとハードを組み合わせたサービスってほんとう素敵。 これこそIoT!(IoT言ってみたかっただけ)