はじめに

has_oneで結んだ親テーブルと子テーブルの値を
1つのフォームで同時に保存する方法を解説!
※has_manyの場合はやり方が少し異なります。ご注意ください。

音楽系のアプリを作っていて、
「Youtubeの埋め込みurlを持った子テーブルから
urlを引っ張ってきて投稿の中に表示したい!」と思い、
それを実現すべくネストしたフォームを作る中でかなり苦戦しました...

ネストしたフォームを作りたい方はお役に立てるかもしれません。

環境

Rails 6.0.3.5
ruby 2.7.2

モデル

models/post.rb
belongs_to :user
has_one :portfolio, dependent: :destroy
accepts_nested_attributes_for :portfolio
models/portfolio.rb
belongs_to :post

qiita_image.jpg

PostがPortfolioをhas_oneで保持されている形ですね。
Postがhas_manyによってuserに保持されています。

accepts_nested_attributes_forをつけることによって
portrofolioをpost内にネストさせることが可能になります。

コントローラー

controllers/post_controller.rb
def new
    @post = Post.new
    @post.build_portfolio
  end

  def create
    @post = current_user.posts.build(post_params)
    if @post.save
      flash[:success] = '投稿しました'
      redirect_to @post
    else
      render 'post/new'
    end
  end

  def edit
    @post.portfolio
  end

  def update
    @post = Posr.find(params[:id])
    if @post.update(post_params)
      flash[:success] = '投稿内容を編集しました'
      redirect_to @post
    else
      render 'edit'
    end
  end


  private
    def post_params
      params.require(:exhibition).permit(:title, :content, 
                                 portfolio_attributes: [:first_url, :second_url, :third_url])
    end



コントローラーで注意すべき点は、newアクションとeditアクションです。

newアクション
@post.build_portfolio
editアクション
@post.portfolio

newとeditのアクションでこの記述をしないと子要素のportfolioの値を送信するための
フォームがビューで表示されなくなってしまいます。

editではnewにはあったbuild_が外れていることに注目してください。
このbuild_はnewではportfolioを新規作成するために必要です。
しかしeditでもbuild_をつけたままにしておくと、初期化しようとするのかなんなのか
投稿の編集画面にアクセスすると投稿に元々紐付いていたportfolioを自動でdeleteしてしまいます。
僕はこの仕様の解決にかなり苦戦しました。。

editに@post.portfolioをつけないとportfolioのフォームに初期値が入らないので
必ず入れましょう。

ビュー

new.html.erb
<div class="post-form-area">
  <div class="form-list">
    <%= form_with(model: @post, local: true, class: 'form') do |f| %>
      <%= render 'shared/error_messages', object: f.object %>

      <%= f.label :title, 'タイトル ※必須' %>
      <%= f.text_field :title, class: 'form-input' %>

      <%= f.label :content, '本文 ※必須' %>
      <%= f.text_area :content, class: 'content form-input' %>

      <%= f.fields_for :portfolio do |p| %>
        <%= p.label :portfolio, 'ポートフォリオ' %>
        <%= p.text_field :first_url, placeholder: 'URL(1)', class: 'pf form-input' %>
        <%= p.text_field :second_url, placeholder: 'URL(2)', class: 'pf form-input' %>
        <%= p.text_field :third_url, placeholder: 'URL(3)', class: 'pf form-input' %>
      <% end %>

      <%= f.submit '投稿', class: 'btn' %>

    <% end %>
  </div>
</div>

fiels_forを使うことでネストしたフォームを作成することができます。
今回はfirst_url,second_url,third_urlをportfolioテーブルにtextとして
保存するためのフォームを作りました。

引数にplaceholderを渡すことでなにも入力されていないときに
URL(1)などを薄く表示できます。

show.html.erb
<%= content_tag :span, "[ #{@user.name} ]", class: 'title' %>
<%= content_tag :p, @postn.title, class: 'title' %>
<%= link_to '記事を編集する', edit_post_path(@post), class: 'edit-link' %>
<div class="content">
  <%= safe_join(@post.content.split("\n"),tag(:br)) %>
</div>
<div class="pf-section">
  <p>ポートフォリオ</p>
    <%= content_tag :iframe, '', src: "https://www.youtube.com/embed/#{@post.portfolio.first_url.last(11)}",
                                class: 'youtube' unless @post.portfolio.first_url.blank? %>
    <%= content_tag :iframe, '', src: "https://www.youtube.com/embed/#{@post.portfolio.second_url.last(11)}",
                                class: 'youtube' unless @post.portfolio.second_url.blank? %>
    <%= content_tag :iframe, '', src: "https://www.youtube.com/embed/#{@post.portfolio.third_url.last(11)}",
                                class: 'youtube' unless @post.portfolio.third_url.blank? %>
</div>

子要素を表示する場合には@親要素.子要素.カラム名で表示することができます。
今回の場合は@post.portfolio.first_urlとなります。

おわりに

以上でネストしたフォームを作れるかと思います!
もしうまくいかなかったりおかしなところがあれば
コメントでお知らせいただけると幸いです!

最後まで読んでいただきありがとうございました!