Hotwire を使って Rails 6.1 の scaffold を SPA にしてみた


## はじめに
DHH 兄貴が公開してくれた [Hotwire](https://hotwire.dev/) 、Websocket でリアルタイム更新が反映できて便利だなと思いましたが、ちょっと触ってみたら、 `get で画面遷移なしでHTMLを取得する時ってどうすればできるんだろう` と疑問が出てきたので、 `rails g scaffold` の画面を SPA にしてみました。

Hotwire の基本については、公式のビデオと [Hotwireでリアルタイムなチャットを作る](https://qiita.com/S_H_/items/9544ce6ded63230ad559) を参考すればイメージできると思います。

## 結論
できたもの
https://github.com/blueplanet/hotwire-scaffold

- 入力フォームを index に直接に表示することで、SPA にできました
  - ~~bootstrap を導入して入力フォームを modal でやりたかったんですが、HTML が取得できたものの、 `それを受けてから modal を表示する` ところがうまくできなかったので、諦めました~~
  - [turbo v7.0.0-beta.2](https://github.com/hotwired/turbo/releases?after=dev-builds%2F59e6d04) で異常系の status code の場合もレスポンスを描画するようになったので、`stimulus` と `turbo` を使って、 `成功したら modal をクローズ；エラーの場合そのまま` を実現できた

```
# controller にエラー時の `render :new` に `status: :unprocessable_entity` 追加
def create
  @post = Post.new(post_params)

  respond_to do |format|
    if @post.save
      format.html { redirect_to @post, notice: 'Post was successfully created.' }
      format.json { render :show, status: :created, location: @post }
    else
      format.html { render :new, status: :unprocessable_entity }
      format.json { render json: @post.errors, status: :unprocessable_entity }
    end
  end
end

# form に turbo:submit-end イベントに `modal#close` メソッドを紐づく
<%= form_with(model: post, data: { controller: "modal", action: 'turbo:submit-end->modal#close' }) do |form| %>

# modal の close アクションに `event.detail.formSubmission.result.success` で結果を判定できる
close(event) {
  if (event.detail.formSubmission.result.success) {
    $('#post-modal').modal('hide')
  }
}
```

- SPA 対応のポイント
    - `turbo_frame_tag 'post_form'` を活用すれば、ほぼ既存の view を変更せずにできる
    - `link_to` に `data: { turbo_frame: 'post_form' }` を付けることで、リンク先のレスポンスから、上記の `turbo_frame_tag` と同じ HTML id （`post_form`) の要素を入れ替えてくれる
    - https://github.com/blueplanet/hotwire-scaffold/blob/master/app/views/posts/index.html.erb

## 感想
- [hotwire-rails](https://github.com/hotwired/hotwire-rails) は、 [turbo-rails](https://github.com/hotwired/turbo-rails) と [stimulus-rails](https://github.com/hotwired/stimulus-rails) の組み合わせの設定をしてくれただけで、メインの仕事をしてくれているのは `turbo-rails`
- `turbo` は５種類の動き `append / prepend / replace / update / remove` だけ定義されていて、それ以外の動きは `stimulus` で頑張ろう的な感じ
- 更新系の処理をトリガーとして、view を更新する処理を broadcast すればよいですが、複数の処理が必要な場合、処理の順番をどう制限できるかは、まだ分かっていない
- `stimulus` は、まだ良くわかってない

### よかったところ
- `turbo_frame_tag` と `data: { turbo_frame: 'post_form' }` と組み合わせるだけで、既存の scaffold で生成した view を SPA にすることができた
- `turbo_stream_from` と `broadcast*` と組み合わせることで、修正系の結果をリアルタイムで反映できた

### 改善したいところ
- webpacker があると、 `turbo_frame_tag` の中にある submit ボタンが Form submit のレスポンスが 200 になっているにもかかわらず disabled のままになっている事象がある
    - よって、できたものの rails project は、 `--skip-javascript` で初期化しました
    - 調べきれてなかったんですが、`rails/ujs` との整合性がよくない感じ
- webpacker をなく、`rails/ujs` なくなり、`form_with ... remote: true` 系が使えなくなってしまうので、不便
- `ujs` がなくても、`You can still use the data-confirm and data-disable-with.` と [ドキュメントに書かれています](https://github.com/hotwired/turbo-rails#compatibility-with-rails-ujs) が、できたものでは、scaffold で生成した delete link は動きませんでした。

## まとめ
- `turbo を使えば、js を書かなくて済むよ` 的な感じがありますが、 `turbo ならでは` のやり方は、慣れる必要がある
- `ujs` や `webpacker` と併用できないため、既存プロジェクトへの導入はもうちょっと待つほうがよいかも

## 参考リンク
- [Hotwireでリアルタイムなチャットを作る](https://qiita.com/S_H_/items/9544ce6ded63230ad559)
- https://turbo.hotwire.dev/handbook/introduction