Hotwire とは
Railsの作者 DHHが提唱する新しいSPAの形を実装したライブラリになります。
今まではサーバーサイドのデータをJSONのようなフォーマットでクライアントに渡し、クライアント側のJavascriptでHTMLをレンダリングすることでSPAを実現するといった流れでしたが、HotwireはサーバーサイドでHTMLをレンダリングし、必要な部分だけをクライアント側で置き換えることで実現されます。
これによって、レンダリングに関わるコードが Ruby で記述できるので、Ruby好きにはうれしいライブラリとなるかと思います。
チュートリアル
なぜかドキュメントの Get Started 的なものは見つからず、 hotwired.dev
のトップページにあるデモ動画が唯一のチュートリアルとなるようです。
ただ、8/13 現在、この通りにやってもうまくいかないのでその部分を交えつつチュートリアルをやっていこうと思います。
Ruby バージョン
ruby 3.0.2p107 (2021-07-07 revision 0db68f0233) [x86_64-linux]
※ 今回のチュートリアルでは内部で Redis を使いますので、Redisのインストールを行ってからチュートリアルをスタートしてください。
Rails アプリの作成
$ rails new chat --skip-javascript $ cd chat
Gemfile の編集
rails
のバージョンを githubのマスターにします。( 現状はimportmap-rails
がrails
の最新に依存しているみたいだったので )gem 'importmap-rails'
を足しますgem 'hotwire-rails'
を足します
gem 'rails', :github => 'rails/rails' # ← ここは編集 gem 'importmap-rails' # ← ここは追加 gem 'hotwire-rails' # ← ここは追加
各種 gem のインストール
$ bundle update $ bundle install $ rails importmap:install $ rails hotwire:install
ベースのアプリを作成
$ rails g scaffold room name:string $ rails g model message room:references content:text $ rails db:migrate $ bundle install
各種ファイルの編集・作成
config/routes.rb 【編集】
Rails.application.routes.draw do resources :rooms do resources :messages end end
app/models/room.rb 【編集】
class Room < ApplicationRecord has_many :messages end
app/controllers/messages_controller.rb 【作成】
class MessagesController < ApplicationController before_action :set_room, only: %i[ new create ] def new @message = @room.messages.new end def create @message = @room.messages.create!(message_params) redirect_to @room end private def set_room @room = Room.find(params[:room_id]) end def message_params params.require(:message).permit(:content) end end
app/views/messages/new.html.erb 【作成】
<h1>New Message</h1> <%= turbo_frame_tag "new_message", target: "_top" do %> <%= form_with(model: [ @message.room, @message ]) do |form| %> <div class="field"> <%= form.text_field :content %> <%= form.submit "Send" %> </div> <% end %> <% end %> <%= link_to 'Back', @message.room %>
app/views/messages/_message.html.erb 【作成】
<p id="<%= dom_id message %>"> <%= message.created_at.to_s(:short) %>: <%= message.content %> </p>
app/views/rooms/show.html.erb 【編集】
<p id="notice"><%= notice %></p> <%= turbo_frame_tag "room" do %> <p> <strong>Name:</strong> <%= @room.name %> </p> <%= link_to 'Edit', edit_room_path(@room) %> | <%= link_to 'Back', rooms_path, "data-turbo-frame": "_top" %> <% end %> <div id="messages"> <%= render @room.messages %> </div> <%= turbo_frame_tag "new_message", src: new_room_message_path(@room), target: "_top" %>
app/views/rooms/edit.html.erb 【編集】
<h1>Editing Room</h1> <%= turbo_frame_tag "room" do %> <%= render 'form', room: @room %> <% end %> <%= link_to 'Show', @room %> | <%= link_to 'Back', rooms_path %>
app/asserts/stylesheets/application.css
/* * This is a manifest file that'll be compiled into application.css, which will include all the files * listed below. * * Any CSS and SCSS file within this directory, lib/assets/stylesheets, or any plugin's * vendor/assets/stylesheets directory can be referenced here using a relative path. * * You're free to add application-wide styles to this file and they'll appear at the bottom of the * compiled file so the styles you add here take precedence over styles defined in any other CSS/SCSS * files in this directory. Styles in this file should be added after the last require_* statement. * It is generally better to create a new file per style scope. * *= require_tree . *= require_self */ turbo-frame { display: block; border: 1px solid blue; }
※ ここまでで rails を実行すると、Roomを作成して編集するときに画面遷移なしに編集ボックスが出るようになり、メッセージも遷移なしに追加されます。
turbo_stream フォーマットの追加
app/controllers/messages_controller.rb 【編集】
class MessagesController < ApplicationController # ... 省略 def create @message = @room.messages.create!(message_params) respond_to do |format| format.turbo_stream format.html { redirect_to @room } end end # ... 省略 end
app/views/messages/create.turbo_streaam.erb 【作成】
<%= turbo_stream.append "messages", @message %>
メッセージの相互通信の実現
app/assets/javascripts/controllers/reset_form_controller.js 【作成】
import { Controller } from "@hotwired/stimulus" export default class extends Controller { reset() { this.element.reset() } }
app/views/messages/new.html.erb 【編集】
<h1>New Message</h1> <%= turbo_frame_tag "new_message", target: "_top" do %> <%= form_with(model: [ @message.room, @message ], data: { controller: "reset_form", action: "turbo:submit-end->reset_form#reset"}) do |form| %> <div class="field"> <%= form.text_field :content %> <%= form.submit "Send" %> </div> <% end %> <% end %> <%= link_to 'Back', @message.room %>
app/views/rooms/show.html.erb 【編集】
<p id="notice"><%= notice %></p> <%= turbo_stream_from @room %> <% #省略 %>
app/models/message.rb 【編集】
class Message < ApplicationRecord belongs_to :room broadcasts_to :room end
app/views/messages/create.turbo_streaam.erb 【編集】
<% # Return handled by cable %>
ルーム名のリアルタイム同期の実装
app/models/room.rb 【編集】
class Room < ApplicationRecord has_many :messages broadcasts end
app/views/rooms/show.html.erb 【編集】
<p id="notice"><%= notice %></p> <%= turbo_stream_from @room %> <%= turbo_frame_tag "room" do %> <%= render @room %> <% #省略 %>
app/views/rooms/_room.html.erb 【編集】
<p id="<%= dom_id room %>"> <strong>Name:</strong> <%= room.name %> </p>