Help us understand the problem. What is going on with this article?

『非同期でのメッセージ投稿』が理解できる最低限のRailsアプリを丁寧に作る(Ajax苦手の自分とお別れしよう)

この記事の基本的な方針

Ajaxはなんだか難しい!は、勘違いです。一歩一歩きちんと進んでいけば、普通のことだと思えてくるでしょう。
ここではAjax非同期通信を理解するためだけの簡易なアプリを一つ丁寧に作成して、Ajax学習の基礎を完了することを目的としています。

この記事は、以下の「登録画面」「ログイン画面」「TOP画面」の3画面の簡単なアプリを元に拡張していきます
【TOP画面(ログイン前)】     【TOP画面(ログイン後)】
a0.png a9.png
【登録画面】
a1.png
【ログイン画面】
a2.png

手を動かしながら読みたいようでしたら、以下でこの3画面アプリを手に入れてください。

Terminal
$ git clone -b 超最低限のRailsアプリ(messageコントローラVer)  https://github.com/annaPanda8170/minimum_rails_application.git
$ bundle install
$ bundle exec rake db:create
$ bundle exec rake db:migrate

これ自体の作り方はこちら

想定する読み手

既に一度Railsアプリをチュートリアルやスクール等で作ったことがある方であり、JQueryの基本文法を理解している方を想定しております。
Mac使用で、パソコンの環境構築は完了していることが前提です。

取り急ぎ同期通信のメッセージ投稿機能をつくる

※本筋ではないので急ぎ足で説明します。詳しい説明が必要な方は、別記事をご覧ください(newアクションを使わず、formをindexに置くという違いがあります)。

① メッセージテーブルを作る

マイグレーションファイルモデルを作るため、

Terminal
$ rails g model message

を打ちます。
マイグレーションファイルに

db/migrate/2020xxxxxxxxxx_create_messages.rb
class CreateMessages < ActiveRecord::Migration[5.2]
  def change
    create_table :messages do |t|
      t.string :message
      t.references :user, foreign_key: true
      t.timestamps
    end
  end
end

となるように追記し、

Terminal
$ rails db:migrate

します。
これを済ませたら、メッセージテーブルは完成です。一応ちゃんと出来ているかデータベースを見に行ってみましょう。
mysql.png
私はSequelProを使っていてこんな感じです。

モデルにテーブル同士の関係を書きましょう。これが無くても投稿できなくはないのですが、投稿した内容を引き出して扱う上で便利なので今済ませてしまいましょう。以下を追記します。

app/models/message.rb
belongs_to :user
app/models/user.rb
has_many :messages

②メッセージだけを投稿できるようにする

ルーティング、ビュー、コントローラを編集します。今回はindexにフォームを置くのでnewアクションはなしです。

config/routes.rb
Rails.application.routes.draw do
  devise_for :users
  root 'messages#index'
  resources :messages, only: [:index, :create]
end
app/views/messages/index.html.erb
<% if user_signed_in? %>
  <%= current_user.email %>
  <%= link_to 'ログアウト', destroy_user_session_path, method: :delete %>
  <%= form_with(model: @message, local: true, class: "form") do |f| %>
    <%= f.text_field :message %>
    <%= f.submit "投稿" %>
  <% end %>
<% else %>
  <%= link_to '新規登録', new_user_registration_path %>
  <%= link_to 'ログイン', new_user_session_path %>
<% end %>

この時点で、localhost:3000でもlocalhost:3000/messagesでも
log.png
が表示されるはずです。この時点ではこの投稿フォームただの飾りなので、データベースに保存できるよう中身を作ります。
コントローラは以下です。

app/controllers/messages_controller.rb
class MessagesController < ApplicationController
  before_action :to_root, except: [:index]
  def index
    @message = Message.new
  end
  def create
    @message = Message.new(message_params)
    @message.save
    redirect_to root_path
  end
  private
  def message_params
    params.require(:message).permit(:message).merge(user_id: current_user.id)
  end
  def to_root
    redirect_to root_path unless user_signed_in?
  end
end

これで一度投稿してみましょう。

goodafternoon.png
問題なさそうですね。

③投稿を表示

あとはTOP画面に投稿されたものを全て表示させます。以下を追記します。

app/controllers/messages_controller.rb
class MessagesController < ApplicationController
  〜省略〜
  def index
    @messages = Message.all
    @message = Message.new
  end
  〜省略〜
end
app/views/messages/new.html.erb
〜省略〜
<% @messages.each do |m| %>
  <div style="margin-top: 20px;"><span style="color: red;"><%= m.user.email %></span><%= m.message %></div>
<% end %>

hello.png
大丈夫ですね。

投稿するときに画像の上の丸い矢印が一瞬×になって投稿が反映されると思いますが、これなしに反映されるのが非同期通信です。

④JQueryを導入し、turbolinksを削除する

turbolinksを削除する理由は、JQueryの動きを阻害する可能性があるからです。(リロードすればjsが動作するのに、リンクで移動すると動作しない、等)

Gemfilegem 'jquery-rails'を加え、gem 'turbolinks'をコメントアウトするか消し、

gemfile
×  gem 'turbolinks'

   gem 'jquery-rails'

bundle installしサーバの再起動します。
app/assets/javascripts/application.js//= require jquery//= require jquery_ujs//= require_tree .より上に追記し、//= require turbolinksを消します。

app/assets/javascripts/application.js
× //= require turbolinks

  //= require jquery
  //= require jquery_ujs
  //= require_tree .

以下を修正します。

app/views/layouts/application.html.erb`
× <%= stylesheet_link_tag    'application', media: 'all', 'data-turbolinks-track': 'reload' %>
   <%= javascript_include_tag 'application', 'data-turbolinks-track': 'reload' %>

       |
       v

◯ <%= stylesheet_link_tag    'application', media: 'all' %>
   <%= javascript_include_tag 'application' %>

続いてapp/assets/javascriptsmessages.jsを作ります。app/assets/javascripts/messages.coffeeがあると作成したファイルが機能しないので削除します。

app/assets/javascripts/messages.js
$(function () {
  console.log("OK")
});

を書いて、ブラウザをどの画面でもいいのでリロードします。
コンソールにOKが表示されたら成功です。

これで準備は終わりです。

いよいよ本筋、非同期実装

完成品GitHub(masterではなく一つのブランチなので注意して下さい)

①投稿ボタンを押すとイベント発火させる

以下のようにjsファイルを直しフォームの投稿ボタンが押されたときにコンソールにOkが出てくるか確認します。
function後の()にeをお忘れなく。

app/assets/javascripts/messages.js
$(function (e) {
  $(".form").on("submit", function () {
    console.log("Ok")
  })
});

一瞬だけ表示されてすぐに消えますね。投稿されたらTOP画面に(つまり同じ画面に)リダイレクトするのですから当然ですね。今はリダイレクトせずに投稿が反映されるようにするためにこの動きをjs内で止めます。以下に直してください。

app/assets/javascripts/messages.js
$(function () {
  $(".form").on("submit", function (e) {
    console.log("Ok")
    e.preventDefault();
  })
});

これでOkが残るようになりました。

②formの情報をjsで受け取り、createアクションに渡して投稿する

まずjsでformの情報を受け取る型は以下のような感じです。

app/assets/javascripts/messages.js
$(function () {
  $(".form").on("submit", function (e) {
    e.preventDefault();
    $.ajax({
      url:  (1) ,
      type: (2) ,
      data:  (3) ,  
      dataType: 'json',
    })
  })
});

加わったのは$.ajaxのくだりですね。
4つの項目がありますが、dataTypeはとりあえず'json'でいいです。jsonとはデータの形式で、{a: b,c: d}みたいなやつです。Rubyでいうハッシュ、JavaScriptでいくオブジェクトですね。簡単です。
(json以外にも、XMLやHTMLでもできるみたいですね)

(1)〜(3)を埋めて行きます。
(1)はcreateアクションにいくurlです。rails routesで確認すれば一発ですね。今回の私の場合は/messagesです。
(2)は、HTTPメソッドです。createアクションに行くので、'POST'ですね。
(3)は、検証ツールでメッセージを書き込むinputタグのname属性をみればわかります。
jjjj.png
ありました。このように参照される値なので、{message: {message: <投稿内容> }}のように渡せばいいですね。
では投稿内容はどうすれば良いかというとidがmessage_messageになっているので、$("#message_message").val()で取れます。(詳しい説明は省きます)
これを埋めて、投稿完了したときにたどり着くdoneメソッドをajaxメソッドに連ねて書きます。

app/assets/javascripts/messages.js
$(function () {
  $(".form").on("submit", function (e) {
    e.preventDefault()
    $.ajax({
      url:  "/messages" ,
      type: "POST" ,
      data:  {message: {message: $("#message_message").val() }} ,  
      dataType: 'json',
    }).done(function (data) {
      console.log("ok");
    });
  })
});

これで一度投稿してみましょう。データベースを見れば投稿は成功しているのがみて取れます。
以下に同期・非同期の両方でのターミナルでの状態を掲載します。

html.png
json.png

このような違いが出てますね。そしてなぜかコントローラのcreateアクションの最後のredirect_to root_pathが効かなくなりました。今コンソールにokは見られません。コントローラに残って機能しなくなったredirect_to root_pathが阻害しているようです。これを削除してもう一度投稿すれば、コンソールにokが見られるはずです。

このあと,投稿が完了した時に、formの値を全てなくして、投稿ボタンを蘇るようにします。
formの値をまとめてなくすには$('.form')[0].reset();を追記します。[0]がなぜ必要なのかはよくわかりません。
続いて投稿ボタンはerbファイルでボタンに適当にidを指定して(私は<%= f.submit "投稿", id:"bbb" %>こうしました)、$('#bbb').prop('disabled', false);を追記します。
全体を見てみます。

app/assets/javascripts/messages.js
$(function () {
  $(".form").on("submit", function (e) {
    e.preventDefault()
    $.ajax({
      url:  "/messages" ,
      type: "POST" ,
      data:  {message: {message: $("#message_message").val() }} ,  
      dataType: 'json',
    }).done(function () {
      $('.form')[0].reset();
      $('#bbb').prop('disabled', false);
    });
  })
});

これでリロードしなくでも、何回でも投稿できるようになりました。

あとは表示できるようにすればOKですね。

③投稿内容を非同期で表示させる

Ajaxでやってきたデータを扱うにはrespond_toメソッドを使います。

createアクションの最後のredirect_to root_pathがあった場所に、以下を追記します。

app/controllers/messages_controller.rb
respond_to do |format|
  format.json {render json: { ccc: @message.message , ddd: @message.user.email}}
end

メッセージ投稿内容とメッセージを送った人のEmailをそれぞれcccとdddに格納してjson形式でレンダーしますよってことですね。

あとは、doneイベント内で情報を受け取ってHTMLに整形してappendするだけです。
doneイベント内のfunction後の()内に何か文字をおけばそこに上のjsonデータが格納されます。今回はeeeとしてみました。これをコンソール出力してみます。

app/assets/javascripts/messages.js
省略
}).done(function (eee) {
  console.log(eee);
  $('.form')[0].reset();
  $('#bbb').prop('disabled', false);
});
省略

これで投稿してみると、コンソールで

Output
{ccc: "ハロー", ddd: "aaa@aaa"}

大丈夫そうですね。 これがeeeの中に入っているわけですから、"ハロー"を取得するにはeee.cccで、"aaa@aaa"を取得するには…大丈夫ですね。

あとは、これを表示させます。appendするために

app/views/messages/new.html.erb
〜省略〜
<% @messages.each do |m| %>
  <div style="margin-top: 20px;"><span style="color: red;"><%= m.user.email %></span><%= m.message %></div>
<% end %>

これをdivタグで囲って、適当なidをつけます。今回はaaaとしました。

app/views/messages/new.html.erb
〜省略〜
<div id="aaa">
  <% @messages.each do |m| %>
    <div style="margin-top: 20px;"><span style="color: red;"><%= m.user.email %> </span><%= m.message %></div>
  <% end %>
</div>

あとは

app/assets/javascripts/messages.js
$("#aaa").append(`<div style="margin-top: 20px;"><span style="color: red;">${eee.ddd}</span>${eee.ccc}</div>`)

を追記するだけです。
これで完成です。投稿して確認してください。

最後に再掲します。

app/assets/javascripts/messages.js
$(function () {
  $(".form").on("submit", function (e) {
    e.preventDefault();
    $.ajax({
      url: "/messages",
      type: "POST",
      data: { message: { message: $("#message_message").val() } },
      dataType: 'json',
    }).done(function (eee) {
      $('.form')[0].reset();
      $('#bbb').prop('disabled', false);
      $("#aaa").append(`<div style="margin-top: 20px;"><span style="color: red;">${eee.ddd}</span>${eee.ccc}</div>`)
    });
  })
});
app/views/messages/new.html.erb
<% if user_signed_in? %>
  <%= current_user.email %>
  <%= link_to 'ログアウト', destroy_user_session_path, method: :delete %>
  <%= form_with(model: @message, local: true, class: "form") do |f| %>
    <%= f.text_field :message %>
    <%= f.submit "投稿" , id: "bbb"%>
  <% end %>
  <div id="aaa">
    <% @messages.each do |m| %>
      <div style="margin-top: 20px;"><span style="color: red;"><%= m.user.email %> </span><%= m.message %></div>
    <% end %>
  </div>
<% else %>
  <%= link_to '新規登録', new_user_registration_path %>
  <%= link_to 'ログイン', new_user_session_path %>
<% end %>

まとめ

本当に最低限です。

これを、整えて十分な状態にするための続きをまた書きます。
フォローしてお待ち下さい。

annaPanda8170
長いコードを"上から順に"説明されても理解できないですよね? "必要な順に"コードを増やしてゆくスタイルで記事を書いていくつもりです。 私の記事は半分パブリックなものだと思っているので、どんどん意見を吸収して育てていきたいです。
https://annapanda.xyz/
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away