Creating a nested model instance (has_many relatio

2019-08-18 22:09发布

问题:

I have a model A und a model B and the relation is A has_many B (and B belongs_to A). I have a model, a controller and views for A and only a model for B. I want to create and edit the instances of B on the edit view of A ( url/a/1/edit).

I know I can create a controller for B and call those methods using a form in the view of A, but then I need to redirect back to the view of A, because I don't want actual views for B.

Is there a recommended way to do this? What I want is to not break any of the helpers rails provides (e.g. after a forward I think it's a pain to get error messages and stuff like that).

Thanks in advance!

回答1:

On the model level you would use accepts_nested_attributes_for.

class A < ApplicationModel
  has_many :bs
  accepts_nested_attributes_for :bs
  validates_associated :bs
end

class B < ApplicationModel
  belongs_to :a
end

This lets A take attributes and create nested bs by passing the attribute bs_attributes with an array of attributes. validates_associated can be used to ensure that A cannot be persisted of the bs are not also valid.

To create the nested form fields use fields_for

<%= form_for(@a) do |f| %>
  # field on A
  <%= f.text_input :foo %>
  # creates a fields for each B associated with A.
  <%= f.fields_for(:bs) do |b| %>
    <%= b.text_input :bar %>
  <% end %>
<% end %>

To whitelist nested attributes use a hash key with an array of permitted attributes for the child records:

params.require(:a)
      .permit(:foo, bs_attributes: [:id, :bar])

When creating new records you also have to "seed" the form if you want there to be inputs present for creating nested records:

class AsController < ApplicationController

  def new
    @a = A.new
    seed_form
  end

  def create
    @a = A.new(a_params)
    if @a.save 
      redirect_to @a
    else
      seed_form
      render :new
    end
  end

  def update
    if @a.update(a_params) 
      redirect_to @a
    else
      render :edit
    end
  end

  private

  def seed_form
    5.times { @a.bs.new } if @a.bs.none?
  end

  def a_params
    params.require(:a)
          .permit(:foo, bs_attributes: [:id, :bar])
  end
end

Edit: seed_form can also just add one and do that every time. So you will always have one "empty" one to add. You need to make sure to filter out the empty one before saving if it was not filled by changing the accepts_nested_attributes_for to:

accepts_nested_attributes_for :bs, reject_if: proc { |attr| attr['bar'].blank? }