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を生成するモジュールを導入しました:
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.rb
にresources :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追加フォームを設置します。
<h2>Todos</h2>
<ul id="todos">
<%= render list.todos %>
</ul>
<%= render "todos/new", list: list %>
<li id="<%= dom_id(todo) %>">
<%= todo.content %>
</li>
<%= form_with model: [ list, Todo.new ] do |form| %>
Add:<br>
<%= form.text_field :content %>
<%= form.submit %>
<% end %>
+ <%= 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
のときに取り消し線を表示して、完了済みであることが視覚的にわかるようにしています。
コントローラーの更新
TodosController
に update
アクションを追加し、チェック状態を保存できるようにします。
# 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
動作確認
- チェックボックスをクリックすると、非同期で
done
状態が更新されます。 -
true
のときには取り消し線が表示され、完了済みであることがわかります。 - チェックを外せば
false
に戻り、再度未完了として扱えます。
これでチェックボックスを使って、Todoの完了状態を柔軟に切り替えられるようになりました ✅
次は、削除機能やリピート処理、またはUX改善などにも進めます!
Discussion