やりたいこと
- Rails の scaffold をジェネレートすると
form_with
にlocal: true
のオプションが付いています。 -
local
オプションのデフォルトはfalse
なわけで、 Ajax にしていこうという流れの中で scaffold のコードが Ajax じゃないのに違和感を覚えました。 - というわけで、 scaffold で生成されたコードに対し、現実的なコード改修量で Ajax リクエストによる CRUD を実現してみます。
前提
- webpacker を使っている
- rails-ujs を使っている
- turbolinks を使っている
$ yarn add rails-ujs turbolinks
import Rails from 'rails-ujs'
Rails.start()
import Turbolinks from 'turbolinks'
Turbolinks.start()
- 下記のようなユーザモデルを scaffold で作成した直後の状態を想定
-
name
およびage
という属性を持つ。前者はstring
で後者はinteger
-
name
およびage
ともに入力必須 (バリデーションエラー時の挙動確認で必要なので)
-
$ ./bin/rails g scaffold User name age:integer
class User < ApplicationRecord
+ validates :name, :age, presence: true
end
- Rails のバージョンは 5.2.2
$ rails -v
5.2.2
実施
ざっくり言うと以下の流れに。
- save 成功時、リダイレクトが Ajax の場合でも動くようにする
- save 失敗時、 form 要素だけを html で返し、その結果により既存 form を入れ替える
_form
パーシャルファイル、 local: true
を外す
このポストの内容の前提なので。
-<%= form_with(model: user, local: true) do |form| %>
+<%= form_with(model: user) do |form| %>
<% if user.errors.any? %>
<div id="error_explanation">
<h2><%= pluralize(user.errors.count, "error") %> prohibited this user from being saved:</h2>
_form
パーシャルファイルが <form>
タグだけを描画する、という前提を変えない
通常、 scaffold ジェネレータを実施すると _form
パーシャルファイルは <form>
タグに該当する html を描画するわけですが、それ以外の要素を _form
パーシャルファイル内で描画しないようにします。
たとえば <form>
タグの直下に <a>
タグでリンクを置きたいからといって _form
パーシャルファイルの中にそれを記述してはいけません。同じことを実現するには _form
パーシャルファイルを呼び出す側に記述すればことが済むはずです。
例えば以下のようにするのではなく。。。
<%= render 'form', user: @user %>
<%= form_with(model: user) do |form| %>
...省略
<% end %>
<%= link_to 'Other Site', 'http://example.com' %>
_form
パーシャルでは <form>
だけ描画します。
<%= render 'form', user: @user %>
<%= link_to 'Other Site', 'http://example.com' %>
<%= form_with(model: user) do |form| %>
...省略
<% end %>
gem turbolinks を入れる
単に turbolinks を動かすだけなら JS の npm 管理下にある turbolinks を入れるだけで良いけど、 gem の turbolinks は別の理由で必要です。
gem turbolinks は Rails コントローラの redirect_to
の挙動をさしかえ、非Ajax の場合のリダイレクトと同じような動きをするようにします。
参考: https://github.com/turbolinks/turbolinks-classic/blob/master/lib/turbolinks/redirection.rb
create/update アクションのバリデーションエラー時に html 全体でなく _form
パーシャルの内容だけ返す
- render で
_form
パーシャルだけを描画して返すようにします。このときにlocals
を指定するのを忘れずに - 更に、ステータスコードは 200 でなく 422 Unprocessable Entity などのクライアントエラーを返すようにします
def create
@user = User.new(user_params)
if @user.save
redirect_to @user, notice: 'User was successfully created.'
else
- render :new
+ if request.xhr?
+ render partial: 'form', status: :unprocessable_entity, locals: { user: @user }
+ else
+ render :new
+ end
end
end
def update
if @user.update(user_params)
redirect_to @user, notice: 'User was successfully updated.'
else
- render :edit
+ if request.xhr?
+ render partial: 'form', status: :unprocessable_entity, locals: { user: @user }
+ else
+ render :edit
+ end
end
end
フォームで ajax:error
イベントを拾ったときに自身の内容を書き換える
import Rails from 'rails-ujs'
Rails.start()
import Turbolinks from 'turbolinks'
Turbolinks.start()
// ここから
document.addEventListener('turbolinks:load', (event) => {
const forms = document.querySelectorAll('form[data-remote="true"]') // remote フォームについて
forms.forEach((form) => {
form.addEventListener('ajax:error', (event) => { // 先のコントローラの処理で 200 を返しているとここは発火しないので注意
const detail = event.detail
const xhr = detail[2]
const contentType = xhr.getResponseHeader('content-type')
if (contentType === 'text/html; charset=utf-8') { // html が返ってきている場合
const target = event.currentTarget
const tmp = document.createElement('div')
tmp.innerHTML = xhr.responseText
const element = tmp.firstElementChild
target.innerHTML = element.innerHTML // <form> タグの innerHTML の中身を入れ替える
} // TODO: form タグ自体の属性についても厳密に丸々入れ替えるべきかもしれないが、ここではそこまでしていません
})
})
})
ここまで実施すると、 scaffold コードをベースにした Ajax CRUD が実現できているはずです。
良し悪し
- Pros.
-
_form
パーシャルに<form>
を書くというルールを前提とした場合に非常にシンプル - Javascript の記述がほとんど要らない
-
rake app:templates:copy
でコピーした controller と view のテンプレートファイルに少し手を入れる程度で、他にほとんど気にすることがない
-
- Cons.
- この実装は、バリデーションエラー時に
<form>
タグ以外の場所を更新できない- 実施する場合、たとえば
<form ... data-remote-placeholder="#form-wrapper">
のような属性があれば form でなくdocument.querySelector(form.dataset.remotePlaceholder)
が示す参照先をベースに書き換えする。みたいな拡張は可能。 - あるいは SJR でやるとか
- 実施する場合、たとえば
- この実装は、バリデーションエラー時に
まとめ
- scaffold はまだ Ajax 化されていない (今後されるの?)
- scaffold のコードをベースに Ajax 化してみた
- gem turbolinks を使うと Ajax/非Ajax を意識せずに
redirect_to
を使える - save 失敗時に form の html 要素を返し、それにより自身の form の中身を差し替える(と決めておくと簡単)
- gem turbolinks を使うと Ajax/非Ajax を意識せずに
補足
https://github.com/hamajyotan/scaffold_ajaxify
この記事での内容を実施したリポジトリの共有です。