Railsの部分テンプレート、パーシャルビューについて
Rialsには共通して使うビューのコードをまとめて再利用する事ができる部分テンプレート、パーシャルビューがあります。
開発効率を上げてくれる便利なモノなのですが、使い方がわからなくて困ってしまうこともあるモノです。
そのため、今回は部分テンプレートのいろいろなパターンに応じての使い方を紹介します。
部分テンプレートとは
まず、単純に部分テンプレートに置き換える例を見ていきます。
ルーティング、コントローラー、DB、ビューがそれぞれ以下のようになっているとします。
※バリデーションは特に設定していないものとします。
部分テンプレートを使う前のコード
1Rails.application.routes.draw do
2 resources :posts, only: [:new]
3end
1class PostsController < ApplicationController
2 def new
3 @post = Post.new
4 @tag = Tag.new
5 end
6end
1create_table "post_tags", force: :cascade do |t|
2 t.integer "post_id", null: false
3 t.integer "tag_id", null: false
4 t.datetime "created_at", null: false
5 t.datetime "updated_at", null: false
6 t.index ["post_id"], name: "index_post_tags_on_post_id"
7 t.index ["tag_id"], name: "index_post_tags_on_tag_id"
8 end
9
10create_table "posts", force: :cascade do |t|
11 t.string "title"
12 t.text "content"
13 t.datetime "created_at", null: false
14 t.datetime "updated_at", null: false
15 end
16
17 create_table "tags", force: :cascade do |t|
18 t.string "name"
19 t.datetime "created_at", null: false
20 t.datetime "updated_at", null: false
21 end
1<%= @post %>
2<%= @tag %>
部分テンプレートを使う前の表示
この状態でビューファイルを部分テンプレートから呼び出してみます。
部分テンプレートを使用
new.html.erbの記述を同じ階層の_form.html.erbに移動します。
1<%= render partial: 'form', locals: { post: @post, tag: @tag } %>
1<%= post %>
2<%= tag %>
すると表示は以下のように同じになります。
postやnewの後ろの数字は異なっていますが、新しくインスタンス化されたpostやtagの数字になるので、同じものが表示されていると認識していただければ大丈夫です。
ここまでただ部分テンプレートにコードを移しただけの例を紹介してきましたが、変数の渡し方について気になった方もいるかと思いますので、コードと実際の画面を紹介していきます。
検証:インスタンス変数をそのまま渡したらどうなるか
先ほどは以下のようにインスタンス変数をlocalsを使って、ローカル変数として引数で渡していました。
インスタンス変数のままでは渡せないのか気になりますよね?検証していきます。
new.html.erbと_form.html.erbを以下のように変更します。
1<%= render partial: 'form' %>
1<%= @post %>
2<%= @tag %>
予想を裏切られた方もいるかもしれませんが、実はインスタンス変数のままでも渡せてしまいます。
では、なぜインスタンス変数ではなく、わざわざlocals変数を使うのか?
それは部分テンプレートが再利用可能なものにしたいためです。具体例で見ていきましょう。
newとupdateで共通の部分テンプレートにしてみる
よくあるパターンとしては、createとupdateで同じフォーム(つまり、newとshowのビューが同じ)になるので、部分テンプレートとして切り出すパターンです。
showを追加で用意していきます。
1Rails.application.routes.draw do
2 resources :posts, only: [:new, :show]
3end
1class PostsController < ApplicationController
2 def new
3 @post = Post.new
4 @tag = Tag.new
5 end
6
7 def show
8 @post = Post.find(params[:id])
9 @tags = @post.tags
10 end
11end
1<%= render partial: 'form' %>
1<%= @post %>
2<%= @tag %>
こちらでshowの画面を表示するとどうなるでしょうか?
tagも表示されるはずが、postしか表示されなくなってしまいました。
それは、なぜかというと、コントローラーのshowアクションで、tagのデータは@tagsとしているためです。
部分テンプレートでは、@tagを表示しようているので、表示できませんよね。
では次にlocalsオプションを使った例を見てみましょう。
localsオプションを再度使ってみる
コードを以下のように書き換えます。
1<%= render partial: 'form', locals: { post: @post, tag: @tags } %>
1<%= render partial: 'form', locals: { post: @post, tag: @tag } %>
1<%= post %>
2<%= tag %>
すると、今度はshowでもtagが表示されるようになりました。
このように、newとshowでは@tagと@tagsとインスタンス変数の名前が違っても、tagとして渡せば、部分テンプレートとして再利用可能になります。
そのためにlocalsオプションはあるんですね。
form_withと部分テンプレートの組み合わせ
ここまでで部分テンプレート単体で説明をしてきましたが、form_withと組み合わせて使う場面も多いかと思います。
そのため、ここからはform_withと部分テンプレートの組み合わせての仕様について説明していきます。
先ほどの例を使い回していきますので、同じところもありますが、改めて具体例の全容をまとめていきます。
1Rails.application.routes.draw do
2 resources :posts, only: [:new, :create, :show]
3end
4
1class PostsController < ApplicationController
2 def new
3 @post = Post.new
4 @tag = Tag.new
5 end
6
7 def create
8 @post = Post.new(post_params)
9 @tag = @post.tags.build(tag_params)
10 if @post.save
11 redirect_to @post
12 else
13 render :new
14 end
15 end
16
17 def show
18 @post = Post.find(params[:id])
19 @tags = @post.tags
20 end
21
22 private
23
24 def post_params
25 params.require(:post).permit(:title, :content)
26 end
27
28 def tag_params
29 params.require(:post).require(:tag).permit(:name)
30 end
31end
32
1<%= render partial: 'form', locals: { post: @post, tag: @tag } %>
1<%= form_with model: post, url: posts_path, local: true do |form| %>
2 <div class="field">
3 <%= form.label :title %>
4 <%= form.text_field :title %>
5 </div>
6 <div class="field">
7 <%= form.label :content %>
8 <%= form.text_area :content %>
9 </div>
10 <%= form.fields_for :tag, tag do |tag_fields| %>
11 <div class="field">
12 <%= tag_fields.label :tag %>
13 <%= tag_fields.text_field :name %>
14 </div>
15 <% end %>
16 <%= form.submit %>
17<% end %>
1<p>
2 <strong>Title:</strong>
3 <%= @post.title %>
4</p>
5
6<p>
7 <strong>Content:</strong>
8 <%= @post.content %>
9</p>
10
11<p>
12 <strong>Tags:</strong>
13 <%= @tags.map(&:name).join(', ') %>
14</p>
上記のようなコードだと以下のように動作します。
urlを見てもわかるように、new-→create→showと動いていますね。
MVCの動きが理解できていないとコードを追うのも大変ですからまずはMVCが理解できているかを確認してみてください。
もしわからない事があれば、無料質問も承っているので、以下からお申し込みください。
ポイント①:複数のインスタンス変数を渡すときのmodelの書き方
postとtag、2つのインスタンス変数を渡しているからといって、modelにpost, tagと記載しないのがポイントです。
1<%= form_with model: post, url: posts_path, local: true do |form| %>
もしpost、tagの2つを記載してしまうと以下のように保存ができなくなってしまいます。
1<%= form_with model: [post, tag], url: posts_path, local: true do |form| %>
何が起きているかターミナルを確認してみましょう。以下のようにエラーが出ています。
1Started POST "/posts" for ::1 at 2024-03-15 08:06:17 +0900
2Processing by PostsController#create as TURBO_STREAM
3 Parameters: {"authenticity_token"=>"[FILTERED]", "tag"=>{"title"=>"ttile", "content"=>"content", "tag"=>{"name"=>"tag"}}, "commit"=>"Create Tag"}
4Completed 400 Bad Request in 2ms (ActiveRecord: 0.0ms | Allocations: 806)
5
6ActionController::ParameterMissing (param is missing or the value is empty: post):
これは何が起きているかというと、paramsが以下のような構造になってしまったために、コントローラーに記述しているpost_paramsのコードが適切にparamsを処理できなくなってしまっています。
1pry(#<PostsController>)> params[:tag]
2=> #<ActionController::Parameters {"title"=>"title", "content"=>"content", "tag"=>{"name"=>"tag"}} permitted: false>
1pry(#<PostsController>)> params[:post]
2=> nil
1 def post_params
2 params.require(:post).permit(:title, :content)
3 end
post_paramsでは、params.require(:post)としているので、params[:post]にpostのtitleやcontentが送られてくることを想定していますね。
paramsの構造が変わってしまったのは、最初にお伝えしていたmodelの書き方が原因です。
1<%= form_with model: [post, tag], url: posts_path, local: true do |form| %>
modelを2つ指定すると、後ろに書いたtagがparasmの構造に使われてしまい、postが無視されているのがわかります。
そのため、modelには1つのみ記載するようにするのがポイントです。
ポイント②:field_forはparamsの構造に影響する
先ほどparamsの構造を紹介した中で、tagの方が気になっている方もいたのではないでしょうか?
1 params[:tag][:tag][:name]
2=> "tag"
入力したタグの名前を取り出すには、paramsのtagを2回掘っていかないと取り出せない状態になっています。
これは、fields_forが影響しています。
1<%= form_with model: post, url: posts_path, local: true do |form| %>
2 <div class="field">
3 <%= form.label :title %>
4 <%= form.text_field :title %>
5 </div>
6 <div class="field">
7 <%= form.label :content %>
8 <%= form.text_area :content %>
9 </div>
10 <%= form.fields_for :tag, tag do |tag_fields| %>
11 <div class="field">
12 <%= tag_fields.label :tag %>
13 <%= tag_fields.text_field :name %>
14 </div>
15 <% end %>
16 <%= form.submit %>
17<% end %>
試しにfields_forをなくしてみたいと思います。
1<%= form_with model: [post, tag], url: posts_path, local: true do |form| %>
2 <div class="field">
3 <%= form.label :title %>
4 <%= form.text_field :title %>
5 </div>
6 <div class="field">
7 <%= form.label :content %>
8 <%= form.text_area :content %>
9 </div>
10 <div class="field">
11 <%= form.label :tag %>
12 <%= form.text_field :name %>
13 </div>
14 <%= form.submit %>
15<% end %>
するとこの場合もparamsの構造が変化し、保存に失敗しています。
1Processing by PostsController#create as TURBO_STREAM
2 Parameters: {"authenticity_token"=>"[FILTERED]", "post"=>{"title"=>"タイトル", "content"=>"内容", "name"=>"タグ"}, "commit"=>"Create Post"}
3
4ActionController::ParameterMissing (param is missing or the value is empty: tag):
今度はtagがemptyとエラーになっていますね。
binding.pryを同じように使えばparamsの構造とtag_paramsが期待している構造が一致していないので、エラーになっている事がわかると思います。
ぜひ試してみてくださいね。
エラーの原因はfields_forを使っているかどうかで、paramsの構造が変化している事です。
補足:多対多のテーブルに同時に保存するときの基本的な書き方
先ほどは説明の都合上、postとtagが多対多で紐づいている際、両方のインスタンス変数をnewアクションで生成し、渡していました。
しかし、基本的には以下のように主となるデータ(postsコントローラーで処理をしているため、今回はpost)だけ渡すようにします。
1class PostsController < ApplicationController
2 def new
3 @post = Post.new
4 end
5
6 def create
7 @post = Post.new(post_params)
8 tag_names = params[:post][:tag_names].split(",").map(&:strip)
9
10 if @post.save
11 tag_names.each do |tag_name|
12 tag = Tag.find_or_create_by(name: tag_name)
13 @post.tags << tag
14 end
15
16 redirect_to @post, notice: 'Post was successfully created.'
17 else
18 render :new
19 end
20 end
21
22 def show
23 @post = Post.find(params[:id])
24 @tags = @post.tags
25 end
26
27 private
28
29 def post_params
30 params.require(:post).permit(:title, :content)
31 end
32end
1<%= form_with(model: @post) do |form| %>
2 <%= form.label :title %>
3 <%= form.text_field :title %>
4
5 <%= form.label :content %>
6 <%= form.text_area :content %>
7
8 <%= form.label :tag_names,"Tag"%>
9 <%= form.text_field :tag_names, id: :post_tag_names %>
10
11 <%= form.submit %>
12<% end %>
ちなみにtagはカンマ区切りでデータを保存するのが実用的なので、合わせて変更しています。
どのコードかはぜひ考えてみてくださいね。
もしわからなかった方は、以下から質問をしてみてください。
まとめ
部分テンプレート、form_withとの関係性を確認しました。
- localsオプションは部分テンプレートの再利用性を向上させる
- modelオプションには一つのインスタンス変数しか指定しない
- fields_forはparamsの構造を変化させる
プログラミングについて聞いてみたい事がある方は以下から、質問をしてみてください。