Search code examples
ruby-on-railsrails-activerecordruby-on-rails-5has-many

Creating a nested model instance (has_many relation) on the view of its parent model


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!


Solution

  • 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? }