コメントの turbo frame 化: 小林研 Rails Tips (85)

このページの内容は以下のリポジトリに1日遅れで反映されます(記事執筆前に前日分をコミットしています)。

https://github.com/hkob/hkob_blog

はじめに

Rails Tips の 85 回目です。コメントの追加・編集削除は articles#show の中に閉じています。この部分はページ遷移が必要ないので、TurboFrame 化してしまいましょう。

Rails をはじめよう - Railsガイド

articles/show の修正

実際にarticles/show.html.haml の Comments 以下を turbo_frame_tag の中に入れるだけです。実行すると確かに Turbo frame ないのみが更新されました。

- content_for :title do
  - @page_title = @article.title

%p= @article.body

- if @article.owned_by? current_user
  %ul
    %li= lotfp edit_article_path(@article)
    %li= destroy_lotfp article_path(@article), Article

= turbo_frame_tag "comments" do
  %h2 Comments
  = render partial: "articles/comments/comment", collection: @comments

  - if user_signed_in? && @comment.new_record?
    %h2 Add a comment:
    = render "articles/comments/form"

turbo frame での flash 対応

TurboFrame は非常に便利なのですが、一箇所しか書き替えできないので、Flash メッセージは更新できません。Flash メッセージのためだけに Turbo Stream を使うのはもったいないので、TurboFrame でなんとかしたいと思っていました。調べていたら以下のようなページを見つけ、校務支援システムでも導入しています。

zenn.dev

まず、application_controller.rbflash_turbo_frame メソッドを作成し、after_action に登録します。これは、turbo_frame のリクエストの時だけ、X-Flash-Messages ヘッダに flash 情報を登録するものになります。

class ApplicationController < ActionController::Base
  before_action :configure_permitted_parameters, if: :devise_controller?
  after_action :flash_turbo_frame

  def flash_turbo_frame
    return if response.redirect?

    message = {}
    if turbo_frame_request?
      message = flash.inject({}) do |hash, (type, _message)|
        # XSS対策&日本語のエスケープ
        hash[type] = CGI.escape("#{ERB::Util.html_escape(_message)}")
        hash
      end.to_json
      flash.discard
    end
    response.set_header('X-Flash-Messages', message)
  end

importmaps の設定

上のページを参考に X-Flash-Messages に登録されていた flash 情報を notice, alert の ID の場所に書き換える処理を行います。この部分は自分で作ってみました。app/javascripts/flash_messages.js として保存しました。

document.addEventListener('turbo:before-fetch-response', function(event){
    const json = JSON.parse(event.detail.fetchResponse.header("X-Flash-Messages"))
    let keys = ["notice", "alert"]

    keys.forEach(key => {
        let value = json[key]
        let element = document.getElementById(key)
        if (value) {
            element.innerHTML = decodeURI(value)
            element.classList.add(key)
        } else {
            element.innerHTML = ""
            element.classList.remove(key)
        }
    })
})

flash_messages.js を config/importmap.rb に pin 登録しました。

pin "flash_messages", to: "flash_messages.js", preload: true

この処理は全てのページで動作して欲しいので、 application.html.haml の最初に module 読み込みを行いました。

    = javascript_importmap_tags
    = stylesheet_link_tag "application", "data-turbo-track": "reload"
    = javascript_import_module_tag "flash_messages"

おわりに

これで show の中にあるコメント追加・編集・削除は TurboFrame によりページ遷移せずに該当部分のみ描画されるようになりました。また、Flash を無理矢理書き換える処理も追加してみました。無事に動いているようです。