フォームにdata-remote="true"
を付けたときの挙動について、ちゃんと理解していなかったので、ここにメモします。
ここらへんの機能はRails 7でHotwire(Trubo)で置き換わるようなので、いずれ無用な知識になるかもしれません。
form_withとform_forのオプション
form_withにオプションlocal: false
を付けます。すると、HTMLのformにdata-remote="true"
が付きます。
<%= form_with(model: @entry, local: false, id: 'entry-form') do |form| %>
<form id="entry-form" action="/entries" accept-charset="UTF-8" data-remote="true" method="post">
Rails 6.0以前では、デフォルトでdata-remote="true"
が付いてしまい、これを防ぐには local: true
を付ける必要がありました。この仕様は混乱の元だったと思います。Rails 6.1ではdata-remote="true"
なしがデフォルトになりました。
なお、form_forに指定するときはremote: true
です。デフォルトは、6.0も6.1もfalseです。
<%= form_for(@entry, remote: true, html: { id: 'entry-form' }) do |form| %>
data-remote="true"で送信し、リダイレクションを返した場合
コントローラで成功時にリダイレクションを返すとします。
def create
@entry = Entry.new(entry_params)
if @entry.save
redirect_to @entry, notice: "保存しました。"
else
render :new, status: :unprocessable_entity
end
end
data-remote="true"
のときは、次のレスポンスが返ります。HTTPステータスは200、Content-Typeはtext/javascriptです。このレスポンスを受け取ると、rails-ujsがJavaScriptを実行します(Turbolinks.visitでページ遷移します)。
Turbolinks.clearCache()
Turbolinks.visit("http://localhost:3000/entries/13", {"action":"replace"})
data-remote="true"
+リダイレクションのときは、次の流れになります。
-
data-remote="true"
のフォームを送信すると、rails-ujsはリクエストヘッダに「Accept: text/javascript, application/javascript, ...」を付けてAjaxで送信。 - このとき、Railsでredirect_toを実行すると、ステータス302ではなく200で、上記のJavaScriptを送信する。
- rails-ujsは、「Content-Type: text/javascript」のレスポンスを受け取ると、そのJavaScriptを実行する(RJS)。
困る点1: 失敗時の処理が必要
上記のコントローラでは、保存に失敗したときはHTMLを返しています。rails-ujsは、「text/javascript」以外が返ったときは自動的に何かしてくれるわけではありません。
困る点2: ajax:successイベントを処理しても遷移する
上記のサンプルでは、次のようにajax:successイベントを処理しても、rails-ujsがレスポンスのJavaScriptを実行してページ遷移してしまいます。
let form = document.getElementById('entry-form');
if(form) {
form.addEventListener('ajax:success', (evt) => {
// 独自の処理...
});
}
実際にフォームでdata-remote="true"を使うには
次の例は、私がフォームでdata-remote="true"
を使うときのやり方です(これが正解というわけではありません)。
コントローラでは、createとupdateはAjax専用ということにして、JSONだけを返します。RJS(レスポンスのJavaScriptを自動実行)は避けます。
成功時はredirect_toを使わずに、遷移先のパスをJSONデータで返します。失敗時はエラーメッセージの配列を返します。
def create
@entry = Entry.new(entry_params)
if @entry.save
flash.notice = "作成しました。"
render json: { location: entry_path(@entry) }, status: :created
else
render json: { errors: @entry.errors.full_messages },
status: :unprocessable_entity
end
end
def update
@entry.assign_attributes(entry_params)
if @entry.save
flash.notice = "更新しました。"
render json: { location: entry_path(@entry) }, status: :ok
else
render json: { errors: @entry.errors.full_messages },
status: :unprocessable_entity
end
end
JavaScriptでは、rails-ujsのイベントajax:success
とajax:errors
を処理して、リダイレクトしたりエラーを表示したりします。
let form = document.getElementById('entry-form');
if(form) {
form.addEventListener('ajax:success', (evt) => {
let data = evt.detail[0];
Turbolinks.visit(data.location);
});
form.addEventListener('ajax:error', (evt) => {
if(evt.detail[2].status == 422) {
let data = evt.detail[0];
// data.errorsを使って何かする
}
});
}
次の例は、erbで出力したフォームをVue.jsで処理するものです。
<%= form_with(model: entry, local: false, id: 'entry-form',
'@ajax:success' => 'formAjaxSuccess',
'@ajax:error' => 'formAjaxError') do |form| %>
new Vue({
el: "#entry-form",
data: {
return {
errors: []
}
},
methods: {
formAjaxSuccess(e) {
let data = e.detail[0]
Turbolinks.visit(data.location)
},
formAjaxError(e) {
if(e.detail[2].status == 422) {
let data = e.detail[0]
this.errors = data.errors
}
}
}
});
以上です。