Rails × Ajaxでクリックする度booleanが反転するボタンを実装する


はじめに

Railsで非同期通信をする際、リロードせずにbooleanを反転させたいケースがあり、やり方がわからず時間を費やしてしまったので自戒の意味を込めて記事に残させていただきます。

やりたいこと

↓画像のようなボタンを作り、非同期でbooleanが反転するようにしたい。

完成版パラメーター

【ボタンクリック1回目】

Parameters: {"user_groups"=>{"subscribed"=>"false"}}

【ボタンクリック2回目(非同期なのでリロードはしない)】

Parameters: {"user_groups"=>{"subscribed"=>"true"}}

ルーティング・コントローラー・ビュー

前提

以下の画像のようにuserとuser_groupは1対多の関係になっています。今回boolean反転させるのはuser_groupsテーブルのsubscribedカラムです。

ルーティング

今回はユーザーが複数所属するグループが『グループ単位でメルマガを購読するかどうか』を変更するオプション(そんなのあまりないかもしれませんが、例としてです・・)をユーザー側で変更できる実装をする前提で、update_optionsというアクション名にします。

update_subscribed等の名前の方が良いかもしれませんが、今後同じような操作をするボタンを他にも作ることを想定して共通化された名前としています。
また今回使用するのはPATCHアクションのみなので、ルーティングもPATCHの部分のみ記載しています。

routes.rb
resources :user_groups do
  member { patch 'update_options' => 'user_groups#update_options' }
end

コントローラー

user_groups_controller.rb
class UserGroupsController < ApplicationController
  before_action :set_group

  def index #set_groupで定義した@groupをviewファイルで使用
  end

  def update_options
    @group.update_attributes(user_group_params)
  end

  private

  def user_params
    params.require(:user_group).permit(:subscribed)
  end

  def set_group
    @group = current_user.user_group.find_by(id: params[:id])
  end
end

ログイン中のユーザーは以下でcurrent_userとして定義しています。

shared.rb
def current_user
  @current_user ||= User.find(session[:user_id]) if session[:user_id]
end

ビュー

スタイリングは本題とは外れるので大変恐縮ですが今回は触れずに行きます。

index.slim
= link_to update_options_user_path(user_group: {subscribed: !@group.subscribed}), remote: true, method: :patch do
 .btn
  | 変更

以上で大体実装ができましたが、このままだとボタンを複数回押した際にパラメーターが切り替わりません。
実際に押してみると、

【ボタンクリック1回目】

Parameters: {"user_groups"=>{"subscribed"=>"false"}}

【ボタンクリック2回目】

Parameters: {"user_groups"=>{"subscribed"=>"false"}}

このように非同期でbooleanが反転されません。もちろんリロードすれば成功します。

解決法①

原因は、index.slimの= link_to部分がボタンを押した際に書き換えられていないので、パラメーターに変化がありませんでした。
ですので、update_optionsアクション側でDBの値を得て変更させれば『非同期で複数回クリックしてもboolean値が切り替わる』ことは実現できました。

user_groups_controller.rb
class UserGroupsController < ApplicationController
  def update_options
    @group.update_attributes(subscribed: !@group.subscribed)
  end
end
index.slim
= link_to update_options_user_path, remote: true, method: :patch do
 .btn
  | 変更

このようにすれば、クリックを押すたびにDBのboolean値が切り替わります。
しかし、ユーザーが同時に複数人ログインしていてこのボタンを一斉に押されると、DBの値がユーザーの意図しない値に書き換わってしまうことも考えられるので、やはり『パラメーターの値によってDBの値が変更される』方が良さそうです。

解決法②(私はこちらを採用しました)

こちらの方法ではJavaScriptを使って= link_toのvalue(今回はURL部分のみ)部分を、クリックするたびに書き換える実装をします。

index.slim
= link_to update_options_user_path(user_group: {subscribed: !@group.subscribed}), id: "change-subscribed-link", remote: true, method: :patch do
 .btn
  | 変更

まずは先程のslimファイルの= link_to部分にidを付与します。

index.slim
id: "change-subscribed-link"

としました。
次にupdate_optionsアクションが走った時にJavaScriptが走るようにしたいので、update_options.js.erbファイルをindex.slimと同ディレクトリに作成し、以下をファイルに記載します。

update_options.js.erb
$("#change-subscribed-link").attr("href", "<%= update_options_user_path(subscribed: {has_filed: [email protected]}) %>")

このようにすることで、複数回クリックしても非同期でbooleanのパラメーターが反転するようになります。
こちらのコードではjQueryのセレクターで該当する= link_toを持ってきて、中身のURL部分だけを上書きしています。

少し見にくいですが、検証ツールでどういう挙動なのか確認できます。

このaタグのhref部分をupdate_options.js.erbファイルで書き換えたということです。

【ボタンクリック1回目】

Parameters: {"user_groups"=>{"subscribed"=>"false"}}

【ボタンクリック2回目(非同期なのでリロードはしない)】

Parameters: {"user_groups"=>{"subscribed"=>"true"}}

パラメーターも無事反転しました!
また今後別のボタンを作り、booleanのみを切り替える動作をさせたい場合は、update_optionsアクションを使用すれば良いので、一からルーティング、コントローラーを追加する必要もなくなりました!

さいごに

誤っている箇所がありましたらご指摘いただきたく存じます。
また、他にも何か良いやり方等ありましたらご教示いただければ幸いです!