Railsのフォームのパターン

Railsアプリケーションで作るフォームは、入門書に載っているような書き方だけとは限りません。実際のアプリケーション開発では、入門書以外のパターンを使うことがあります。既存のアプリケーションに新しいフォームを加えるとき、既存のフォームを改修するときは、「どのパターンなのか」をはっきり意識するようにしましょう。

なお、ここでは作成・更新フォーム（POST、PATCHで送るフォーム）だけ取り上げます。検索フォームのようにGETで送るフォームは別の機会に。

フォームに関するその他の記事:

- [バリデーションのエラー表示をなるべく出さない](https://qiita.com/kazutosato/items/141d9880097fd51d7a37)
- [Rails+Vue.jsによるフォームの作例](https://qiita.com/kazutosato/items/38caffdbd21508a5c126)
- [フォームでdata-remote="true"を使うには](https://qiita.com/kazutosato/items/1661a249220e2870bed4)

## 1. Railsのデフォルトのフォーム（POSTで遷移）

入門書に載っているような、Railsのデフォルトのフォームは、form_withメソッドで作ります。Rails 6でも、以前からあるform_forメソッドやform_tagメソッドも使えます。

```erb
<%= form_with(model: user, local: true) do |form| %>
```

すると、POSTで送信する普通のフォームができます。

```html
<form action="/users" accept-charset="UTF-8" method="post">
```

### デフォルトのフォームの問題点

Railsでは、フォームの送信でバリデーションエラーが起きたときは、エラー画面をHTMLで返すのが伝統的な書き方です。

```ruby
  # 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](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/308595/018f1598-3e03-dd1d-4476-3d2e9ec0a8cd.png)
※ ChromeとSafariでは、POST /users のページでリロードすると、メッセージを出さずに GET /users を取りに行きます。つまり、エラー画面でリロードすると一覧画面や詳細画面になります。いつからこうなってたのかな? Safariでは、昔ながらのメッセージが出ます。

__2 エラー画面で戻るボタンを押したとき__

フォームのエラー画面でブラウザーの戻るボタンを押すと、送信前のフォームの画面になります。「前のページ」ではなく同じページが表示されるように見えるかもしれません。

1と2は、昔ながらのブラウザーの伝統的な挙動なのですが、「使いにくい」「動きがおかしい」と突っ込まれるかもしれません。

### Hotwire（Turbo）では

Rails 7に組み込まれる予定のHotwire（Turbo）を使うと、バリデーションエラーのときでもPOSTで遷移せずに、URLはそのままでエラー画面になります。Rails 7のデフォルトに従えば、上記の問題は解消しそうです。

参照: [Hotwire（Turbo）を試す その1: 導入、作成・更新フォーム](https://qiita.com/kazutosato/items/10a5bc04443d6b7e5bf8#%E4%BD%9C%E6%88%90%E6%9B%B4%E6%96%B0%E3%83%95%E3%82%A9%E3%83%BC%E3%83%A0) 


## 2. rails-ujsを使ったAjax送信

上記のデフォルトの問題点を避けるには、ページ全体を遷移させずにAjaxでリクエストを送信します。成功したらJavaScriptでリダイレクト、エラーが出たらJavaScriptでエラーを表示、とします。

Ajaxを使う方法はいろいろありますが、１つには、Rails用のJavaScriptライブラリ rails-ujs を使う方法があります。form_withメソッドにオプション`local: false`を付けます。

```erb
<%= form_with(model: @entry, local: false, id: 'entry-form') do |form| %>
```

すると、formに属性`data-remote="true"`が付きます。

```html
<form id="entry-form" action="/entries" accept-charset="UTF-8" data-remote="true" method="post">
```

コントローラのcreate、updateではJSONを返すようにします。

```ruby
  # 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のイベント処理でも同様です）。

```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"を使うには](https://qiita.com/kazutosato/items/1661a249220e2870bed4) をご覧ください。

rails-ujsを使ったAjaxの問題点は、次のようになるでしょうか。

- ドキュメント（本、ウェブ）が少なく、学習しにくい。
- jQueryやAxiosを使ってもあまり手間が変わらないかも。
- コードにいろんなAjaxの方法が混じっていると混乱する。

## 3. Railsの機能を使わない場合

Railsの機能にこだわらずに、Ajaxを使うのもありです。

### 3.1 フォームを使ったAjax送信

JavaScriptでformのsubmitイベントを処理し、フォームの送信を抑止して、入力欄からデータを集め、Ajaxで送信する、という方法です。次の例ではjQueryとAxiosを使っています。

処理するイベントは、formのsubmitイベントです。送信ボタンのclickイベントだと、入力欄のenterキーでの送信を処理できません。

```js
$('#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対策用のトークンを使う設定](https://qiita.com/kazutosato/items/0ce670272489cb647fb3)を参照してください。rails-ujsを使う場合は、トークンは自動的に送られます。

### 3.2 フォームを使わないAjax送信

上記のフォームを使ったAjax送信では、HTMLのフォームの機能が使えます。次のような機能です。

- 入力欄（`<input type="text">`など）でenterキーを押すとフォームが送信される。
- required、maxlength、pattern属性などでクライアント側のバリデーションが使える（不正な場合送信されない）。

enterキーでの送信は必要ないし、バリデーションもしない、というときは、フォームなしのAjaxを使うこともできます。次のようにformなしで入力欄を作り、

```html
名前: <input type="text" id="user-name"><br>
メールアドレス: <input type="email" id="user-email"><br>
<input type="button" value="送信" id="submit">
```

ボタンのclickイベントを処理してデータを送信します。

```js
$('#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を使ってもいいでしょう。

```js
$('#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を組み合わせる形になるんじゃないでしょうか。