LoginSignup
3
1

More than 3 years have passed since last update.

(rails,jQuery)クリックでカウントアップとデータの保存

Last updated at Posted at 2019-11-27

タイトルのように、クリックするごとに数字をカウントアップし、非同期でデータを保存する方法を紹介します

イメージとしては、動画を確認してください⬇️

自分もプログラミング初学者なので、もっとやり方あると思いますが、ググっても良い記事がなく、同じような実装に悩まれている方が多かったので、「とりあえず実装できる」レベルですが、誰かの参考になればと思います

あと、ここで例として紹介しているアプリケーションの内容は小説を定義(maintitle)し、その小説の内容(story)を随時アップしていくという内容になっていますので、なんとなくイメージしていただくと記事の内容が理解しやすいかなと思います

また、マイグレーションファイルの作成や、コントローラの作成など、ここで紹介していない部分の実装等については、別の方の記事等を参考にしてください

それではちょっと長くなりますが紹介していきます

  • ruby 2.5.1
  • rails 5.2.3

使用しているgem

  • 'gon'
  • 'device'
  • 'jquery-rails'
○○_create_reviews.rb
class CreateReviews < ActiveRecord::Migration[5.2]
  def change
    create_table :reviews do |t|
      t.integer :user_id
      t.integer :story_id
      t.integer :maintitle_id
      t.integer :review
      t.timestamps
    end
  end
end
app/models/review.rb
class Review < ApplicationRecord
  belongs_to :user
  belongs_to :story
  belongs_to :maintitle
end
config/routes.rb
resources :maintitles do
    # 省略
    resources :stories do
      post 'like_review'
      delete 'unlike_review' #まだ実装できてません・・
      resources :comments, only: [:create]
    end
  end
app/controllers/stories.controller.rb
before_action :set_maintitle, only: [:new,
                                     :create,
                                     :show,
                                     :like_review,
                                     # 省略]
before_action :set_story, only: [:show,
                                   # 省略]

////////////////////////////////////////////////////////
def show

    #set_maintitle
    #set_story

    @user = User.find(@story.user_id)
    if user_signed_in?
      @review = Review.find_by(story_id: @story.id, user_id: current_user.id)
      if @review != nil
        gon.my_review_count = @review.review + 1
      else
        @review = Review.create(user_id: current_user.id, story_id: @story.id, maintitle_id: @maintitle.id, review: 0)
        gon.my_review_count = @review.review + 1
      end
    end
    @reviews = Review.where(story_id: @story.id)
    @a = []
    @reviews.each do |review|
      p = review.review
      @a.push(p)
    end
    @review_all_count = @a.sum
    gon.review_all_count = @a.sum + 1

  end

////////////////////////////////////////////////////////

def like_review
  # set_maintitle
  @story = Story.find(params[:story_id])
  @review = Review.find_by(story_id: @story.id, user_id: current_user.id)
  p = @review.review + 1
  @review.update(review: p)
  render json: @story.id
end

////////////////////////////////////////////////////////

 private

  def set_maintitle
    @maintitle = Maintitle.find(params[:maintitle_id])
  end

  def set_story
    @story = Story.find(params[:id])
  end
app/views/stories/show.html.erb
# 省略
<%if @story.user_id != current_user.id%>
  <div class="like_review_area">
    <%=link_to maintitle_story_like_review_path(@maintitle, @story), method: :post, remote: :true, class: :like_review_btn do%>
      <div class="like_review_link">
        <div class="like_review", id="<%=@maintitle.id%>">
          面白かった数だけここをクリック!
        </div>
      </div>
    <%end%>
  </div>
<%end%>
<%if user_signed_in?%>
  <%if @story.user_id != current_user.id%>
    <div class="show_review_my_count_area">
      あなたの評価:
      <i class="fa fa-thumbs-o-up ">
      </i> 
      <i class="review_my_count_area">
        <%if @review.present?%>
          <i class="my_count">
            <%=@review.review%>
          </i>
        <%else%>
          <i class="my_count">
            0
          </i>
        <%end%>
      </i>
    </div>
  <%end%>
<%end%>
app/assets/javascripts/like_review.js
function appendFaThoumbsOUpMore(my_review_count) {
  var fa_thumbs_o_up_more = $(".review_my_count_area");
  var my_count_more = `<i class="my_count">
                        ${my_review_count}
                        </i>`
  fa_thumbs_o_up_more.append(my_count_more);
};

function appendFaThoumbsOUpAll(all_review_count) {
  var fa_thumbs_o_up_all = $(".review_all_count_area");
  var all_count = `<i class="all_count">
                        ${all_review_count}
                        </i>`
  fa_thumbs_o_up_all.append(all_count);
};

$(function() {
  $(document).on("ajax:success", ".like_review_btn", function(e) {
    e.preventDefault();
    $(".my_count").remove();
    $(".all_count").remove();
    var my_review_count = gon.my_review_count++ ;
    var all_review_count = gon.review_all_count++ ;
    appendFaThoumbsOUpMore(my_review_count);
    appendFaThoumbsOUpAll(all_review_count);
  });
});

ずらずら記載してすいません

ということでポイントを説明していきます

reviewテーブルを作成

自分は、誰がどの投稿に対して評価をつけたかわかるように、

○○_create_reviews.rb
class CreateReviews < ActiveRecord::Migration[5.2]
  def change
    create_table :reviews do |t|
      t.integer :user_id
      t.integer :story_id
      t.integer :maintitle_id
      t.integer :review
      t.timestamps
    end
  end
end

としましたが、単に評価の数だけならuser_idとかいらないと思います
あとそれぞれに

 null: false
 foreign_key: true

もつけた方がいいかもですね

modelにアソシエーションを定義

app/models/review.rb
class Review < ApplicationRecord
  belongs_to :user
  belongs_to :story
  belongs_to :maintitle
end

これはこのままでいいと思います
※ コードは載せませんが、ここでアソシエーションを組んでいる相手方には、has_many :reviewsを書いてください

routeを設定

config/routes.rb
resources :maintitles do
    # 省略
    resources :stories do
      post 'like_review'
      delete 'unlike_review' #まだ実装できてません・・
      resources :comments, only: [:create]
    end
  end

maintitlesにstoriesをネストさせ、storiesコントローラ内に

  • like_review
  • unlike_review

のアクションを設定します(後述)
ネストさせる意味は、reviewテーブルにmaintitle_idとstory_idを保存するためにやってます

controllerを設定

app/controllers/stories.controller.rb
before_action :set_maintitle, only: [:new,
                                     :create,
                                     :show,
                                     :like_review,
                                     # 省略]
before_action :set_story, only: [:show,
                                   # 省略]

////////////////////////////////////////////////////////
def show

    #set_maintitle
    #set_story

    @user = User.find(@story.user_id)
    if user_signed_in?
      @review = Review.find_by(story_id: @story.id, user_id: current_user.id)
      if @review != nil
        gon.my_review_count = @review.review + 1
      else
        @review = Review.create(user_id: current_user.id, story_id: @story.id, maintitle_id: @maintitle.id, review: 0)
        gon.my_review_count = @review.review + 1
      end
    end
    @reviews = Review.where(story_id: @story.id)
    @a = []
    @reviews.each do |review|
      p = review.review
      @a.push(p)
    end
    @review_all_count = @a.sum
    gon.review_all_count = @a.sum + 1

  end

////////////////////////////////////////////////////////

def like_review
  # set_maintitle
  @story = Story.find(params[:story_id])
  @review = Review.find_by(story_id: @story.id, user_id: current_user.id)
  p = @review.review + 1
  @review.update(review: p)
  render json: @story.id
end

////////////////////////////////////////////////////////

 private

  def set_maintitle
    @maintitle = Maintitle.find(params[:maintitle_id])
  end

  def set_story
    @story = Story.find(params[:id])
  end

ごちゃごちゃしててすいません・・
ここから実装についてのポイントになってきます

まず、before_actionで、set_maintitleとset_storyをshowアクションに反映させます

で、ログインしていれば評価ができる仕様にしてますので、

if user_signed_in?

を使用します

@review = Review.find_by(story_id: @story.id, user_id: current_user.id)   

これで、現在閲覧している小説の内容のidと、自分のidを持ったreviewレコードを探して取得します
すでに訪れていた場合はデータがありますが(後述)、初めて訪れた場合はもちろんnilとなります

if @review != nil
  gon.my_review_count = @review.review + 1
else
  @review = Review.create(user_id: current_user.id, story_id: @story.id, maintitle_id: @maintitle.id, review: 0)
  gon.my_review_count = @review.review + 1
end

これで、すでに訪れていた場合、gon.my_review_countというjsに渡す変数に、現在のreviewの数に+1させた数字を代入し、初めて訪れた場合はreviewに0を入れてcreateし、さらに前記同様変数に+1させた数字を代入します(後述)

@reviews = Review.where(story_id: @story.id)
  @a = []
  @reviews.each do |review|
    p = review.review
    @a.push(p)
  end
  @review_all_count = @a.sum
  gon.review_all_count = @a.sum + 1

この部分はstoryに対する各userの総評価数を取得する記述です

変数@reviewsに、whereでReviewテーブルから@story.idに一致するテーブルを全て取得し代入します

ここで、取得したデータからreviewカラムのデータを格納する空の配列@aを定義しておきます

whereで取得すると、配列でデータが取得されるので、eachで回し、reviewカラムのデータを抽出し、変数pへ代入し、空配列@aへpushにより格納していきます

そして、変数@review_all_countへ@aに入ったreviewデータの合計を.sumを用いて計算し、代入します
そして、jsへデータを渡すため、変数gon.review_all_countへ@a.sum + 1と、総評価数に+1をして代入します

like_reviewアクションを定義する

def like_review
  # set_maintitle
  @story = Story.find(params[:story_id])
  @review = Review.find_by(story_id: @story.id, user_id: current_user.id)
  p = @review.review + 1
  @review.update(review: p)
  render json: @story.id
end

このアクションで、reviewカラムにアクション毎(クリック毎)に+1させていきます

まず、現在のstoryのデータを取得し、変数@storyへ代入します

そして、変数@reviewへReviewテーブルからstory_id,user_idで条件指定したレコードを取得します

変数pへ、取得したReviewレコードのreviewカラムのデータへ+1したデータを代入します

最後に、そのテーブルのreviewカラムをアップデートします

※ render json: @story.idについては自分もよくわかっていませんが、この記述がないと、like_reviewのviewに飛ぼうとします
結果like_reviewアクションにタイムロスが生じてしまいますので、とりあえず書いておきましょう(ターミナルで確認するとわかります)
でもエラーにはならないんだよなーよくわからん

viewを作成する

<%=link_to maintitle_story_like_review_path(@maintitle, @story), method: :post, remote: :true, class: :like_review_btn do%>

viewは好みで作成していただくとして、大事なのは⬆️の一文です

link_toでlike_reviewアクションを実行します(pathはrake routesで確認してください)
このとき「remote: :true」を記述することにより、ajaxにより非同期でアクションが実行されます
よって、データ上の処理はこの時点でアクション毎に+1されていきます
(自分はまだajaxの仕組みをよく理解していませんので、詳しく知りたい方は別の記事等ググって調べてみてください)

また、showに初めて訪れた際、reviewカラムに0を入れてcreateすると前述しましたが、初めて訪れた直後というのは、何もデータが作成されていません
初めて訪れたときの処理の流れを説明すると、

  • showに訪れる
  • createされる

という順番のため、createしたデータを渡したくてもエラーになります

そのため、

<%if @review.present?%>
  <i class="my_count">
    <%=@review.review%>
  </i>
<%else%>
  <i class="my_count">
    0
  </i>
<%end%>

と、@reviewにデータが入っていない条件分岐を定義してあげて、エラーを回避&初期のreviewのデータ「0」を表示させなければなりません

jsでアクション毎に数字を表示する

app/assets/javascripts/like_review.js
function appendFaThoumbsOUpMore(my_review_count) {
  var fa_thumbs_o_up_more = $(".review_my_count_area");
  var my_count_more = `<i class="my_count">
                        ${my_review_count}
                        </i>`
  fa_thumbs_o_up_more.append(my_count_more);
};

function appendFaThoumbsOUpAll(all_review_count) {
  var fa_thumbs_o_up_all = $(".review_all_count_area");
  var all_count = `<i class="all_count">
                        ${all_review_count}
                        </i>`
  fa_thumbs_o_up_all.append(all_count);
};

$(function() {
  $(document).on("ajax:success", ".like_review_btn", function(e) {
    e.preventDefault();
    $(".my_count").remove();
    $(".all_count").remove();
    var my_review_count = gon.my_review_count++ ;
    var all_review_count = gon.review_all_count++ ;
    appendFaThoumbsOUpMore(my_review_count);
    appendFaThoumbsOUpAll(all_review_count);
  });
});

さて、最後の説明になります
まず、jsの大まかな処理の流れとして、

  • like_reviewアクションが実行されたら(ajax通信が行われたら)イベント発火
  • 古いデータの入ったhtml要素を排除
  • 新しいデータの入ったhtml要素を挿入

という流れになります

まず、上段のhtmlは置いておいて、下段を見ていきます

  $(document).on("ajax:success", ".like_review_btn", function(e) {

この記述は「like_review_btn」というクラスを持ったhtml要素がajax通信を成功したら処理を行うという意味になります

$(".my_count").remove();
$(".all_count").remove();

この記述で、それぞれのクラスを持ったhtml要素を排除します

var my_review_count = gon.my_review_count++ ;
var all_review_count = gon.review_all_count++ ;

これは、先ほどgonで定義した変数に+1したものを、それぞれ変数へ代入するものとなっています

ここで、controllerで定義したgonの変数について説明します
controllerでなぜ+1したかというと、データの表示はあくまで、「アクションが実行された後のデータ」を表示しなければなりません
なので、jsの++のみでは、1度目のアクションでの表示が「showに訪れた時のデータ」が表示されてしまいます
よって、コントローラで(jsでもできると思いますが)+1してあげることで、1度目のアクションでの表示がちゃんと+1されて表示することができるのです
(console.log等で確認してみるとわかります)

appendFaThoumbsOUpMore(my_review_count);
appendFaThoumbsOUpAll(all_review_count);

///////////////////////////////////////////////
function appendFaThoumbsOUpMore(my_review_count) {
  var fa_thumbs_o_up_more = $(".review_my_count_area");
  var my_count_more = `<i class="my_count">
                        ${my_review_count}
                        </i>`
  fa_thumbs_o_up_more.append(my_count_more);
};

function appendFaThoumbsOUpAll(all_review_count) {
  var fa_thumbs_o_up_all = $(".review_all_count_area");
  var all_count = `<i class="all_count">
                        ${all_review_count}
                        </i>`
  fa_thumbs_o_up_all.append(all_count);
};

ここでは、先ほどremoveで排除したhtml要素へ、データを更新したhtml要素を挿入する記述となっています

以上で説明を終了します!!
長々読んでいただきありがとうございました

文章書くの苦手なので非常にわかりにくかったかとは思いますが、これでとりあえずは実装できます
また、これで「評価の高い投稿」や、「自分が評価した投稿」等の検索や並び替えなども可能となります(と思う)

あと、今回の実装では最初、「1度目のアクションがcreateアクション」「2度目からupdateアクション」という2段構えでやっていたのですが、どうにもうまく行かなかったため、「showへ訪れたらcreateアクション」するという方法にシフトチェンジしました
なので、評価していなくてもデータが作成されるため、本当にこの実装の仕方でいいのか疑問が残っています
ここは要検討ですね

また、冒頭でも言いましたが、自分は初学者ですので、もっと簡単にできる等あれば、是非教えていただけるとありがたく感じます

ということで失礼します!

3
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3
1