【メモ】Hotwired Turboを採用した時にカスタマイズした2点


どうもみなさんこんにちは

Rails10年マンのせせりです

旧TurbolinksからTurboになってしばらく経ちましたがみなさん使ってますか?

古くは「まずapplication.jsを開いてTurbolinksという文字列を消しましょう(おまじない)」なんて言われていた彼ですが、2022年現在では(多分2018年くらいからは)使用していてturbo起因でバグを感じる事はほぼなくなっているので、利用しているjsプラグインとの兼ね合いが解決できるなら非常におすすめできるものとなっています(僕個人としてもturboをオフにすることは無いです)し、新規でRails開発(view含む)を行うなら利用した方が圧倒的にコスパが良いと個人的には思っております

さて、そんなTurboですがRails以外での利用を意識した結果本来あるべき機能がいくつかommitされておりそのままだと不便だと感じた2点を個人的にカスタマイズしているので共有してみます

1. flashメッセージがturbo frameから送れない

以前書いた記事

https://zenn.dev/sesere/articles/29b59a79ffdda8

これは多分全人類困ると思います。turbo frameの中でformを出して、それを更新させる~なんてよくやりますし「フォローボタンをturbo frameでpartial化する」みたいなやりがちな時にflashメッセージが表示できないと悲しいです

例えばこういうやつですね


1. turbo_frameでの通信時のみ( turbo_frame_request? || request.format.to_s.match?('turbo-stream') で確認可能 )flashメッセージをそのまま保持せずheaderに内包して返すようにする

/app/controllers/application_controller.rb
    # =========================================
    # turbo_frame用のflash
    # =========================================
    helper_method %i(
     is_turbo_frame_request?
    )
    def is_turbo_frame_request?
      return turbo_frame_request? || request.format.to_s.match?('turbo-stream')
    end
    after_action :flash_turbo_frame
    def flash_turbo_frame
      return if response.redirect?
      message = {}
      if turbo_frame_request? || request.format.to_s.match?('turbo-stream')
        message = flash.inject({}) do |hash, (type, _message)|
          # 日本語のメッセージをレスポンスヘッダに含めるために URL エンコードしておく。
          hash[type] = CGI.escape("#{ERB::Util.html_escape(_message)}")
          hash
        end.to_json
        
	flash.discard # flashの中身を全部消す / 消さないと次の遷移時まで残って変なタイミングでflashが出る
	
        # レスポンスがtubo frameかどうかをHeaderに入れる(フロントのjsでturbo frameか確認する為に使う)
        response.set_header(
          'X-Turbo-Connect',
          true
        )
      end
      
      # flash messageをheaderに突っ込む
      response.set_header(
        'X-Flash-Messages',
        message
      )
    end

大体こんな感じです。ポイントはafter_actionを使っている所で、これにより全てのactionの後に自動で差し込むことができます

2. 通常のflashの出力をturbo frameの時は行わないようにする

/app/views/layouts/application.html.erb
// flashメッセージを出力する
<% if !is_turbo_frame_request? %>
  <% %i(success notice error).each do |_type| %>
    <% next if flash[_type].blank? %>
    <script>
      alert("<%= _type %> -> <%= j flash[_type] %>")
    </script>
  <% end %>
<% end %>  

これはシンプルにifでturbo_frameの時以外だけ表示する~と囲っただけですね。ここで表示してしまうと、after_actionでflashを取り出そうとした時に既に利用されたflashなので中身が消えてしまうだか、jsとの兼ね合いで何かダメだったか忘れましたが当時の僕が上手く動かなかった記憶があるのでturbo frameの時は表示しないという形にしています

なんでわざわざhelperを経由して is_turbo_frame_request? にしているのかというと、erb内からはturbo_frame_request?が使えないからです

3. headerにflashメッセージが含まれていたら表示する

jsファイルを作って書いてください
document.addEventListener('turbo:before-fetch-response', (event) => {
  var json = JSON.parse(
    event.detail.fetchResponse.header('X-Flash-Messages')
  )
  // メッセージを表示する
  for(const key in json){
    alert(key + ':' + decodeURI(json[key]))
  }
})

これもシンプルですね、turbo:before-fetch-response(loadではないので注意)でheaderを確認して先程書いたflashが入っていたら表示する(ここでは alert(key + ':' + decodeURI(json[key])))だけです

実際にはtoastを使って表示していますので、みなさんの使いたいライブラリに書き換えてご利用ください

https://www.sukerou.com/2018/09/javascripttoastr.html

2. turbo frameからリダイレクトしたい

turbo frame内でformの保存ができたので親ごとリダイレクトしたい~みたいなシチュエーションもよくあると思います

そんな時に普通にredirecを書くと、turbo frame内しかredirectされません

個人的にはあまりそういった挙動は求めないと思う(redirectするならページ全部をしたい)と思うので、turbo frame内でのredirectを親全体に反映するようにしています

さっき書いたやつ
document.addEventListener('turbo:before-fetch-response', (event) => {
  var json = JSON.parse(
    event.detail.fetchResponse.header('X-Flash-Messages')
  )
  // メッセージを表示する
  for(const key in json){
    alert(key + ':' + decodeURI(json[key]))
  }
  
  // ---------------
  // 追加
  // ---------------
  // turbo frameから リダイレクトを望まれていたらリダイレクトする
  if(event.detail.fetchResponse.header('X-Turbo-Connect') && typeof (event.detail.fetchResponse) !== 'undefined') {
    var response = event.detail.fetchResponse.response
    if (response.redirected) {
      console.log('need redirect')
      event.preventDefault()
      Turbo.visit(response.url, {action: 'replace'})
    }
  }
})

header('X-Turbo-Connect') は application_controllerで追加していたアレです。これをつけることでturbo frame内のリクエストなのかを確認します
それ以外は多分なんとなく分かると思います。注意点として {action: 'replace'}になっている部分ですが、ここにきた時点でページの移動が行われるというアクションが発生しているためこれをしないと2重に遷移が行われるパターンがある(と思います)あんまり深く考えてないので思った挙動しなかったら適当に変えてください

その他

turboめちゃくちゃ便利なのでみなさんもカスタマイズしている点とかあったらぜひ教えて下さい!