Qiita Teams that are logged in
You are not logged in to any team

Log in to Qiita Team
Community
OrganizationEventAdvent CalendarQiitadon (β)
Service
Qiita JobsQiita ZineQiita Blog
59
Help us understand the problem. What are the problem?
Organization

【Rails 6】(初心者向け)Ajax版最小構成CRUDアプリ(ページ移動をゼロに!)

こちらの記事を解説した動画を YouTube にアップしております。是非ご視聴下さい!


Railsの学習で最初に学ぶのが,メッセージの作成・表示(読み取り)・更新・削除のできるCRUDアプリであると思います。私も最初にCRUDアプリを作成したときは,正直理解が追いつきませんでした:sweat_smile:

ところが,実際に作成してみるといろいろと不満が出てくることでしょう。一番は「見た目」だと思いますが,

「ボタンをクリックするたびにページを移動するのは嫌だなあ……」

と思いませんでしたか?特に本番環境では読み込みに時間がかかってしまいます。そこで,この記事では,ページ遷移ゼロの最小構成CRUDアプリを作成していきたいと思います:grinning:

通常のCRUDアプリを理解していたならば,本記事も理解できるようなるべく丁寧に解説していきます。

この記事では,Rails標準の Ajax の使い方を学ぶことだけに焦点を当てます。そのため,見た目・バリデーション・例外処理など細かいことは全て削ります。なお, jQuery の使用は避けて, Javascript を使用することとします。

完成後のアプリ

ajax_crud_sample.gif

  • 使用したアクションの用途
アクション HTTPメソッド 用途
index GET メッセージの一覧表示
new GET 新規投稿フォームに置き換える
create POST メッセージの「新規作成」
edit GET 更新フォームに置き換える
update PATCH / PUT メッセージの「更新」
destroy DELETE メッセージの「削除」
show GET (使用せず)

開発環境

  • macOS Catalina 10.15.2
  • Ruby 2.6.4
  • Rails 6.0.2

手順

0. 準備

  • まずはアプリを作成し,最低限の準備をしていきます。
    • Heroku へのデプロイまで挑戦されたい場合は, rails new のところで -d postgresql オプションを付け, $ rails db:create も実行して下さい。
ターミナル
$ rails new ajax_crud_sample
$ cd ajax_crud_sample
$ rails g controller messages index
$ rails g model Message content:string
$ rails db:migrate
  • 念のため,$ rails sでサーバーを起動し,http://localhost:3000にアクセスし,「Yay! You’re on Rails!」を確認しておいて下さい。

  • CRUDアプリを作成するための,ルーティングを設定しておきます。

config/routes.rb
Rails.application.routes.draw do
-  get 'messages/index'
+  root 'messages#index'
+  resources :messages
end
  • 一覧ページに投稿メッセージが表示されるようにします。
app/controllers/messages_controller.rb
class MessagesController < ApplicationController
  def index
    @messages = Message.all
  end
end
app/views/messages/index.html.erb
<!-- 各メッセージに対し,部分テンプレート _message.html.erb を呼び出す -->
<%= render @messages %>
app/views/messages/_message.html.erb
<p>
  <%= message.content %>
</p>

  • 確認用として,適当なメッセージをデータベースに投入します
db/seeds.rb
messages = %w[おはよう こんにちは こんばんは]
messages.each do |message|
  Message.create!(content: message)
end
puts '初期データの保存に成功しました!'
ターミナル
$ rails db:seed
  • トップページ(http://localhost:3000)にアクセスし,次のように表示されたならばOKです!

スクリーンショット 2019-12-23 10.54.26.png

1.メッセージの作成

準備ができましたので,まずはメッセージを投稿し,ページ遷移無しでそのメッセージを追加表示できるようにしましょう!

  • 最初にフォームを作成します。
app/views/messages/index.html.erb
<!-- 部分テンプレート _form.html.erb を呼び出す -->
<%= render 'form' %>
<hr>
<!-- 各メッセージに対し, _message.html.erb を呼び出す -->
<%= render @messages %>
app/views/messages/_form.html.erb
<!-- 新規メッセージ投稿用のフォーム -->
<%= form_with model: Message.new, local: false do |f| %>
  <%= f.text_field :content %>
  <%= f.submit "投稿" %>
<% end %>
  • ここでトップページを確認してみて下さい。次のように表示されていればOKです!

スクリーンショット 2019-12-23 11.12.59.png

もちろん,このままでは「投稿ボタン」を押しても,何も起こりません。対応するcreateアクションでメッセージの保存を指示します。

app/controllers/messages_controller.rb
  # (略)
  # ********** 以下を追加 ********** 
  def create
    Message.create!(message_params)
  end

  private

  # Strong Parameters はサボらずに使っておくこととします
  def message_params
    params.require(:message).permit(:content)
  end
  # ********** 以上を追加 ********** 
end

この時点で,フォームに入力して投稿してみて下さい。ページ内には変化は起きませんが,SQLが発行されていることをターミナルから確認できます。ページを更新してみて下さい。先ほど投稿したメッセージが反映されているはずです!

ここで,通常のCRUDアプリならば,例えば次のように書くでしょう。

app/controllers/messages_controller.rb
  def create
    Message.create!(message_params)
    # 実際に次の一行を追加して確認してもよいですが,その後削除して下さい!
    redirect_to root_path
  end

このようにすれば,新規メッセージを投稿 するだけでなく ページ更新 まで行われますので,一応,新規投稿機能ができたことになります。

ところが,今回は ページ遷移無し で投稿メッセージを追加表示させたいので,これではダメです:scream:ここで,トップページのソースを確認してみます。

<form action="/messages" accept-charset="UTF-8" data-remote="true" method="post">
<!-- (略) -->
</form>

フォームの箇所に, data-remote="true" が入っているはずです。

form_withでフォームを作成した場合は,local: trueオプションを付けない限り,自動的にこのデータ属性が追加されます。


【注意】 Rails 6.1 から仕様が変わり, local: true がデフォルトになりました。js.erb を呼び出すには local: false が必要となります。


この data-remote="true"含まれていない 場合は,コントローラの createメソッドで指示をしない限り, create.html.erb が呼び出されます。create.html.erb を自動で呼び出すにはlocal: trueオプションを付けなければなりません。
(なお, form_forform_tag の使用は 現在推奨されておりません

では, data-remote="true"含まれている 場合はどうなるのでしょうか。実は,自動的に Ajax を利用して非同期通信(ページ遷移無しに通信)が行われ, create メソッドが実行されます。更に,指示がない場合は create.js.erb が呼び出されるようになるのです。つまり,ページ遷移は起こらず,この create.js.erb に書いた処理が実行されるようになるのです!

Javascript を使うことで,ページの一部分を変更することができます。そこで,

  • 投稿したメッセージを,メッセージ一覧の一番下に追加する

というプログラムを書くことで, ページ遷移無し に新規メッセージを追加表示できるのです!具体的には次のように書きます。

app/views/messages/index.html.erb
<%= render 'form' %>
<hr>
<!-- メッセージ一覧を div タグで囲み,idを付けておく -->
<div id="messages">
  <%= render @messages %>
</div>
app/controllers/messages_controller.rb
  # 以下を変更(新規メッセージを create.js.erb で使えるようにインスタンス変数に入れておく)
  def create
    @message = Message.create!(message_params)
  end
  • 次の create.js.erb を作成します。
app/views/messages/create.js.erb
// メッセージ一覧の一番下に新規メッセージを追加する
document.getElementById('messages').insertAdjacentHTML('beforeend', '<%= j(render @message) %>')

Javascriptに慣れていない方もいらっしゃると思いますので,解説を入れます。 document.getElementById('messages') は, messagesというidが付いている要素を取得する操作です。今回のケースならば,次が取得されます。

document.getElementById('messages')
<div id="messages">
    <p>
        おはよう
    </p>
    <p>
        こんにちは
    </p>
    <p>
        こんばんは
    </p>
</div>

ここの</div>の前に新規メッセージを追加したいので,続けて次を書くことになります。

.insertAdjacentHTML('beforeend', //新規メッセージ// )

「新規メッセージ」の箇所は,例えば'<%= @message.content %>' としてもメッセージが追加されるのですが,これではダメです!

'<%= @message.content %>'ただの文字列 です。<p>タグで囲まれていませんので,2回投稿すると前のメッセージと繋がってしまいます

そもそも,単に文字列を追加したいのではなく, _message.html.erb に書いたテンプレートで新規メッセージを追加したいわけです。そこで,j(render @message)と書くことになります。

j とはなんぞや?」と思われたかもしれません。これは, escape_javascript メソッドです。 _message.html.erb 内の改行 \n をエスケープしないとJavascriptの構文エラーが発生します。

さて,メッセージを投稿すれば,ただデータベースに保存されるだけでなく,新規メッセージが一番下に追加表示されるようになりましたが,もう一つ問題があります。投稿したのに, フォームの文字が残ったまま になっています!

ページを更新していないので,指示をしなければ当然フォームの文字も残ったままになります。そこで,create.js.erb に次を追加して下さい。

app/views/messages/create.js.erb
// (略)
// フォームの文字を空にする
document.getElementById('message_content').value = ''

実は,フォームのテキストフィールドに id="message_content" が自動で付いています。これを取得し,値を空にする指示を出せばOKです。ここの id は ハイフン ではなく アンダーバー ですのでご注意下さい。

2. メッセージの削除

次にメッセージの削除機能を付けます:open_mouth:

  • まずは削除用のリンクを作成します。
app/views/messages/_message.html.erb
<!-- idを追加 -->
<p id="message-<%= message.id %>">
  <%= message.content %>
  <!-- 削除のリンクを追加 -->
  <%= link_to '削除', message_path(message),
              method: :delete,
              data: { confirm: '削除しますか?', remote: true } %>
</p>

  • コントローラに次を追加します
app/controllers/messages_controller.rb
  # idからメッセージを取り出す操作は他でも必要となるので最初からまとめておきます
  before_action :set_message, only: %i[destroy]
  # (略)
  def destroy
    @message.destroy!
  end

  private
  # (略)
  def set_message
    @message = Message.find(params[:id])
  end

普通のCRUDアプリとの違いは,まず, data 属性に remote: true を入れていることです。これで,コントローラの destroy メソッドの後にページ遷移が起こらず, destroy.html.erb ではなく destroy.js.erb が動作するようになります。

また,削除するメッセージを特定できるようにするため,各メッセージに id を付けておきます。

この時点でデータベースからメッセージの削除はできます。ところが,何も指示しなければページを更新しない限り,ページ内からメッセージが消えません。そこで,destroy.js.erb にメッセージを削除するプログラムを書きます。

app/views/messages/destroy.js.erb
document.getElementById('message-<%= @message.id %>').outerHTML = ''

ここも解説を入れておきます。削除する @message.id2 であるとすると,

document.getElementById('message-<%= @message.id %>')

により,次が取り出されます。

<p id="message-2">
    こんにちは
    <a data-confirm="削除しますか?" data-remote="true" rel="nofollow" data-method="delete" href="/messages/2">削除</a>
</p>

全てを消去したいので, outerHTML を空にするように指示します。これでページ遷移無しにメッセージが消えるようになりました!

3. メッセージの更新

メッセージの更新は少々大変です。普通のCRUDアプリならば,更新ボタンを押した後「更新用のページ」に移動させます。今回はページ遷移無しに更新部分を実装したいので,まずは更新ボタンを押した時に「更新用のフォーム」を表示できるようにしなければなりません。

なるべく簡単にしたいと思いますので,この記事では「新規メッセージの投稿フォーム」を「更新用のフォーム」に置き換えることにします。

  • まずは,更新フォームを呼び出すリンクを作成します。
app/views/messages/_message.html.erb
<p id="message-<%= message.id %>">
  <%= message.content %>
  <!-- ********** 以下を追加 ********** -->
  <%= link_to '更新', edit_message_path(message), data: { remote: true } %>
  <!-- ********** 以上を追加 ********** -->
  <%= link_to '削除', message_path(message),
              method: :delete,
              data: { confirm: '削除しますか?', remote: true } %>
</p>

  • フォームを更新用としても使えるように,フォームの部分テンプレートを修正しておきます。
app/views/messages/_form.html.erb
<!-- Message.new を 変数 message に変更 -->
<%= form_with model: message, local: false do |f| %>
  <%= f.text_field :content %>
<!-- ボタンの文字を 変数 value に変更 -->
  <%= f.submit value %>
<% end %>
app/views/messages/index.html.erb
<!-- 新規投稿メッセージなので,ボタンの文字は「投稿」とする -->
<%= render 'form', message: Message.new, value: '投稿' %>
<hr>
<div id="messages">
  <%= render @messages %>
</div>

この時点で次のような表示になります。

スクリーンショット 2019-12-23 19.57.35.png

  • コントローラに edit, update を追加します。
app/controllers/messages_controller.rb
  # edit, update でも呼び出すようにする
  before_action :message_set, only: %i[destroy edit update]

  # 更新用のフォームに置き換えることだけに使用する
  def edit
  end

  def update
    @message.update!(message_params)
  end

更新のリンクをクリックしたときに動作するのは edit.js.erb です。ここに,「更新用のフォーム」に置き換える操作を書きましょう。

app/views/messages/edit.js.erb
// フォームは一つしか無いので, id を使わず,タグ名から要素を取得
// ここの変数宣言に let や const を使用すると2回目にエラーが発生します
var form = document.getElementsByTagName('form')[0]
// フォームを更新用フォームに置き換えます。ボタンの文字は「更新」にします。
form.outerHTML = '<%= j(render 'form', message: @message, value: '更新') %>'

例えば,2番目のメッセージにある更新ボタンをクリックすると,次のような表示に変わります。

スクリーンショット 2019-12-23 21.01.06.png

コントローラにデータベースを更新する操作はすでに入れています。あとは更新用フォームの「更新」ボタンを押したときに,ページ内のメッセージを更新するようにします。

app/views/messages/update.js.erb
// 対応するメッセージを更新
document.getElementById('message-<%= @message.id %>').innerHTML = '<%= j(render @message) %>'
// フォームを新規投稿フォームに戻す
var form = document.getElementsByTagName('form')[0]
form.outerHTML = '<%= j(render 'form', message: Message.new, value: '投稿') %>'

これで,メッセージの更新も ページ遷移無し で実現できるようになりました:grin:

4. おまけ

最後に,フォームを「更新用」に変更したあと,「新規投稿用」に戻すリンクを作っておきます。見た目がいまいちですが,本筋から外れますので許して下さい:sweat_smile:

  • 更新用フォームにだけ「取消」のリンクを追加
app/views/messages/_form.html.erb
<%= form_with model: message, local: false do |f| %>
  <%= f.text_field :content %>
  <%= f.submit value %>
<!-- ********** 以下を追加 ********** -->
  <% if value == '更新' %>
    <%= link_to '取消', new_message_path, data: { remote: true } %>
  <% end %>
<!-- ********** 以上を追加 ********** -->
<% end %>
  • コントローラに追加
app/controllers/messages_controller.rb
  # 新規投稿用のフォームに置き換えることだけに使用する
  def new
  end
  • 「取消」ボタンを押したとき,「新規投稿用フォーム」に戻すようにする
app/views/messages/new.js.erb
var form = document.getElementsByTagName('form')[0]
form.outerHTML = '<%= j(render 'form', message: Message.new, value: '投稿') %>'

これで,「新規投稿用フォーム」に戻すこともできるようになりました。

今回の記事は Ajax を利用して ページ遷移無し でCRUDアプリを作成することのみに重点をおいて解説をしました。少しでも理解を進めるお手伝いができたならば幸いです。お疲れ様でした:grinning:

最終的なコード

$ git clone https://github.com/T-Tsujii/ajax_crud_sample.git
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
59
Help us understand the problem. What are the problem?