やりたいこと
以下のように、ルートが親子関係を持つ場合のフォームの作り方を再現する。
resources :articles do resources :logs end
前提
以下の2つのモデルが存在する。
- Article モデル
- 属性 title:string
- Log モデル
- 属性 title:string
Article モデルは「記事」を管理するモデルであり、Log モデルとは「1対多」の関係にある。
Article has_many Logs
Log モデルとは「ログ」を管理するモデルであり、ひとつの記事にたいして複数存在することができる。
結論
モデルのインスタンスを複数渡すと良い。
<%= form_with(model: [article, log], local: true) do |form| %>
環境
$ ruby -v ruby 2.6.5 $ rails -v Rails 6.0.3.2
前準備
$ rails new nested-routes $ rails g scaffold Article title:string $ rails g scaffold Log title:string article:references $ rails db:migrate
routes.rb
Rails.application.routes.draw do root "articles#index" resources :articles do resources :logs end end
モデルのリレーション
#app/models/article.rb class Article < ApplicationRecord has_many :logs end #app/models/log.rb class Log < ApplicationRecord belongs_to :article end
とりあえず記事を作ってみる。
show をクリック。
記事の詳細が表示された。これで前準備は終了。
次はここにログを表示していく。
ログを表示する
以下を追記する。
app/views/articles/show.html.erb
<p id="notice"><%= notice %></p> <p> <strong>Title:</strong> <%= @article.title %> </p> #追記 <% if @article.logs.present? %> <% @article.logs.each do |log| %> <li><%= log.title %></li> <% end %> <% end %> <%= link_to 'Edit', edit_article_path(@article) %> | <%= link_to 'Back', articles_path %>
「もしこの記事にログが存在するなら、ログのタイトルを表示してね」と記述。
今はまだログを作っていないので、表示はされない。
ログを作成するページへのリンクをつくる。
開発環境でサーバーを起動していると、次のURLでルーティングを確認できる。
http://localhost:3000/rails/info/routes
以下コマンドでもルーティングは確認可能。
$ rails routes
すると以下のようになっている。
new_article_log_path /articles/:article_id/logs/new(.:format) logs#new
上記のルーティングにしたがい、app/views/articles/show.html.erb を修正する。
<%= link_to 'Create log', new_article_log_path(@article) %> | <%= link_to 'Edit', edit_article_path(@article) %> | <%= link_to 'Back', articles_path %>
これでサーバーを起動しアクセスしてみると・・・
こんなエラーメッセージが出る。
undefined method `logs_path' for app/views/logs/_form.html.erb where line #1 raised:
logs_controller.rb を修正する。
def new #追記 @article = Article.find(params[:article_id]) @log = Log.new end ~ ~ private ~ def log_params params.require(:log).permit(:title, :article_id) end
次に app/views/logs/new.html.erb を修正する。
<h1>New Log</h1> #削除する <%= render 'form', log: @log %> <%= link_to 'Back', logs_path %>
わかりやすくするため、今はパーシャルを使わない。よって<%= render 'form', log: @log %>
を削除する。
app/views/logs/_form.html.erb からフォーム内容をコピペしてこよう。エラーメッセージ系の記述も今は削除する。
app/views/logs/new.html.erb
<h1>New Log</h1> <%= form_with(model: log, local: true) do |form| %> #ここらへんにあったエラーメッセージ系の記述は削除する <div class="field"> <%= form.label :title %> <%= form.text_field :title %> </div> #これも削除 <div class="field"> <%= form.label :article_id %> <%= form.text_field :article_id %> </div> <div class="actions"> <%= form.submit %> </div> <% end %> <%= link_to 'Back', logs_path %>
次のように変更する。
<h1>New Log</h1> #変更点 <%= form_with(model: [@article, @log], local: true) do |form| %> <div class="field"> <%= form.label :title %> <%= form.text_field :title %> </div> <div class="actions"> <%= form.submit %> </div> <% end %> #変更点 <%= link_to 'Back', article_path(@article) %>
以下のように変更している。
#変更前 <%= form_with(model: log, local: true) do |form| %> #変更後 <%= form_with(model: [@article, @log], local: true) do |form| %>
こちらも変更。
#変更前 <%= link_to 'Back', logs_path %> #変更後 <%= link_to 'Back', article_path(@article) %>
記事の詳細ページへ。
create log できるようになった。
ログ作成
ログを作成する、create アクションを修正する。
def create @log = Log.new(log_params) #追記 @log.article_id = params[:article_id] if @log.save #変更点 redirect_to article_path(@log.article_id), notice: 'Log was successfully created.' else render :new end end
パラメーターから記事IDを取得し、ログのarticle_id
に代入する。
@log.article_id = params[:article_id]
登録後のリダイレクト先は記事詳細ページにする。
redirect_to article_path(@log.article_id)
ためしにログを作成してみる。
作成できた。
ログを編集する
まずはログ編集ページへリンクを貼る。
app/views/articles/show.html.erb
<% if @article.logs.present? %> <% @article.logs.each do |log| %> <li><%= link_to log.title, article_log_path(log) %></li> <% end %> <% end %>
app/views/logs/show.html.erb を変更。
<p id="notice"><%= notice %></p> <p> <strong>Title:</strong> <%= @log.title %> </p> #削除↓ <p> <strong>Article:</strong> <%= @log.article_id %> </p> #変更点 <%= link_to 'Edit', edit_article_log_path(@log) %> | <%= link_to 'Back', article_path(@log.article_id) %>
app/views/logs/edit.html.erb
わかりやすくするため、また render メソッドを削除してフォームをこのファイルに直書きする。
<h1>Editing Log</h1> #削除 <%= render 'form', log: @log %> <%= link_to 'Show', @log %> | <%= link_to 'Back', logs_path %>
そして以下のように変更。
<h1>Editing Log</h1> <%= form_with(model: [@log.article, @log], local: true) do |form| %> <div class="field"> <%= form.label :title %> <%= form.text_field :title %> </div> <div class="actions"> <%= form.submit %> </div> <% end %> <%= link_to 'Back', article_path(@log.article_id) %>
logs_controller.rb の update アクションを修正。
def update if @log.update(log_params) redirect_to article_path(@log.article_id), notice: 'Log was successfully updated.' else render :edit end end
編集してみる。
編集できた。
ログを削除する
作成、編集ができたので最後に削除機能を実装する。
logs_controller.rb
def destroy @log.destroy redirect_to logs_url, notice: 'Log was successfully destroyed.' end
以下のように変更。リダイレクト先を変えただけ。
def destroy @log.destroy redirect_to article_path(@log.article_id), notice: 'Log was successfully destroyed.' end
app/views/logs/show.html.erb に追記。
<%= link_to 'Edit', edit_article_log_path(@log) %> | #追記 <%= link_to 'Delete', article_log_path(@log), method: :delete, data: { confirm: 'Are you sure?' } %> | <%= link_to 'Back', article_path(@log.article_id) %>
削除してみる。
削除できた。
ルーティングを整理
やりたいことは実現できた。しかし、ルーティングが必要以上に複雑になっている。
とくに以下
article_log_path /articles/:article_id/logs/:id(.:format) logs#show
ログ詳細を見る場合、すでにログオブジェクトには記事IDが保存されている。よって、パラメーターから:article_id
をわざわざ渡す必要はない。
article_log_path /articles/:article_id/logs/:id(.:format) logs#update
ログ詳細のときと同様に、すでにログオブジェクトに記事情報があるため、パラメーターから渡す必要はない。
article_log_path /articles/:article_id/logs/:id(.:format) logs#destroy
削除時に:article_id
を持っている必要はない。
よって以下のようにルーティングを変えてみる。
routes.rb
resources :articles do resources :logs, only: %i(new create update) end resources :logs, only: %i(show destroy edit)
Ruby の機能で%i
でシンボルの配列を簡単に表記できる。
以下の意味と同様になる。
resources :logs, only: [:new, :create, :update]
ルーティングを確認してみる。
log_path GET /logs/:id(.:format) logs#show
かなり短くなった。
app/views/articles/show.html.erb を修正。
<% if @article.logs.present? %> <% @article.logs.each do |log| %> <li><%= link_to log.title, log_path(log) %></li> <% end %> <% end %>
app/views/logs/show.html.erb を修正。
<p id="notice"><%= notice %></p> <p> <strong>Title:</strong> <%= @log.title %> </p> #変更点 <%= link_to 'Edit', edit_log_path(@log) %> | <%= link_to 'Delete', article_log_path(@log), method: :delete, data: { confirm: 'Are you sure?' } %> | <%= link_to 'Back', article_path(@log.article_id) %>
編集してみる。
編集できた。
URLも短くキレイになっている。
http://localhost:3000/logs/2/edit
削除
app/views/logs/show.html.erb を修正。
<%= link_to 'Edit', edit_log_path(@log) %> | #変更点 <%= link_to 'Delete', @log, method: :delete, data: { confirm: 'Are you sure?' } %> | <%= link_to 'Back', article_path(@log.article_id) %>
<%= link_to 'Delete', article_log_path(@log),
だったのを<%= link_to 'Delete', log_path(@log)
に変更。
省略して<%= link_to 'Delete', @log,
と書くこともできる。これでルーティングの修正は完了。
フォームをパーシャル化
これまで、new と edit にはフォームを直書きで記述した。これらのフォームをパーシャル化して切り出す。
app/views/logs/new.html.erb
<h1>New Log</h1> <%= render partial: "form", locals: { article: @article, log: @log} %>
app/views/logs/edit.html.erb
<h1>Editing Log</h1> <%= render partial: "form", locals: { article: @log.article, log: @log} %>
app/views/logs/_form.html.erb
<%= form_with(model: [article, log], local: true) do |form| %> <% if log.errors.any? %> <div id="error_explanation"> <h2><%= pluralize(log.errors.count, "error") %> prohibited this log from being saved:</h2> <ul> <% log.errors.full_messages.each do |message| %> <li><%= message %></li> <% end %> </ul> </div> <% end %> <div class="field"> <%= form.label :title %> <%= form.text_field :title %> </div> <div class="actions"> <%= form.submit %> </div> <% end %>
リポジトリ
よかったら参考にどうぞ。
https://github.com/kotakanazawa/nested-routes
感想
RESTむずかしいなぁ・・・。
関連
render
メソッドはちょっと注意ポイントがある。
親と子モデルを複数オブジェクトを一括で登録したい場合のフォームの作り方。