🍐

HotwireとRailsで極めてシンプルなTODOアプリを作成する

4 min read

前提

  • Rails 7.0.0.alpha2

書いてあること

Hotwireで極めてシンプルなTODOアプリを作る方法をステップバイステップで説明
なお、今回はTurbo FramesとTurbo Streamsを使っているものの、ActionCableは使っていない
また、Hotwireと言っておきながら、Stimulusも使っていない

ステップ0

ルーティング、モデル、マイグレーションを作成

config/routes.rb

Rails.application.routes.draw do
  resources :todos
end

app/models/todo.rb

class Todo < ApplicationRecord
  validates :title, presence: true
end

db/migrate/xxx_create_todos.rb

class CreateTodos < ActiveRecord::Migration[7.0]
  def change
    create_table :todos do |t|
      t.string :title

      t.timestamps
    end
  end
end

ステップ1 一覧

まずは一覧を実装

ソースコード

app/controllers/todos_controller.rb

class TodosController < ApplicationController
  def index
    @todos = Todo.all
  end
end

app/views/todos/index.html.erb

<div id='todos'>
  <%= render @todos %>
</div>

app/views/todos/_todo.html.erb

<%= turbo_frame_tag todo do %>
  <div>
    <p><%= todo.title %></p>
    <%= link_to 'Edit', edit_todo_path(todo) %>
    <%= button_to 'Destroy', todo_path(todo), method: :delete %>
  </div>
<% end %>

ポイント

各TODOを表示するパーシャルはTurbo Streamで差し替える想定のため、turbo_frame_tagで囲う。

ステップ2 新規作成

新規作成を追加

ソースコード

app/controllers/todos_controller.rb

class TodosController < ApplicationController
  ()
 
  def new
    @todo = Todo.new
  end

  def create
    @todo = Todo.new(todo_params)
    if @todo.save
      render :create
    else
      render :new, status: :unprocessable_entity
    end
  end

  private
    def todo_params
      params.require(:todo).permit(:title)
    end
end

app/views/todos/index.html.erb

<div id='todos'>
  <%= render @todos %>
</div>

<%= turbo_frame_tag 'new_todo', src: new_todo_path do %>
<% end %>

app/views/todos/new.html.erb

<%= turbo_frame_tag 'new_todo' do %>
  <%= render "form", todo: @todo %>
<% end %>

app/views/todos/create.turbo_stream.erb

<%= turbo_stream.append 'todos' do %>
  <%= render @todo %>
<% end %>

<%= turbo_stream.replace 'new_todo' do %>
  <%= turbo_frame_tag 'new_todo', src: new_todo_path do %>
  <% end %>
<% end %>

app/views/todos/_form.html.erb

<%= form_with(model: todo) do |form| %>
  <% if todo.errors.any? %>
    <ul>
      <% todo.errors.each do |error| %>
        <li><%= error.full_message %></li>
      <% end %>
    </ul>
  <% end %>

  <%= form.text_field :title %>
  <%= form.submit %>
<% end %>

ポイント

turbo_frame_tagとsrcを使って一覧ページに新規作成フォームを追加している。
また、createアクション実行後にTurbo Streamを使ってturbo_frame_tagを2つ差し替えている。
1つ目はappendで新規作成したTODOをdiv#todosの末尾に追加しており、2つ目はreplaceでnew_todoをターゲットにして新規作成フォームをクリアしている。

ステップ3 更新

更新を追加

app/controllers/todos_controller.rb

class TodosController < ApplicationController
  ()

  def edit
    @todo = Todo.find(params[:id])
  end

  def update
    @todo = Todo.find(params[:id])
    if @todo.update(todo_params)
      render :update
    else
      render :edit, status: :unprocessable_entity
    end
  end
end

app/views/todos/edit.html.erb

<%= turbo_frame_tag @todo do %>
  <%= render 'form', todo: @todo %>
<% end %>

app/views/todos/update.turbo_stream.erb

<%= turbo_stream.replace @todo do %>
  <%= render 'todo', todo: @todo %>
<% end %>

ポイント

一覧画面でEditボタンを押したときにTODOのturbo_frame_tagが編集用フォームに差し替えられて、updateアクション実行後に該当のTODOをパーシャルで置き換えている。

ステップ4 削除

削除を追加

app/controllers/todos_controller.rb

class TodosController < ApplicationController
  ()

  def destroy
    @todo = Todo.find(params[:id])
    @todo.destroy
    render :destroy
  end
end

app/views/todos/destroy.turbo_stream.erb

<%= turbo_stream.remove @todo do %>
<% end %>

ポイント

一覧画面でDestoryボタンを押したときにdestroyアクションが実行されて、該当のTODOがremoveで削除される。

所感

Turbo Streamはxxx.turbo_stream.erbといったビューを使わなくてもコントローラに直接書けるが、わかりやすさ優先ですべてビューに書く方法で統一した。
(コントローラー側のアクションとビューファイルが1対1で対応しており、ビューのディレクトリだけを見てもどのアクションの結果がTurbo Streamなのか判別しやすいため)

今回のようなシンプルなCRUDアプリをTurbo縛りでいくつか実装してみるとHotwireの理解が進むように思う。

Discussion

ログインするとコメントできます