おぴよの気まぐれ日記

おぴよの気まぐれ日記

岡山やプログラミング、ファッションのこと、子育てや人生、生き方についての備忘録。

30歳まで残り2年の僕は人生を変えるためにRailsチュートリアルを始めようと思う(番外編:いいね機能の拡張)

こんにちは。opiyoです。

今回は、番外編:いいね機能の拡張をやっていきます。

マイクロポストにハートアイコンを表示して、「いいね」できるようにします。

ではでは、早速行ってみましょう。

やること

仕様

「いいね」機能を実装するに当たって、ざっくりだけど仕様を明確にしてみようと思う。

  • 一つ一つのマイクロポストに「いいね」することができる
  • 「いいね」は一つのマイクロポストに対して一人一回まで
  • 「いいね」表示場所は、投稿日下にアイコンを使って表示する(いいね:ハート、削除:ゴミ箱)
  • 「いいね」されたら赤いハート、取り消されたら白いハートにする
  • 「いいね」された数を表示する(実装中)
  • 「いいね」ボタンはajaxで処理する(困ってる)

こんな感じだろうか。一人一回までって制御が出来れば色々サンプルはありそうだし出来そうかな?

対象画面とイメージ

対象になる画面は2つ

【トップページ:app/views/static_pages/home.html.erb】 f:id:opiyotan:20170908180203p:plain

【ユーザー詳細ページ:app/views/users/show.html.erb】 f:id:opiyotan:20170908180300p:plain

完成形(まだ途中だけど)

全部乗っけると数が多くなるので、主要な部分を載せます。

【view】

# app/views/likes/create.js.erb
$(".micropost<%= @micropost.id %> i").addClass("fa-heart");
$(".micropost<%= @micropost.id %> i").removeClass("fa-heart-o");
// $(".micropost<%= @micropost.id %>").text("<%= @micropost.likes_count %>"); # これやるとアイコンが消えちゃう
# app/views/likes/destroy.js.erb
$(".micropost<%= @micropost.id %> i").addClass("fa-heart-o");
$(".micropost<%= @micropost.id %> i").removeClass("fa-heart");
// $(".micropost<%= @micropost.id %>").text("<%= @micropost.likes_count %>"); # これやるとアイコンが消えちゃう
# app/views/microposts/_micropost.html.erb
<li id="micropost-<%= micropost.id %>">
  <%= link_to gravatar_for(micropost.user, size: 50), micropost.user %>
  <span class="user"><%= link_to micropost.user.name, micropost.user %></span>
  <span class="content">
    <%= micropost.content %>
    <%= image_tag micropost.picture.url if micropost.picture? %>
  </span>
  <span class="timestamp">
    Posted <%= time_ago_in_words(micropost.created_at) %> ago.
  </span>
  <span class="action">
    <% if micropost.like_user(current_user.id) %> # ----- ここから -----
      <%= link_to fa_icon("heart", text: micropost.likes_count), likes_destroy_path(micropost_id: micropost.id), remote: true, class: "micropost#{micropost.id}" %>
    <% else %>
      <%= link_to fa_icon("heart-o", text: micropost.likes_count), likes_create_path(micropost_id: micropost.id), remote: true, class: "micropost#{micropost.id}"  %>
    <% end %>
    <% if current_user?(micropost.user) %>
      <%= link_to fa_icon("trash"), micropost, method: :delete, data: {confirm: "You sure?"}, micropost_id: micropost.id  %>
    <% end %>  # ----- ここまで -----
  </span>
</li>

【controller】

# app/controllers/likes_controller.rb
class LikesController < ApplicationController
  def create
    micropost = Micropost.find(params[:micropost_id])
    Like.create(user_id: current_user.id, micropost_id: micropost.id)
    @micropost = Micropost.find(params[:micropost_id])

    respond_to do |format|
      format.html { redirect_to root_path}
      format.js
    end
  end

  def destroy
    micropost = Micropost.find(params[:micropost_id])
    Like.find_by(user_id: current_user.id, micropost_id: micropost.id).destroy
    @micropost = Micropost.find(params[:micropost_id])

    respond_to do |format|
      format.html { redirect_to root_path}
      format.js
    end
  end
end

【model】

# app/models/like.rb
class Like < ApplicationRecord
  belongs_to :micropost, counter_cache: :likes_count
  belongs_to :user
end
# app/models/micropost.rb
class Micropost < ApplicationRecord
  has_many :likes, dependent: :destroy
end

【routes】

# config/routes.rb
  get 'likes/create'
  get 'likes/destroy'

【migration】

class CreateLikes < ActiveRecord::Migration[5.0]
  def change
    create_table :likes do |t|
      t.integer :user_id
      t.integer :micropost_id

      t.timestamps
    end
  end
end
class AddIndexToLikesUserIdAndMicropostId < ActiveRecord::Migration[5.0]
  def change
    add_index :likes, [:user_id, :micropost_id], unique: true
  end
end
class AddlikesCountToMicroposts < ActiveRecord::Migration[5.0]
  def change
    add_column :microposts, :likes_count, :integer
  end
end

githubにpushしています。

色々と試行錯誤している跡がコミット追うと分かりますね。

ハマった/気づきポイント

いいねの数が変わらない

ハートのアイコンを押したらいいねの数がカウントアップするって処理をしたいのだけど、数が変わらない。

DB見ると間違いなく登録されている。何が問題なのかなーと思っていたら…馬鹿やろうだった。

# app/controllers/likes_controller.rb
  def create
    micropost = Micropost.find(params[:micropost_id])
    Like.create(user_id: micropost.user_id, micropost_id: micropost.id)
    @micropost = Micropost.find(params[:micropost_id]) # これしてなかった

    respond_to do |format|
      format.html { redirect_to root_path}
      format.js
    end
  end

createした後にデータを再取得する処理をしてなかったので、viewに返しているデータはcreateする前のオブジェクト。

そりゃー変わらないわ

未解決問題

いいねボタンの連続で押せない

今の段階だとajaxでアイコンといいねの数だけ切り替えてしまっている。 これだと切り替えたあと、link_toのパスとかが変わらないからおかしな状況になる。

だからきっと、その部分だけを別ファイルにしてrender hogeみたいにしてやればいいのかなって思ってる。

いいねの数を書き換えるとアイコンが消える!

やりたいことは、いいねの数を更新させたいだけなのだけど、単純に.text("hoge")ってやると子階層にあるタグが消えちゃう。

<div id="parent">
    親要素
  <p>子要素</p>
</div>
<button id="button">変更する</button>

$("#button").on("click", function(){
  $("#parent").text("変更後の文章");
});

<div id="parent">
    変更後の文章
</div>

こんな状況の時、pタグは消えます!

これはどうすればいいじゃろうか。多分根本的には上で書いたようにviewrenderするってのが正しいアプローチなんだとは思う。

最後に

まだ完成してないのだが、どうにかこうにか形になってきたので一先ずまとめてみました。

実装時間が長くなってしまったので当時ハマったことが少し思い出せない所もあり、もったい無いなと感じております。

解決できた時は嬉しくて先へ先へ進んじゃいがちだけど、一度立ち止まって何がダメだったのかをしっかり考える→まとめてみるってのは今後やっていこうと思いました。きっとそこが一番成長できる所だと思うので次回出会った時に「あーこの前のあれねって」思えるように。

まだまだ解決できてない部分もあるし、Likeモデルのcounter_cacheの機能もいまいち良く分かって無いので引き続き頑張ろうと思います。

未解決問題のajaxでの処理を追記

上で書いた2つの問題点を解決することに成功しましたので、追記でまとめます!

問題点

上に書いてある通りですが、大きく二つ

  • クラスの付けかけだけしてるので、link_toのパスが変わらない
  • いいねの数が消える

これに対するアプローチは、やはり

更新したい場所のみを再度renderする

うん。アプローチはやはり間違ってなかった!

解決したソース

とりあえずソースを貼ります!

【「いいね」前】 f:id:opiyotan:20170912074005p:plain

【「いいね」後】 f:id:opiyotan:20170912074034p:plain

# app/views/microposts/_micropost.html.erb
  <span class="action">
    <span id="like"> # ajaxで処理する時に「どこを差し替える」が必要なので、それを追加
      <%= render "microposts/hart", micropost: micropost %> # この部分を`_hart`としてテンプレート化する!
    </span>
    <% if current_user?(micropost.user) %>
      <%= link_to fa_icon("trash"), micropost, method: :delete, data: {confirm: "You sure?"}, micropost_id: micropost.id  %>
    <% end %>
  </span>
# app/views/microposts/_hart.html.erb
<% if micropost.like_user(current_user.id) %>
  <%= link_to fa_icon("heart", text: micropost.likes_count), likes_destroy_path(micropost_id: micropost.id), remote: true, class: "micropost#{micropost.id}" %>
<% else %>
  <%= link_to fa_icon("heart-o", text: micropost.likes_count), likes_create_path(micropost_id: micropost.id), remote: true, class: "micropost#{micropost.id}"  %>
<% end %>
# app/views/likes/create.js.erb
$("#like").html("<%= escape_javascript(render 'microposts/hart', micropost: @micropost) %>");
console.log("ajaxで処理")
# app/views/likes/destroy.js.erb
$("#like").html("<%= escape_javascript(render 'microposts/hart', micropost: @micropost) %>");
console.log("ajaxで処理")

解決のポイント

アプローチの仕方は間違ってなかった。ようはhartの部分だけ再度描画させたかったので、その部分を外出ししてそこだけ描画させる。

描画のやり方がjsのファイルで書いてあるやつだ。

likeクラスを持ったspanタグの中身をhtmlメソッドで全て置き換えているイメージ。

<%= escape_javascript(render 'microposts/hart', micropost: @micropost) %>の部分は極端に言うと展開されたhtmlになるようなイメージ。

だからこんな感じなのかな。

$("#like").html(<a class="micropost301" data-remote="true" href="/likes/create?micropost_id=301">
  <i class="fa fa-heart-o"></i> 0
</a>)


やっていく中でハマったポイントはmicropostインスタンスの渡し方。

最初jsのファイルにはmicropost: @micropostを書いて無くてずっとエラーになっていた。

Completed 500 Internal Server Error in 181ms (ActiveRecord: 2.7ms)

ActionView::Template::Error (undefined local variable or method `micropost' for #<#<Class:0x007fe0a1a1a368>:0x007fe0a6dc1110>
Did you mean?  @micropost):

micropostがねーよってことですね。だからコントローラーで取得したインスタンスを渡してあげて解決!

もっかいまとめ

ajaxの処理にめちゃくちゃハマって、すげー時間を使ってしまった。

最終的には教えてもらう形で解決出来たのだが、本当に感謝感謝感謝ですね。

よく未経験エンジニアからエンジニアへっていう記事がありますが、そこに絶対書かれているのが

メンターを見るけること

これは絶対そー思います。僕が3日くらい悩んでいたものが30分で解決出来てしまうのですから間違い無いです。

いつでも相談出来るちゃうものや無料でプログラミグを教えてくれて転職支援までしてくれるサービスなど本当にいっぱいあるので一度覗いてみてください。

オンラインプログラミング研修のCodeCamp

プログラミングのオンラインスクールCodeCamp

【未経験からプログラマ】完全無料であなたの就職をサポート


ちなみに、今回ajax処理を頑張って実装しましたが、別に無くても良いんじゃ無い?という話もありました。

普通にhtmlで返しても全く動きは同じだし、このぐらいのレベルならば正直あまりメリットは無い。だからスピードが遅くてダメだーってなってから対策するってアプローチの方で問題無いって。

一つのことにこだわることも大事だけど、別の事もやりたいとか全体が終わってないとかなるぐらいならば思い切って後回しってのは良いアプローチなのかもしれないですね。

ってな感じでした。