Hotwire チュートリアルをする

Hotwire とは

hotwired.dev

Railsの作者 DHHが提唱する新しいSPAの形を実装したライブラリになります。

今まではサーバーサイドのデータをJSONのようなフォーマットでクライアントに渡し、クライアント側のJavascriptでHTMLをレンダリングすることでSPAを実現するといった流れでしたが、HotwireはサーバーサイドでHTMLをレンダリングし、必要な部分だけをクライアント側で置き換えることで実現されます。

これによって、レンダリングに関わるコードが Ruby で記述できるので、Ruby好きにはうれしいライブラリとなるかと思います。

チュートリアル

なぜかドキュメントの Get Started 的なものは見つからず、 hotwired.dev のトップページにあるデモ動画が唯一のチュートリアルとなるようです。

www.youtube.com

ただ、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-railsrails の最新に依存しているみたいだったので )
  • 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>

参考資料

qiita.com

qiita.com

qiita.com

qiita.com