Hotwire: Stimulus と Turbo Streams の組み合わせで定期的にページの特定箇所を更新する

前回作成した習作アプリはページをリロードするとスクロール位置が最上部に戻ってしまいます。
今回は Turbo Streams を用いてページの特定箇所の更新を行いたいと思います。

Hotwire v7.0.0-rc.1

要求
複数箇所の要素の文字列を更新する。
1秒間隔で更新する。更新を3回行う。
更新時にスクロール位置を変えない。
対象の文字列をページの上下に用意する。

画面

画像1

仕様
Stimulus コントローラーが定期的に更新処理を行う。
要更新フラグが false なら更新をやめる。
習作アプリなのでフラグは確認しやすいようにページに表示する。
Turbo Streams の MIME タイプで get 'home/status' する。
status アクションは文字列置換用のメッセージを返す。
返されたメッセージを Turbo.renderStreamMessage で処理する。
(参考)Is there a way to programmatically invoke a Turbo Stream? · Issue #34 · hotwired-turbo

前回との違い
Turbo Streams には属性だけを置き換える方法がない。
属性を含む要素を置き換えると Stimulus コントローラーが初期化される。
このため今回のアプリは 要更新フラグを別の要素の文字列に持たせる。
今回のアプリは Stimulus の Change Callback 機能は使用しない。
Stimulus コントローラーは生き続けるため setInterval で更新を繰り返す。

追加または変更するファイル
app/assets/javascripts/controllers/reload_controller.js
app/controllers/home_controller.rb
app/views/home/index.html.erb
app/views/home/status.turbo_stream.erb
config/routes.rb

rails new hotwire_update_elements \
--skip-action-mailbox \
--skip-action-text \
--skip-skip-active-storage \
--skip-action-cable \
--skip-javascript \
--skip-turbolinks \
--skip-jbuilder \
--skip-test \
&& cd hotwire_update_elements && tmux

bin/bundle add hotwire-rails
bin/rails hotwire:install

bin/rails g controller home index


# config/routes.rb
'home/status'を追加する。

Rails.application.routes.draw do
  get 'home/index'
  get 'home/status'
  # For details on the DSL available within this file, see https://guides.rubyonrails.org/routing.html
end

# app/controllers/home_controller.rb
更新を3回行う。
Stimulus コントローラーは @reload がtrueなら更新、falseなら更新しない。

class HomeController < ApplicationController
  RELOAD_MAX = 3

  def index
    @@reload_count = 1
  end

  def status
    @textContent = "#{@@reload_count}/#{RELOAD_MAX}, #{Time.now}"

    @@reload_count += 1
    @reload = (@@reload_count <= RELOAD_MAX)
  end
end

// app/assets/javascripts/controllers/reload_controller.js
canReload() が true なら、1秒周期でメッセージを取得する。
fetch に 'text/vnd.turbo-stream.html' を設定して Stream を受け取る。

import { Controller } from "stimulus"

export default class extends Controller {
  static values = { url: String }

  initialize() {
    this.intervalId = 0
  }

  connect() {
    this.intervalId = setInterval(() => {
      if (this.canReload()) {
        this.updateElements()
      } else {
        this.stopReloading()
      }
    }, 1000);
  }

  disconnect() {
    this.stopReloading()
  }

  updateElements() {
    fetch(this.urlValue, { headers: { 'Accept': 'text/vnd.turbo-stream.html' } })
      .then(response => response.text())
      .then(message => Turbo.renderStreamMessage(message))
      .catch (() => this.stopReloading())
  }

  canReload() {
    const reload = document.getElementById('reload').textContent
    return (reload === 'true')
  }

  stopReloading() {
    if (this.intervalId !== 0) {
      clearInterval(this.intervalId)
      this.intervalId = 0
    }
  }
}

<!-- app/views/home/index.html.erb -->

<h1>Home#index</h1>
<p>Find me in app/views/home/index.html.erb</p>

<hr>

<div id="status-1">Status1</div>

<hr>

<% 20.times do %>
  <p>paragraph</p>
<% end %>

<hr>

<div id="status-2">Status2</div>

<hr>

<div id="reload">true</div>

<div data-controller="reload"
     data-reload-url-value="/home/status">
  Reload Div
</div>

<!-- app/views/home/status.turbo_stream.erb -->

<%= turbo_stream.update "status-1", @textContent %>

<%= turbo_stream.update "status-2", @textContent %>

<%= turbo_stream.update "reload", @reload.to_s %>

以上です。

追記:Stream で update する要素(ID)を動的に生成する

Update 対象の ID を @reload_ids に設定して、テンプレートで<turbo-stream>タグを動的に生成する。

  def status
    @reload_ids = ["1", "2"]
    @textContent = "#{@@reload_count}/#{RELOAD_MAX}, #{Time.now}"

    @@reload_count += 1
    @reload = (@@reload_count <= RELOAD_MAX)
  end
<% @reload_ids.each do |id| %>
  <%= turbo_stream.update "status-#{id}", @textContent %>
<% end %>
<%= turbo_stream.update "reload", @reload.to_s %>

以上です。

この記事が気に入ったらサポートをしてみませんか?