Rails: フォームでdata-remote="true"を使うには

フォームに`data-remote="true"`を付けたときの挙動について、ちゃんと理解していなかったので、ここにメモします。

ここらへんの機能はRails 7で[Hotwire（Trubo）](https://qiita.com/kazutosato/items/10a5bc04443d6b7e5bf8)で置き換わるようなので、いずれ無用な知識になるかもしれません。

## form_withとform_forのオプション

form_withにオプション`local: false`を付けます。すると、HTMLのformに`data-remote="true"`が付きます。

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

```html
<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です。

```erb
<%= form_for(@entry, remote: true, html: { id: 'entry-form' }) do |form| %>
```

## data-remote="true"で送信し、リダイレクションを返した場合

コントローラで成功時にリダイレクションを返すとします。

```ruby
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を実行してページ遷移してしまいます。

```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データで返します。失敗時はエラーメッセージの配列を返します。

```ruby
  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`を処理して、リダイレクトしたりエラーを表示したりします。

```javascript
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で処理するものです。

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

```javascript
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
      }
    }
  }
});
```

以上です。