rails-tips

Railsの部分テンプレート、パーシャルビューについて

pepe87
記事内に商品プロモーションを含む場合があります

Rialsには共通して使うビューのコードをまとめて再利用する事ができる部分テンプレート、パーシャルビューがあります。

開発効率を上げてくれる便利なモノなのですが、使い方がわからなくて困ってしまうこともあるモノです。

そのため、今回は部分テンプレートのいろいろなパターンに応じての使い方を紹介します。

部分テンプレートとは

まず、単純に部分テンプレートに置き換える例を見ていきます。

ルーティング、コントローラー、DB、ビューがそれぞれ以下のようになっているとします。

※バリデーションは特に設定していないものとします。

部分テンプレートを使う前のコード

routes.rb
1Rails.application.routes.draw do
2  resources :posts, only: [:new]
3end
posts_controller.rb
1class PostsController < ApplicationController
2  def new
3    @post = Post.new
4    @tag = Tag.new
5  end
6end
shemarb
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
new.html.erb
1<%= @post %>
2<%= @tag %>

部分テンプレートを使う前の表示

この状態でビューファイルを部分テンプレートから呼び出してみます。

部分テンプレートを使用

new.html.erbの記述を同じ階層の_form.html.erbに移動します。

new.html.erb
1<%= render partial: 'form', locals: { post: @post, tag: @tag } %> 
_form.html.erb
1<%= post %>
2<%= tag %>

すると表示は以下のように同じになります。

postやnewの後ろの数字は異なっていますが、新しくインスタンス化されたpostやtagの数字になるので、同じものが表示されていると認識していただければ大丈夫です。

ここまでただ部分テンプレートにコードを移しただけの例を紹介してきましたが、変数の渡し方について気になった方もいるかと思いますので、コードと実際の画面を紹介していきます。

検証:インスタンス変数をそのまま渡したらどうなるか

先ほどは以下のようにインスタンス変数をlocalsを使って、ローカル変数として引数で渡していました。

インスタンス変数のままでは渡せないのか気になりますよね?検証していきます。

new.html.erbと_form.html.erbを以下のように変更します。

new.html.erb
1<%= render partial: 'form' %> 
_form.html.erb
1<%= @post %>
2<%= @tag %>

予想を裏切られた方もいるかもしれませんが、実はインスタンス変数のままでも渡せてしまいます。

では、なぜインスタンス変数ではなく、わざわざlocals変数を使うのか?

それは部分テンプレートが再利用可能なものにしたいためです。具体例で見ていきましょう。

newとupdateで共通の部分テンプレートにしてみる

よくあるパターンとしては、createとupdateで同じフォーム(つまり、newとshowのビューが同じ)になるので、部分テンプレートとして切り出すパターンです。

showを追加で用意していきます。

showでデータを表示するにはデータを保存している必要があるので注意してください。

routes.rb
1Rails.application.routes.draw do
2  resources :posts, only: [:new, :show]
3end
posts_controller.rb
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
show.html.erb
1<%= render partial: 'form' %> 
_form.html.erb
1<%= @post %>
2<%= @tag %>

こちらでshowの画面を表示するとどうなるでしょうか?

tagも表示されるはずが、postしか表示されなくなってしまいました。

それは、なぜかというと、コントローラーのshowアクションで、tagのデータは@tagsとしているためです。

部分テンプレートでは、@tagを表示しようているので、表示できませんよね。

では次にlocalsオプションを使った例を見てみましょう。

localsオプションを再度使ってみる

コードを以下のように書き換えます。

show.html.erb
1<%= render partial: 'form', locals: { post: @post, tag: @tags } %> 
new.html.erb
1<%= render partial: 'form', locals: { post: @post, tag: @tag } %> 
_form.html.erb
1<%= post %>
2<%= tag %>

すると、今度はshowでもtagが表示されるようになりました。

このように、newとshowでは@tagと@tagsとインスタンス変数の名前が違っても、tagとして渡せば、部分テンプレートとして再利用可能になります。

そのためにlocalsオプションはあるんですね。

form_withと部分テンプレートの組み合わせ

ここまでで部分テンプレート単体で説明をしてきましたが、form_withと組み合わせて使う場面も多いかと思います。

そのため、ここからはform_withと部分テンプレートの組み合わせての仕様について説明していきます。

先ほどの例を使い回していきますので、同じところもありますが、改めて具体例の全容をまとめていきます。

routes.rb
1Rails.application.routes.draw do
2  resources :posts, only: [:new, :create, :show]
3end
4
posts_controller.rb
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
new.html.erb
1<%= render partial: 'form', locals: { post: @post, tag: @tag } %> 
_form.html.erb
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 %> 
show.html.erb
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と記載しないのがポイントです。

_form.html.erb
1<%= form_with model: post, url: posts_path, local: true do |form| %>

もしpost、tagの2つを記載してしまうと以下のように保存ができなくなってしまいます。

_form.html.erb
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を処理できなくなってしまっています。

gem ‘pry-rails’をbundle installした状態で、「binding.pry」を記述すると、処理を途中で止めて確認できます。

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の書き方が原因です。

_form.html.erb
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の方が気になっている方もいたのではないでしょうか?

paramsの構造
1 params[:tag][:tag][:name]
2=> "tag"

入力したタグの名前を取り出すには、paramsのtagを2回掘っていかないと取り出せない状態になっています。

これは、fields_forが影響しています。

_form.html.erb
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をなくしてみたいと思います。

_form.html.erb
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)だけ渡すようにします。

posts_controller.rb
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
_form.html.erb
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の構造を変化させる

プログラミングについて聞いてみたい事がある方は以下から、質問をしてみてください。

ABOUT ME
いず
いず
1995年生まれ・愛知県出身・都心で二人暮らし。IT系PDM・プロコーチ・プログラミング学習サービス代表。趣味:ミニマリスト・コーヒー・デザイン
記事URLをコピーしました