1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

【Ruby on Rails】ajaxを使ってタグの追加と削除、(成功・エラー)メッセージの表示を行う。

Last updated at Posted at 2020-09-29

環境

Ruby 2.5.7
Rails 5.2.4

ライブラリ

jQuery 1.12.4

前提

今回はTagテーブルを使って解説していきます。

まずはajaxを使わなくてもタグの追加と削除ができることを事前にご確認ください。

エラーメッセージの日本語化やアソシエーションについては触れませんので必要な方はご自身でお願い致します。

turbolinksは無効にしています。(有効時の動作は確認していません。)

手順

タイトルにもある通り、次の手順でajaxを利用してタグの追加・削除・エラーメッセージの表示を実装していきます。

1.tag.rbにバリデーションの設定
2.部分テンプレートの用意(views/layouts/_error_messages.html.erb, views/layouts/_flash_messages.html.erb, views/tags/_tag.html.erb)
3.タグの一覧画面、兼新規作成フォームを作成(views/tags/index.html.erb)
4.アクションごとのjs.erbを用意(views/tags/create.js.erb, views/tags/destroy.js.erb)
5.tags_controller.rb(:index, :create, :destroy)の用意

イメージとしては、送信ボタンを押すとコントローラーのcreateアクションが実行
createアクション内でcreate.js.erbを呼び出すメソッドが実行
create.js.erb内に書かれているJavaScript(jQuery)が発火
DOM操作により、エラーメッセージの要素とタグ一覧の要素が上書き(jQueryの.html()メソッド)
削除時のdestroyアクションも同様です。

完成形の流れとしては
1.index画面でフォームに値を入力する。
1-1.存在しないタグなら新規保存(サクセスメッセージ)
1-2.存在するタグなら保存失敗(バリデーションエラーメッセージ)
1-3.空白なら保存失敗(バリデーションエラーメッセージ)

2.個別のタグを削除する
1-1.サクセスメッセージを表示

全てajaxのため、ページの全体の更新はありません。

1.tag.rbにバリデーションの設定

tag.rb
class Tag < ApplicationRecord

  ...

  # 空白の禁止と一意性(重複NG)を付与します。
  validates :name, presence: true, uniqueness: true

end

2.部分テンプレートの用意(layouts/_error_messages.html.erb, layouts/_flash_messages.html.erb, _tag.html.erb)

ここで作る3つの部分テンプレートを、アクションに応じてDOM操作で上書きしていく形です。
jQueryで書くと1行1行DOM操作をすることになって冗長になってしまうので、部分テンプレートを利用して後述するjQueryが<%= render ... %>1行で済むようになっています。

バリデーションエラーメッセージ用の部分テンプレート
すでにある方は流用してもらって大丈夫です。

layouts/_error_messages.html.erb
<% if model.errors.any? %>
  <div id="validation-errors">
    <p><%= model.errors.count %>件のエラーがあります。</p>
    <ul class="error-messages">
      <% model.errors.full_messages.each do |message| %>
        <li class="error-message"><%= message %></li>
      <% end %>
    </ul>
  </div>
<% end %>

jQueryのDOM操作によって上書きされた時にバリデーションエラーが発生していれば、エラーメッセージが表示されます。
ローカル変数のmodelは後述するindex.html.erbで@tag(Tag.new)に置き換えます。

次にサクセスメッセージ(フラッシュメッセージ)用の部分テンプレート
こちらもすでにある方は流用してください。

layouts/_flash_messages.html.erb
<% flash.each do |key, value| %>
  <p class="alert alert-<%= key %>">
    <%= value %>
  </p>
<% end %>

eachで配列から取得することになっていますが、これは複数のフラッシュメッセージを取得するというよりも、どのようなキー(flash[キー])でもこの一文で再表示ができるようになっています。
今回使うキーは[:success], [:warning]の2種類です。(ちなみにこの2種類はbootstrapでデザインも用意されているため、今回選択しました。)
この二つを使う場合は部分テンプレートを下記のように書き換えることもできます。

layouts/_flash_messages.html.erb
<% case flash.keys[0] %>
  <% when "success" %>
  <p class="alert alert-<%= flash.keys[0] %>">
    <%= flash[:success] %>
  </p>
  <% when "warning" %>
  <p class="alert alert-<%= flash.keys[0] %>">
    <%= flash[:warning] %>
  </p>
<% end %>

flashに格納されるキーは配列のため、keys[0]とする必要があります。
case文でkeyパターンに応じて表示するフラッシュメッセージを変化させます。
p要素のclassでkeys[0]を使用している理由は、キーによって(今回の場合はsuccessかwarning)cssのデザインを変更するためです。

しかしこれでは、キーの種類(キーの名前は必要に応じて任意でつけることができます。)が増えた時にその都度行を増やしていく必要があるため、冗長になってしまいます。

そこで今回はeach文を使用することで、その手間をなくした形です。
また部分テンプレート化することで、バリデーションエラーメッセージと同様に、他のviewでも使い回すことができます。

次にタグ一覧を表示する部分テンプレート

tags/_tag.html.erb
<div class="tags-index__tags--tag">
  <%= tag.name %>(<%= tag.menu_tag_ids.count %>)
  <%= link_to 'X', tag_path(tag), method: :delete, remote: true %>
</div>

こちらも部分テンプレートを使わなければ後述するjQueryで4行分書く必要がり、jQueryでのDOM操作は極力シンプルに管理したいので部分テンプレート化しています。
ローカル変数のtagはindex.html.erbでインスタンス変数@tags(Tag.all)を指定することで、部分テンプレート内では個別のローカル変数tagとして自動で置き換わります。(index.html.erbのところで詳しく解説します。)
(<%= tag.menu_tag_ids.count %>)はタグ名の横にそのタグが使われているメニューの数を表示しています。(メニューテーブル(割愛) 1:多 メニュータグテーブル(中間テーブル)(割愛) 多:1 タグテーブル(今回))
<%= link_to 'X', tag_path(tag), method: :delete, remote: true %>を押すとdestroyアクションが発動します。remote: trueとすることで、ajax通信であることを明示します。

"remote: true"の仕組み 通常のリクエスト(リンク)ではHTMLを取得しますが、`remote: true`(html変換後は`data-remote="true"`属性)が付与されたリクエストの場合はJSファイルのみを取得します。
Railsの場合はlayouts/application.html.erbの``タグ内にjsを読みこむタグがあるので、通常はHTMLを取得するとjsも自動で再読み込みがされます。なお、turbolinksを使っている場合はこの限りではありません。

3.タグの一覧画面、兼新規作成フォームを作成(views/tags/index.html.erb)

タグの一覧画面とその中に新規入力のフォームも作成していきます。

views/tags/index.html.erb
<div class="contents tags-index">
  <div class="tags-index--messages"><!--メッセージの表示エリア--></div>
  <h2>タグ一覧</h2>
  <div class="tags-index__list" id="tags-index--tag-list">
    <%= render @tags %>
  </div>
  <div class="tags-index__form">
    <%= form_with model: @tag, url: tags_path(@tag) do |f| %>
      <%= f.text_field :name %>
      <%= f.submit %>
    <% end %>
  </div>
</div>

タグ一覧の下に新規追加フォームがくっついてる状態です。

新規登録フォームはform_withを利用します。
form_withはデフォルトがremote: true属性を持っているので__省略可能__です。(form_forを使う場合はremote: true属性を明示する必要があります。)
form_withでajaxを使わない場合はlocal: true属性を明示する必要があります。
フォームヘルパーはremote: trueの場合はJSのみを、local: trueの場合はHTMLをアクションに伝えます。
また、フォームヘルパーはデフォルトのHTTPメソッドがPOSTでもあるのでmethod: :postも__省略可能__です。

<%= render @tags %>のインスタンス変数 @tagsは後述するコントローラー側でTag.allを取得しています。
見慣れない部分テンプレートの書き方ですが、この略記法の部分テンプレートは変数名に対応する_変数名.html.erbが呼び出され、その部分テンプレート内ではローカル変数(今回でいう部分テンプレート内のtag)として使えるようになります。
今回の@tagsように配列で取得した場合は、部分テンプレート内ではその配列の数だけ、繰り返し表示がされます。
これは書き換えると以下のようになります。

views/tags/index.html.erb
<%= render partial: 'tag', locals: {tags: @tags} %>
views/tags/_tag.html.erb
<% tags.each do |tag| %>
  <div class="tags-index__tags--tag">
    <%= tag.name %>(<%= tag.menu_tag_ids.count %>)
    <%= link_to 'X', tag_path(tag), method: :delete, remote: true %>
  </div>
<% end %>

今回の記述方法はこれらを略記した形です。
Railsガイド - レイアウトとレンダリング

4.アクションごとのjs.erbを用意(views/tags/create.js.erb, views/tags/destroy.js.erb)

コントローラーから呼び出すjs(.erb)を作成します。
ここにjsを記述することで、コントローラーからcreate, destroyがそれぞれ呼び出された時に発火するjQueryでDOM操作が行われます。
ここで使われている.html()はそこに要素が何もなければ追加となり、すでに別の要素があれば書き換えとなります。

createアクション時のjs(.erb)

views/tags/create.js.erb
<% if @tag.errors.any? %>
  $(".tags-index--messages").html("<p><%= j(render partial: 'layouts/error_messages', locals: {model: @tag}) %></p>")
<% else %>
  $(".tags-index--messages").html("<p><%= j(render partial: 'layouts/flash_messages', locals: {model: @tag}) %></p>")
  $("#tags-index--tag-list").html("<%= j(render @tags) %>")
  $("#tag_name").val('')
<% end %>

<% if @tag.errors.any? %>はsubimitボタンが押された時に@tagにエラーが発生しているかどうかをチェックします。

"オブジェクト.errors.any?"と"オブジェクト.invalid?"の違い 今回の場合、コントローラー側で`@tag.save`が試された後にcreate.js.erbを呼んでいるため`@tag`にはすでにerrorが発生しているかどうかがわかる状態でjs.erbに渡って来ています。 他方、`invalid?`メソッドは保存前のオブジェクトに対して手動でバリデーションチェックを行うものです。 通常はオブジェクトが保存されようとした時(`.save`を試みた時)に自動でバリデーションチェックが実行され、今回は`.save`の成否によってアクションを分けるため、改めて手動で`invalid?`をするよりは、`.save`の時点ですでにエラーが発生しているかどうか(`.errors.any?`)を確認した方がDRYに則っていると言えます。

エラーが発生している場合(空白やタグ名の重複)
・バリデーションエラーメッセージ(error_messages.html.erb)を$(".tags-index--messages")に追加または書き換え

エラーが発生していない場合(正常に保存ができた)は
・フラッシュメッセージ(flash_message.html.erb)を$(".tags-index--messages")に追加または書き換え
・タグ一覧($("#tags-index--tag-list"))を書き換え
・フォームに入力されたタグ名をクリア($("#tag_name").val(''))

が、実行されます。

destroyアクション時のjs(.erb)

views/tags/destroy.js.erb
$(".tags-index--messages").html("<p><%= j(render partial: 'layouts/flash_messages', locals: {model: @tag}) %></p>")
$("#tags-index--tag-list").html("<%= j(render @tags) %>")

create時と同じです。
・タグが削除されたらフラッシュメッセージの追加または書き換え
・タグ一覧の書き換え

が、実行されます。

"partial: "の必要性renderを使ってオプションの追加(今回でいう`locals: {tag: @tag}`)が必要な場合はファイル名(パス)の前に`partial:`を明示する必要があります。 [Railsガイド - レイアウトとレンダリング](https://railsguides.jp/layouts_and_rendering.html#%E3%83%91%E3%83%BC%E3%82%B7%E3%83%A3%E3%83%AB%E3%82%92%E4%BD%BF%E7%94%A8%E3%81%99%E3%82%8B)

5.tags_controller.rb(:index, :create, :destroy)の用意

コントローラーに今回必要なアクションを記載していきます

tags_controller.rb
class TagsController < ApplicationController

  def index
    @tags = Tag.all
    @tag = Tag.new
  end

  def create
    @tags = Tag.all
    @tag = Tag.new(tag_params)

    respond_to do |format|
      if @tag.save
        format.js { flash.now[:success] = "保存しました。" }
      else
        format.js
      end
    end
  end

  def destroy
    @tags = Tag.all
    @tag = Tag.new
    Tag.find(params[:id]).destroy
    flash.now[:warning] = "削除しました。"
  end

  private
  def tag_params
    params.require(:tag).permit(:name)
  end

end

今回はcreateのform_with、destroyのlink_to、それぞれでremote: true(form_withの場合はデフォルトなので省略)を明示しているため、どちらもJSのみをリクエストしていることになります。

tags_controller.rb
def create

  ...

  respond_to do |format|
    if @tag.save
      format.js { flash.now[:success] = "保存しました。" }
    else
      format.js
    end
  end
end

respond_to do |format|はリクエストの形式によって処理を分けるブロックパラメータです。
今回、タグの追加・削除についてはHTMLで要求が来る(local: true)ことは想定していませんのでformat.js {処理}のみを記載しています。
HTMLでの要求が来た時に処理を分けたい場合はformat.html {処理}を書けば、リクエストの方式に応じで処理を分けることができます。
if @tag.saveが成功した場合はflash[:success]キーに"保存しました。"というメッセージを載せてcreate.js.erbに渡します。
失敗した場合はバリデーションエラーメッセージのみを表示するため、フラッシュメッセージは使いませんので、format.jsの後ろに処理は書かず、表示のみを行います。
前述しましたが、この時にバリデーションエラーが発生している@tagをcreate.js.erbに渡して、そこでエラー処理(バリデーションエラーメッセージの表示)をしています。

destroyアクションではflash[:warning]キーに"削除しました。"というメッセージを載せてdestroy.js.erbに渡します。

"format.js"を書く場合と書かない場合 通常、コントローラーにはviewファイルに対応する同名のアクション名を作ります。(`def index end` ならindex.html.erb)
アクション名の中身で特に`render`や`redirect_to`などのに指定がなければアクションの最後に`render :アクション名`が省略されて(暗示的に)実行されています。 (`def index end`など、中身を書かなくてもviewがレンダリングされるのはこのためです。) 今回、createアクションでは、条件によってフラッシュメッセージを渡したいので明示的に`format.js`を記述し、その中で処理を分けていましたが、destroyアクションでは今のところ処理を分ける必要はないので、`respond_to`や`format.js`、`render :destroy`は記述していません。 `render :destroy`に関してはすでに暗示的に記述されているため、書く必要がないということです。最初に書きましたが、暗示的に`render :アクション名`が発動するのはアクションの中で他に`render :別のアクション名`や`redirect_to`がない場合に限るので、他方createアクションのように明示的にformat.jsを記述し実行された場合、アクションの最後に暗示的な`render :create`が実行されることはありません。(DoubleRenderingErrorにならない理由です。)

Railsガイド - Action Controller の概要
Pikawaka - resoond_to
Qiita - 【Rails】flashメッセージを使用して簡易メッセージを表示させる詳しい方法と解説

まとめ

質問や解釈の違い、記述方法に違和感ありましたら、コメント等でご指摘いただけると幸いです。

最後まで読んでいただきありがとうございました。

参考サイト

Railsガイド - レイアウトとレンダリング
Railsガイド - Action Controller の概要
Pikawaka - resoond_to
Qiita - 【Rails】flashメッセージを使用して簡易メッセージを表示させる詳しい方法と解説

1
0
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
1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?