[Ruby on Rails] Add and delete tags and display (success / error) messages using ajax.

environment

Ruby 2.5.7 Rails 5.2.4

Library

jQuery 1.12.4

Premise

This time, I will explain using the Tag table.

First of all, please confirm in advance that you can add and remove tags without using ajax.

I will not touch on the Japanese localization of error messages and associations, so if you need it, please do it yourself.

turbolinks is disabled. (We have not confirmed the operation when it is enabled.)

procedure

As the title says, we will implement tag addition / deletion / error message display using ajax in the following procedure.

  1. Validation settings in tag.rb
  2. Preparation of partial templates (views / layouts / _error_messages.html.erb, views / layouts / _flash_messages.html.erb, views / tags / _tag.html.erb)
  3. Create tag list screen and new form (views / tags / index.html.erb)
  4. Prepare js.erb for each action (views / tags / create.js.erb, views / tags / destroy.js.erb)
  5. Preparation of tags_controller.rb (: index,: create,: destroy)

As an image, when you press the submit button, the controller create action is executed The method that calls create.js.erb is executed in the create action JavaScript (jQuery) written in create.js.erb fires DOM manipulation overwrites error message element and tag list element (jQuery .html () method) The same applies to the destroy action when deleting.

As the flow of the completed form

  1. Enter the value in the form on the index screen. 1-1. Save a new tag if it does not exist (success message) 1-2. If the tag exists, save failure (validation error message) 1-3. If blank, save failure (validation error message)

  2. Delete individual tags 1-1. Display success message

Since everything is ajax, there is no update of the entire page.

1. Validation settings in tag.rb

tag.rb


class Tag < ApplicationRecord

  ...

  #Prohibits blanks and grants uniqueness (duplicate NG).
  validates :name, presence: true, uniqueness: true

end

2. Preparation of partial template (layouts / _error_messages.html.erb, layouts / _flash_messages.html.erb, _tag.html.erb)

The three partial templates created here are overwritten by DOM operations according to the action. If you write in jQuery, it will be redundant because you will be doing DOM operations line by line, so use a partial template so that jQuery described later can be done with <% = render ...%> 1 line It has become.

Partial template for validation error messages If you already have one, you can use it.

erb:layouts/_error_messages.html.erb


<% if model.errors.any? %>
  <div id="validation-errors">
    <p><%= model.errors.count %>There are some errors.</p>
    <ul class="error-messages">
      <% model.errors.full_messages.each do |message| %>
        <li class="error-message"><%= message %></li>
      <% end %>
    </ul>
  </div>
<% end %>

If there is a validation error when overwritten by jQuery's DOM manipulation, an error message will be displayed. Replace the local variable model with @ tag (Tag.new) in index.html.erb, which will be described later.

Next, a partial template for success messages (flash messages) If you already have this, please use it.

erb:layouts/_flash_messages.html.erb


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

It is supposed to get from the array with each, but this is not to get multiple flash messages, but any key (flash [key]) can be redisplayed with this one sentence I will. There are two types of keys used this time: [: success] and [: warning]. (By the way, these two types are also available in bootstrap, so I chose this time.) If you use these two, you can rewrite the partial template as follows.

erb: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 %>

The keys stored in flash are arrays, so they must be keys [0]. Change the flash message to be displayed according to the key pattern in the case statement. The reason for using keys [0] in the class of the p element is to change the design of css depending on the key (success or warning in this case).

However, this is redundant because it is necessary to increase the number of rows each time the number of key types (key names can be arbitrarily given) increases.

Therefore, this time, by using the each statement, the trouble is eliminated. Also, by making it a partial template, it can be reused in other views as well as validation error messages.

Partial template to display the tag list next

erb: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>

If you do not use a partial template, you need to write 4 lines with jQuery, which will be described later, and I want to manage DOM operations with jQuery as simply as possible, so I made it a partial template. The local variable tag is automatically replaced as an individual local variable tag in the partial template by specifying the instance variable @ tags (Tag.all) in index.html.erb. (I will explain in detail in index.html.erb.) (<% = tag.menu_tag_ids.count%>) shows the number of menus that use that tag next to the tag name. (Menu table (omitted) 1: Multi-menu tag table (intermediate table) (omitted) Multi: 1 tag table (this time)) For <% = link_to'X', tag_path (tag), method:: delete, remote: true%>, pressing X will trigger the destroy action. By setting remote: true, it is specified that it is ajax communication.

How "remote: true" works In a normal request (link), HTML is fetched, but in the case of a request with `remote: true` (`data-remote = "true" `attribute after html conversion), only JS file is fetched. ..
In the case of Rails, there is a tag that reads js in the `` tag of layouts / application.html.erb, so normally when HTML is acquired, js is also automatically reloaded. This does not apply if you are using turbolinks.

3. Create tag list screen and new form (views / tags / index.html.erb)

We will also create a tag list screen and a new input form in it.

erb:views/tags/index.html.erb


<div class="contents tags-index">
  <div class="tags-index--messages"><!--Message display area--></div>
  <h2>Tag list</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>

The newly added form is attached to the bottom of the tag list.

The new registration form uses form_with. form_with is optional because the default has the remote: true attribute. (When using form_for, it is necessary to specify the remote: true attribute.) If you don't use ajax in form_with, you need to specify the local: true attribute. The form helper tells the action only JS if remote: true and HTML if local: true. The form helper also has a default HTTP method of POST, so method:: post is also __optional __.

The instance variable @tags of<% = render @tags%>is gettingTag.all on the controller side described later. It is an unfamiliar way to write a partial template, but in this abbreviation partial template, _variable name.html.erb corresponding to the variable name is called, and the local variable ( in the partial template in this case) is called in that partial template. It can be used as tag). If you get it as an array like @ tags this time, it will be displayed repeatedly as many times as the number of arrays in the partial template. This can be rewritten as follows.

erb:views/tags/index.html.erb


<%= render partial: 'tag', locals: {tags: @tags} %>

erb: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 %>

The description method this time is an abbreviation of these. [Rails Guide-Layout and Rendering](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)

4. Prepare js.erb for each action (views / tags / create.js.erb, views / tags / destroy.js.erb)

Create js (.erb) to call from the controller. By writing js here, DOM operation will be performed with jQuery that fires when create and destroy are called from the controller respectively. The .html () used here will be added if there is no element there, and will be rewritten if there is already another element.

js (.erb) at the time of create action

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?%> Checks if there is an error in @ tag when the submit button is pressed.

Difference between "object.errors.any?" And "object.invalid?" In this case, since create.js.erb is called after `@ tag.save` is tried on the controller side, js.erb is in a state where it can be seen whether an error has already occurred in` @ tag`. I'm crossing. On the other hand, the ʻinvalid?` method manually validates the object before it is saved. Normally, the validation check is automatically executed when the object is about to be saved (when `.save` is tried), and this time the action is divided according to the success or failure of` .save`, so manually ʻinvalid?` Rather than doing, it is better to check if an error has already occurred at the time of `.save` (`.errors.any? `).

If an error occurs (blanks or duplicate tag names) -Add or rewrite validation error messages (error_messages.html.erb) to $ (". tags-index--messages ")

If no error has occurred (save was successful) -Add or rewrite flash message (flash_message.html.erb) to $ (". tags-index--messages ") -Rewrite the tag list ($ ("# tags-index--tag-list ")) -Clear the tag name entered in the form ($ ("# tag_name "). Val (''))

Will be executed.

js (.erb) during destroy action

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) %>")

Same as when creating. -Add or rewrite flash message when tag is deleted ・ Rewriting the tag list

Will be executed.

"Necessity of partial:" If you need to add an option using render (`locals: {tag: @tag}` in this case), the file name (path) You must specify `partial:` before. [Rails Guide-Layout and Rendering](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. Preparation of tags_controller.rb (: index,: create,: destroy)

I will describe the necessary actions this time on the controller

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] = "I saved it." }
      else
        format.js
      end
    end
  end

  def destroy
    @tags = Tag.all
    @tag = Tag.new
    Tag.find(params[:id]).destroy
    flash.now[:warning] = "It has been deleted."
  end

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

end

This time, form_with of create and link_to of destroy are each specifying remote: true (in the case of form_with, it is omitted because it is the default), so both are requesting only JS.

tags_controller.rb


def create

  ...

  respond_to do |format|
    if @tag.save
      format.js { flash.now[:success] = "I saved it." }
    else
      format.js
    end
  end
end

respond_to do |format|Is a block parameter that divides the process according to the request format. This time, we do not assume that a request will come in HTML (local: true) for adding / deleting tags, so onlyformat.js {processing}is described. If you want to separate the processing when a request in HTML comes, you can write format.html {processing} and divide the processing according to the request method. If ʻif @ tag.save is successful, put the message "Saved." In the flash [: success] key and pass it to create.js.erb. If it fails, only the validation error message is displayed, so the flash message is not used, so no processing is written after format.js, only the display is performed. As mentioned above, the @ tag` where the validation error occurred at this time is passed to create.js.erb, and the error processing (display of the validation error message) is performed there.

The destroy action puts the message " deleted. " On the flash [: warning] key and passes it to destroy.js.erb.

When writing "format.js" and when not writing Normally, the controller will have an action name with the same name that corresponds to the view file. (index.html.erb for `def index end`)
Unless otherwise specified in the contents of the action name, such as` render` or `redirect_to`,` render: action name` is omitted at the end of the action. It is running (implicitly). (This is why the view is rendered without writing the contents, such as `def index end`.) This time, in the create action, I want to pass a flash message depending on the condition, so I explicitly described `format.js` and separated the processing in it, but in the destroy action, it is not necessary to separate the processing so far. , `Respond_to`,` format.js`, `render: destroy` are not described. The `render: destroy` is already implicitly mentioned, so you don't have to write it. As I wrote at the beginning, the `render: action name` is implicitly triggered only when there is no other` render: another action name` or `redirect_to` in the action, so on the other hand the create action If you explicitly write format.js and execute it, the implicit `render: create` will not be executed at the end of the action. (This is the reason why Double Rendering Error does not occur.)

[Rails Guide-Overview of Action Controller](https://railsguides.jp/action_controller_overview.html#%E3%83%A1%E3%82%BD%E3%83%83%E3%83%89%E3%81 % A8% E3% 82% A2% E3% 82% AF% E3% 82% B7% E3% 83% A7% E3% 83% B3) Pikawaka - resoond_to [Qiita-[Rails] Detailed method and explanation for displaying simple messages using flash messages](https://qiita.com/dice9494/items/2a0e92aba58a516e42e9#flash%E3%81%A8flashnow%E3%81%AE] % E4% BD% BF% E3% 81% 84% E5% 88% 86% E3% 81% 91% E6% 96% B9% E3% 81% AF)

Summary

If you have any questions, differences in interpretation, or any discomfort in the description method, we would appreciate it if you could point them out in the comments.

Thank you for reading until the end.

Reference site

[Rails Guide-Layout and Rendering](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) [Rails Guide-Overview of Action Controller](https://railsguides.jp/action_controller_overview.html#%E3%83%A1%E3%82%BD%E3%83%83%E3%83%89%E3%81 % A8% E3% 82% A2% E3% 82% AF% E3% 82% B7% E3% 83% A7% E3% 83% B3) Pikawaka - resoond_to [Qiita-[Rails] Detailed method and explanation for displaying simple messages using flash messages](https://qiita.com/dice9494/items/2a0e92aba58a516e42e9#flash%E3%81%A8flashnow%E3%81%AE] % E4% BD% BF% E3% 81% 84% E5% 88% 86% E3% 81% 91% E6% 96% B9% E3% 81% AF)

Recommended Posts