💭

RailsでTodoアプリを作る(その1)

に公開

RailsでTodoアプリを作ろうと思いましたので、その過程をここに記す。

このTodoアプリは、以下のような機能を持つ予定です。

  • ユーザー登録・ログイン
  • 複数のリスト(例:仕事、プライベート)を作成可能
  • 各リストに複数のTodoを追加
  • Todoの完了・未完了の管理
  • リストの共有機能(URLを使って他のユーザーと共有)

この記事では、Railsでこのアプリをゼロから構築していく流れを段階的にまとめていきます。自分自身の学習記録も兼ねているので、細かいエラーや試行錯誤も記録していくつもりです。

開発環境

今回の開発環境は以下の通りです。

  • Ruby: 3.2.3
  • Rails: 8.0.2
  • エディタ: Visual Studio Code
  • バージョン管理: Git(GitHubでソースコードを管理)

プロジェクトの作成

まずは新しくRailsプロジェクトを作成します。

rails new todo_app
cd todo_app

Rails 8からは propshaft がデフォルトのアセットパイプラインになっていたりと、いくつか変更点がありますが、今回は標準設定のまま進めていきます。

開発用サーバーの起動確認

todo_app ディレクトリに移動したら、以下のコマンドで開発用サーバーを起動できます。

bin/rails server

実行すると、以下のようなログが表示されます:

=> Booting Puma
=> Rails 8.0.2 application starting in development 
=> Run `bin/rails server --help` for more startup options
Puma starting in single mode...
...
Use Ctrl-C to stop

ブラウザで http://localhost:3000 にアクセスすると、Railsの初期ページが表示されれば成功です 🎉

💡 もし ruby: No such file or directory -- rails のようなエラーが出る場合は、bin/rails が生成されていない可能性があります。その場合は bundle install を実行してから、再度 bin/rails server を試してみてください。

了解!Zennのリンク先の記事のように、Railsの primary_key をUUIDやランダムな文字列にすることで、予測しにくいID設計にできますね。

記事の内容をベースに、IdGenerateModule を導入する流れをZenn記事内にまとめるなら、こんな感じで書けます:


Listモデルの作成

主キーをランダムな文字列にする

デフォルトでは、Railsの主キー(id)は連番の整数ですが、今回はセキュリティやURLの美しさも考慮して、主キーをランダムな文字列に変更します。

以下の記事を参考に、ランダムIDを生成するモジュールを導入しました:
https://zenn.dev/linkedge/articles/616c3884527769

ID生成モジュールの定義

まず、以下のように lib/id_generate_module.rb を作成します:

# lib/id_generate_module.rb
module IdGenerateModule
  extend ActiveSupport::Concern

  included do
    before_create :generate_id
    self.primary_key = 'id'
  end

  private

  def generate_id
    self.id ||= SecureRandom.urlsafe_base64(8)
  end
end

💡 urlsafe_base64(8) は長さ約11文字のランダムな文字列になります。用途に応じて長さを調整できます。

Todoを分類するための「リスト(List)」を作成します。scaffold コマンドを使うことで、モデル、ビュー、コントローラーがまとめて生成され、基本的なCRUD操作も一通りそろいます。

以下のコマンドを実行します:

rails g scaffold list title:string repeat_interval:string 

このコマンドにより、以下のようなファイルが自動生成されます:

  • app/models/list.rb(モデル)
  • app/controllers/lists_controller.rb(コントローラー)
  • app/views/lists/(ビュー一式)
  • db/migrate/xxxxxxxxxxxxxx_create_lists.rb(マイグレーションファイル)
  • ルーティング設定(config/routes.rbresources :lists が追加される)

マイグレーションファイルの修正

主キーを文字列に変更するため、生成済みのマイグレーションファイルを修正します:

# db/migrate/xxxx_create_lists.rb
class CreateLists < ActiveRecord::Migration[8.0]
  def change
    create_table :lists, id: :string, limit: 10 do |t|
      t.string :title
      t.string :repeat_interval

      t.timestamps
    end
  end
end

その後、マイグレーションを実行:

bin/rails db:migrate

モデルに組み込む

たとえば List モデルでこのモジュールを使うには、以下のように記述します:

# app/models/list.rb
class List < ApplicationRecord
  include IdGenerateModule

  validates :title, presence: true
end

動作確認

ブラウザから /lists にアクセスし、リストを新規作成すると、ランダムな文字列のIDが割り当てられていることが確認できます。

Todoを追加する

次に、リストごとにTodo(やること)を登録できるようにします。

モデルとマイグレーションの生成

まずは Todo モデルを List に紐づける形で生成します。
List の主キーがランダムな文字列(string)であるため、list_id の型も明示的に string にする必要があります。

bin/rails g resource todo list_id:string content:string done:boolean

マイグレーションファイルを開いて、done にデフォルト値 false を設定します:

create_table :todos do |t|
  t.string :list_id, null: false
  t.string :content
  t.boolean :done, default: false

  t.timestamps
end

add_foreign_key :todos, :lists, column: :list_id, primary_key: :id

その後マイグレーションを実行します:

bin/rails db:migrate

モデルの関連付け

モデル同士を関連付けておきます。

# app/models/list.rb
class List < ApplicationRecord
  has_many :todos, dependent: :destroy
end

# app/models/todo.rb
class Todo < ApplicationRecord
  belongs_to :list

  validates :content, presence: true
  attribute :done, :boolean, default: false
end

コントローラーの作成

TodosController を作成し、Todoを登録できるようにします。

# app/controllers/todos_controller.rb
class TodosController < ApplicationController
  before_action :set_list

  def create
    @list.todos.create!(todo_params)
    redirect_to @list
  end

  private

  def set_list
    @list = List.find(params[:list_id])
  end

  def todo_params
    params.require(:todo).permit(:content, :done).merge(list_id: @list.id)
  end
end

フォームの作成

リスト詳細ページにTodo追加フォームを設置します。

app/views/todos/_todos.html.erb
<h2>Todos</h2>

<ul id="todos">
  <%= render list.todos %>
</ul>

<%= render "todos/new", list: list %>
app/views/todos/_todo.html.erb
<li id="<%= dom_id(todo) %>">
  <%= todo.content %>
</li>
app/views/todos/_new.html.erb
<%= form_with model: [ list, Todo.new ] do |form| %>
  Add:<br>
  <%= form.text_field :content %>
  <%= form.submit %>
<% end %>
app/views/lists/_new.html.erb
+ <%= render "todos/todos", list: @list %>

動作確認

フォームに入力して送信すると、対応するリストにTodoが追加されるようになります。
done がチェックされていない場合でも、自動的に false として保存されるようになっています。


このように、List に紐づく Todo を適切に扱うためには、主キーが文字列であることを考慮して、マイグレーションやコントローラーを少し工夫する必要があります。

チェックボックスで完了状態を切り替える

Todoを完了済みにしたい場合、チェックボックスを使って done フラグを切り替えられるようにします。

ビューの変更

lists/show.html.erb に、チェックボックス付きのフォームを組み込みます。
チェックの変更と同時に PATCH リクエストが送信され、状態が更新されるようにします。

<!-- app/views/lists/show.html.erb -->
<ul>
  <% @list.todos.each do |todo| %>
    <li id="<%= dom_id(todo) %>" style="<%= 'text-decoration: line-through;' if todo.done %>">
      <%= form_with(model: [@list, todo], method: :patch, data: { turbo_frame: "_top" }) do |f| %>
        <%= f.check_box :done, { checked: todo.done, onchange: "this.form.requestSubmit()" } %>
        <%= todo.content %>
      <% end %>
    </li>
  <% end %>
</ul>
  • チェックが変更された時点で、自動的にフォームが送信されます。
  • done: true のときに取り消し線を表示して、完了済みであることが視覚的にわかるようにしています。

コントローラーの更新

TodosControllerupdate アクションを追加し、チェック状態を保存できるようにします。

# app/controllers/todos_controller.rb
class TodosController < ApplicationController
  before_action :set_list
  before_action :set_todo, only: [:update]

  def create
    @list.todos.create!(todo_params)
    redirect_to @list
  end

  def update
    @todo.update(todo_params)
    redirect_to @list
  end

  private

  def set_list
    @list = List.find(params[:list_id])
  end

  def set_todo
    @todo = @list.todos.find(params[:id])
  end

  def todo_params
    params.require(:todo).permit(:content, :done)
  end
end

ルーティングの確認

ルーティングで update アクションが有効になっているか確認します。

# config/routes.rb
resources :lists do
  resources :todos, only: [:create, :update]
end

動作確認

  1. チェックボックスをクリックすると、非同期で done 状態が更新されます。
  2. true のときには取り消し線が表示され、完了済みであることがわかります。
  3. チェックを外せば false に戻り、再度未完了として扱えます。

これでチェックボックスを使って、Todoの完了状態を柔軟に切り替えられるようになりました ✅
次は、削除機能やリピート処理、またはUX改善などにも進めます!

GitHubで編集を提案

Discussion