Railsアプリケーションで作るフォームは、入門書に載っているような書き方だけとは限りません。実際のアプリケーション開発では、入門書以外のパターンを使うことがあります。既存のアプリケーションに新しいフォームを加えるとき、既存のフォームを改修するときは、「どのパターンなのか」をはっきり意識するようにしましょう。
なお、ここでは作成・更新フォーム(POST、PATCHで送るフォーム)だけ取り上げます。検索フォームのようにGETで送るフォームは別の機会に。
フォームに関するその他の記事:
1. Railsのデフォルトのフォーム(POSTで遷移)
入門書に載っているような、Railsのデフォルトのフォームは、form_withメソッドで作ります。Rails 6でも、以前からあるform_forメソッドやform_tagメソッドも使えます。
<%= form_with(model: user, local: true) do |form| %>
すると、POSTで送信する普通のフォームができます。
<form action="/users" accept-charset="UTF-8" method="post">
デフォルトのフォームの問題点
Railsでは、フォームの送信でバリデーションエラーが起きたときは、エラー画面をHTMLで返すのが伝統的な書き方です。
# POST /users
def create
@user = User.new(user_params)
if @user.save
redirect_to @user, notice: "作成に失敗しました。" # 成功時はリダイレクト
else
render :new, status: :unprocessable_entity # 失敗したらHTMLを返す
end
end
この書き方に従って実際のアプリケーションを開発していると、微妙に困ったことが起きます。
1 エラー画面でリロードしたとき
POSTのリクエストに対してHTMLで返したページでは、ブラウザーをリロードすると、「フォームを再送信しますか」というようなメッセージが出ます。これはユーザーを戸惑わせることになります。
※ ChromeとSafariでは、POST /users のページでリロードすると、メッセージを出さずに GET /users を取りに行きます。つまり、エラー画面でリロードすると一覧画面や詳細画面になります。いつからこうなってたのかな? Safariでは、昔ながらのメッセージが出ます。
2 エラー画面で戻るボタンを押したとき
フォームのエラー画面でブラウザーの戻るボタンを押すと、送信前のフォームの画面になります。「前のページ」ではなく同じページが表示されるように見えるかもしれません。
1と2は、昔ながらのブラウザーの伝統的な挙動なのですが、「使いにくい」「動きがおかしい」と突っ込まれるかもしれません。
Hotwire(Turbo)では
Rails 7に組み込まれる予定のHotwire(Turbo)を使うと、バリデーションエラーのときでもPOSTで遷移せずに、URLはそのままでエラー画面になります。Rails 7のデフォルトに従えば、上記の問題は解消しそうです。
参照: Hotwire(Turbo)を試す その1: 導入、作成・更新フォーム
2. rails-ujsを使ったAjax送信
上記のデフォルトの問題点を避けるには、ページ全体を遷移させずにAjaxでリクエストを送信します。成功したらJavaScriptでリダイレクト、エラーが出たらJavaScriptでエラーを表示、とします。
Ajaxを使う方法はいろいろありますが、1つには、Rails用のJavaScriptライブラリ rails-ujs を使う方法があります。form_withメソッドにオプションlocal: false
を付けます。
<%= form_with(model: @entry, local: false, id: 'entry-form') do |form| %>
すると、formに属性data-remote="true"
が付きます。
<form id="entry-form" action="/entries" accept-charset="UTF-8" data-remote="true" method="post">
コントローラのcreate、updateではJSONを返すようにします。
# POST /users
def create
@user = User.new(user_params)
if @user.save
flash.notice = "作成しました。"
render json: { location: user_path(@user) }, status: :created
else
render json: { errors: @user.errors.full_messages },
status: :unprocessable_entity
end
end
フォームに data-remote="true"
があると、rails-ujsによって送信時にイベント ajax:success や ajax:error が発生するようになるので、JavaScriptでこれを処理します(ここでは、jQueryを使っていますが、バニラJSのaddEventListenerでもVue.jsのイベント処理でも同様です)。
$('#user-form').on('ajax:success', (evt) => {
let data = evt.detail[0];
Turbolinks.visit(data.location);
}).on('ajax:error', (evt) => {
if(evt.detail[2].status == 422) {
let data = evt.detail[0];
// エラー表示
}
});
data-remote="true" を使ったフォームについて詳しくは、別記事 Rails: フォームでdata-remote="true"を使うには をご覧ください。
rails-ujsを使ったAjaxの問題点は、次のようになるでしょうか。
- ドキュメント(本、ウェブ)が少なく、学習しにくい。
- jQueryやAxiosを使ってもあまり手間が変わらないかも。
- コードにいろんなAjaxの方法が混じっていると混乱する。
3. Railsの機能を使わない場合
Railsの機能にこだわらずに、Ajaxを使うのもありです。
3.1 フォームを使ったAjax送信
JavaScriptでformのsubmitイベントを処理し、フォームの送信を抑止して、入力欄からデータを集め、Ajaxで送信する、という方法です。次の例ではjQueryとAxiosを使っています。
処理するイベントは、formのsubmitイベントです。送信ボタンのclickイベントだと、入力欄のenterキーでの送信を処理できません。
$('#user-form').on('submit', (evt) => {
evt.preventDefault(); // フォームの送信を抑止
let data = { user: { name: $('#user_name').val(), email: $('#user_email').val() } };
Axios.post('/users.json', data)
.then((response) => {
location.href = response.data.location;
})
.catch((error) => {
let data = error.response.data;
console.log(error.response.data);
// エラーを表示
});
});
送信するデータは、user[name]=Taro&user[email]=taro@example.com
のようなRailsらしいパラメータにする必要があります。JSONで送るときは { user: { name: "Taro", emal: "taro@example.com" }}
のような入れ子のオブジェクトを作ります。
また、Railsの機能を使わずにAjaxを使うときは、CSRF対策用のトークンも送る必要があります。Rails+AxiosでCSRF対策用のトークンを使う設定を参照してください。rails-ujsを使う場合は、トークンは自動的に送られます。
3.2 フォームを使わないAjax送信
上記のフォームを使ったAjax送信では、HTMLのフォームの機能が使えます。次のような機能です。
- 入力欄(
<input type="text">
など)でenterキーを押すとフォームが送信される。 - required、maxlength、pattern属性などでクライアント側のバリデーションが使える(不正な場合送信されない)。
enterキーでの送信は必要ないし、バリデーションもしない、というときは、フォームなしのAjaxを使うこともできます。次のようにformなしで入力欄を作り、
名前: <input type="text" id="user-name"><br>
メールアドレス: <input type="email" id="user-email"><br>
<input type="button" value="送信" id="submit">
ボタンのclickイベントを処理してデータを送信します。
$('#submit').on('click', (evt) => {
let data = { user: { name: $('#user-name').val(), email: $('#user-email').val() } };
Axios.post('/users.json', data)
.then((response) => {
location.href = response.data.location;
})
.catch((error) => {
let data = error.response.data;
// エラーを表示
});
});
また、入力欄なしで、ボタンを押すだけの機能(「削除」や「いいね」)を作るときも、フォームなしのAjaxを使ってもいいでしょう。
$('#delete').on('click', (evt) => {
Axios.delete(`/users/${user_id}.json`)
.then((response) => {
location.href = response.data.location;
});
});
今後の展望
今後のRailsフォームの書き方は、Rails 7に組み込まれるHotwire(Turbo)次第です。ややこしいJavaScriptを書くのはやめてTurboに全部任せる、とはならないと思います。たぶん、Turboを使いつつ自分で書くAjaxを組み合わせる形になるんじゃないでしょうか。