【Ruby on Rails】シンプルなTodoアプリを作ってみる

Ruby

こんにちは、かつコーチです。

これまでルーティング・ActiveRecord・バリデーション・アソシエーション・Service Objectと、Railsの要素を1つずつ解説してきました。

今回はその総まとめとして、シンプルなTodoアプリを実際に手を動かしながら作っていきます。

一覧表示・作成・完了・削除という基本のCRUD操作を、これまで学んだベストプラクティスを織り込みながら実装します。

実装手順

プロジェクトとモデルの作成

まずは新規プロジェクトを作成し、Todoモデルを生成します。

rails new todo_app
cd todo_app
rails generate model Todo title:string done:boolean

生成されたマイグレーションに、done列のデフォルト値とnull制約を追加しておきます。

# db/migrate/xxxxxx_create_todos.rb
class CreateTodos < ActiveRecord::Migration[8.0]
  def change
    create_table :todos do |t|
      t.string :title, null: false
      t.boolean :done, null: false, default: false

      t.timestamps
    end
  end
end
rails db:migrate

バリデーションとスコープの追加

モデルには、タイトルの必須チェックと、未完了/完了を絞り込むスコープを追加します。

# app/models/todo.rb
class Todo < ApplicationRecord
  validates :title, presence: true, length: { maximum: 100 }

  scope :pending, -> { where(done: false) }
  scope :completed, -> { where(done: true) }
  scope :recent_first, -> { order(created_at: :desc) }
end

length: { maximum: 100 }を入れておくことで、想定外に長いタイトルが登録されるのを防げます。

ルーティングとコントローラの実装

一覧・作成・完了切り替え・削除に必要なルーティングを定義します。

# config/routes.rb
Rails.application.routes.draw do
  root "todos#index"

  resources :todos, only: %i[index create destroy] do
    member do
      patch :toggle
    end
  end
end

resourcestoggleのような独自アクションを足す場合は、member do ... endブロックの中に書くのが定石です。

続いてコントローラを実装します。

# app/controllers/todos_controller.rb
class TodosController < ApplicationController
  def index
    @todo = Todo.new
    @todos = Todo.recent_first
  end

  def create
    @todo = Todo.new(todo_params)

    if @todo.save
      redirect_to root_path, notice: "Todoを追加しました"
    else
      @todos = Todo.recent_first
      render :index, status: :unprocessable_entity
    end
  end

  def toggle
    todo = Todo.find(params[:id])
    todo.update!(done: !todo.done)
    redirect_to root_path
  end

  def destroy
    Todo.find(params[:id]).destroy
    redirect_to root_path, notice: "Todoを削除しました"
  end

  private

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

createが失敗したときに@todosを再セットしてからrenderしているのがポイントです。

これを忘れると、バリデーションエラー時に一覧が空のまま表示されてしまいます。

ビューの実装

最後に、一覧表示と入力フォームを1画面にまとめたビューを作成します。

<%# app/views/todos/index.html.erb %>
<h1>Todoリスト</h1>

<%= form_with model: @todo, url: todos_path do |f| %>
  <% if @todo.errors.any? %>
    <ul class="errors">
      <% @todo.errors.full_messages.each do |message| %>
        <li><%= message %></li>
      <% end %>
    </ul>
  <% end %>

  <%= f.text_field :title, placeholder: "やることを入力" %>
  <%= f.submit "追加" %>
<% end %>

<ul>
  <% @todos.each do |todo| %>
    <li class="<%= 'done' if todo.done? %>">
      <%= todo.title %>

      <%= button_to (todo.done? ? "未完了に戻す" : "完了にする"),
            toggle_todo_path(todo), method: :patch %>

      <%= button_to "削除", todo_path(todo), method: :delete,
            data: { turbo_confirm: "本当に削除しますか?" } %>
    </li>
  <% end %>
</ul>

button_todata: { turbo_confirm: "..." }を指定すると、Rails 8標準のTurboが確認ダイアログを出してから削除リクエストを送ってくれます。

よくあるつまずきポイント・エラー対処

turbo_confirmが効かずに即削除されてしまう

私が実際にハマったのが、確認ダイアログなしで削除ボタンを押した瞬間に即削除されてしまった問題です。

❌ Before:古いRailsの書き方をそのまま使ってしまう

<%= button_to "削除", todo_path(todo), method: :delete,
      data: { confirm: "本当に削除しますか?" } %>

Rails 6以前はdata: { confirm: "..." }で確認ダイアログが出ていましたが、Rails 8ではデフォルトのJavaScriptライブラリがUJS(jquery-ujs/rails-ujs)からTurboに変わっています。

古いconfirm属性はTurboに認識されず、確認なしでそのまま削除リクエストが送信されてしまいました。

✅ After:Turbo対応のturbo_confirmを使う

<%= button_to "削除", todo_path(todo), method: :delete,
      data: { turbo_confirm: "本当に削除しますか?" } %>

data-turbo-confirm属性に変更するだけで、Turboが標準の確認ダイアログを表示してくれるようになりました。

Rails 8系のプロジェクトでボタンの確認ダイアログが効かない場合は、まずこの属性名の違いを疑うとよいです。

resources内のmemberルーティングでNoMethodError

もう1つ、独自アクションのルーティングでつまずいた経験があります。

❌ Before:memberブロックの外にpatchを書いてしまう

resources :todos, only: %i[index create destroy]
patch "toggle", to: "todos#toggle"

このルーティングでビューからtoggle_todo_path(todo)を呼び出したところ、次のエラーが出ました。

NoMethodError: undefined method `toggle_todo_path' for an instance of #<Class:...>

patch "toggle", to: "todos#toggle"のようにトップレベルで定義すると、toggle_pathというルーティングヘルパーにはなっても、個々のTodoに紐づくtoggle_todo_pathにはなりません。

✅ After:resourcesのmemberブロック内に定義する

resources :todos, only: %i[index create destroy] do
  member do
    patch :toggle
  end
end

memberブロックの中に書くことで、/todos/:id/toggleというURLと、IDを引数に取るtoggle_todo_path(todo)ヘルパーが自動生成されます。

個々のレコードに対する操作を追加する場合は、必ずmemberブロックを使うのが正解です。

まとめ

この記事のポイント

  • モデル・バリデーション・スコープ・ルーティング・コントローラ・ビューを組み合わせてシンプルなTodoアプリを実装した
  • create失敗時は@todosを再セットしてからrenderしないと一覧が空になる
  • Rails 8ではTurboが標準のため、確認ダイアログはdata-confirmではなくdata-turbo-confirmを使う
  • 個々のレコードに対する独自アクションは、resourcesmemberブロック内に定義する

次に読むべき記事

→ 次の記事:Ruby on RailsとNext.js、どちらを選ぶべきか比較検証

タグ: Ruby on Rails, 上級者向け, 実践プロジェクト

タイトルとURLをコピーしました