2
2

More than 1 year has passed since last update.

Railsのフォームのパターン

Last updated at Posted at 2021-11-17

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で返したページでは、ブラウザーをリロードすると、「フォームを再送信しますか」というようなメッセージが出ます。これはユーザーを戸惑わせることになります。
image.png
※ 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を組み合わせる形になるんじゃないでしょうか。

2
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
2