Last Updated: March 22, 2019
·
10.17K
· sslotsky

Dynamically add nested forms to an ActiveModel::Model form

Last week we talked about how to create nested records using an ActiveModel::Model form. The problem with the end result of my example is that we could only add a single Contact record, whereas we need to be able to add arbitrarily many. What we need here is a link or a button that will add the fields for another Contact whenever clicked.

There is a great Railscast devoted to this, and it almost works for us except for one detail. Check out the first line of the link_to_add_fields function:

def link_to_add_fields(name, f, association)
  new_object = f.object.class.reflect_on_association(association).klass.new
  fields = f.fields_for(association, new_object, :child_index => "new_#{association}") do |builder|
    render(association.to_s.singularize + "_fields", :f => builder)
  end
  link_to_function(name, "add_fields(this, \"#{association}\", \"#{escape_javascript(fields)}\")")
end

ActiveModel::Model classes don't have associations and they don't have reflections. So once again, we'll have to add some stuff to our class to fake out the behavior. It looks like we just need it to return an object that has the klass method defined where calling this method will return the class of the association.

require 'ostruct'
class ContactListForm
  include ActiveModel::Model

  attr_accessor :contacts

  def self.association association, klass
    @@attributes ||= {}
    @@attributes[association] = klass
  end

  association :contacts, Contact

  def self.reflect_on_association(association)
    data = { klass: @@attributes[association] }
    OpenStruct.new data
  end

  .....
end

What we did here is pretty simple. We define a class method self.association so that we can register classnames for our associations into a class variable. Then we define self.reflect_on_associations to create an OpenStruct object whose klass method simply looks in its @@attributes hash for the passed in association name and returns the class that we registered.

With this change, we can use Ryan's technique verbatim to create this link on our form.

Thanks for reading, and hopefully this helps at least a few of you.

12 Responses
Add your response

Can I use ActiveRecord for the master entity and ActiveModel for the detail entity? I am trying to save the data for the detail into a text column in the master table itself.

over 1 year ago ·

@geordee I'm not sure what your use case is but this doesn't sound very clean. If you can make that input part of the form for the master entity that would make a whole lot more sense.

Having said that, in general you probably could use ActiveModel as a detail entity if there were a good use case for it. You'd probably have to define the detail entity as a virtual attribute on your ActiveRecord model and maybe add a before_save hook that validates your detail entity if it's present. But this doesn't sound all that clean either. Maybe understanding your use case would be helpful.

over 1 year ago ·

Say, I have an invoice model and multiple taxes attached to it. It may not be too many (unlike invoiceitems), so I was thinking of avoiding the invoicetaxes table and the join thereof. I was thinking of leveraging RyanB's nestedform for dynamically generating the tax lines. I am already using the gem for invoiceitems, so writing another set of helpers and JavaScripts isn't all that exciting.

over 1 year ago ·

So you have multiple taxes for an invoice, but you're not storing tax records in their own table. How do you store them on the invoice then? Do you have a field for each possible type of tax? A single field that stores a JSON string?

The answer for you might be to use an ActiveModel form object for your main object. When it's submitted, you can do whatever calculations you need to with the tax parameters and then create your Invoice.

over 1 year ago ·

I intend to store it as a JSON string. I started with ActiveModel form, but the Nested Form was giving errors. In your article association and reflectonassociation are on the ContactList model, right? If I use it that way, other things (invoice items) break.

over 1 year ago ·

I think I see what you mean. You're trying to dynamically add tax information using Ryan B's technique, but you don't have a tax class of any kind. In your case you wouldn't even use reflect_on_association. You would have to do something else to generate the fields you need and add them to the form.

over 1 year ago ·

Right. But, since I use RyanB's gem already for invoice items, it would be almost a duplication of logic, and that's how I thought of a hack and came across this article. Then I was wondering, will I be able to convince Rails that there is a hasmany relation and acceptsnestedattributesfor so that nested_form will generate the lines for me.

over 1 year ago ·

I see. This sounds pretty difficult, but maybe define a Tax class with ActiveModel::Model setup and then your Invoice model can override self.reflect_on_association so that when :taxes is passed in, it will return an object where the klass method returns your Tax class (your method will call super if anything else is passed in). Then you should be able to get your fields in the form by defining attributes in your Tax class.

How to handle submission would be a different story. You might have to start with an ActiveModel::Model object that encapsulates the Invoice and displays it in a f.fields_for :invoice do |invoice_fields|, and then the taxes would show up as a nested form within invoice_fields.

over 1 year ago ·

I had a feeling that I was looking for the impossible, or impractical one. ActiveRecord Store is another route I took. Only if the nested_form gem could cover some more scenarios.

Thanks for the and it was a great discussion.

over 1 year ago ·

Hello,
I didn't need to all this mambo jambo to work with nested_form.
I just submitted an object directly to use as a blueprint to the add link this way:

f.linktoadd 'Add new item', :items, model_object: Item.new

The Item class is a plain Ruby Struct class.

PS: How come your posts are not dated? It's useful, especially when things move so fast like in the Rails community.

over 1 year ago ·

Hi @geordee, I'm facing the same scenario which you explained. Did you find out any workaround for it?

over 1 year ago ·

thanks to this i could implement the solution. beware that the JavaScript of the screencast is old and faulty.
Also, linktofunction is not available in 4.2 anymore. You have to use link_to with onclick

function remove_fields(link) {
$(link).prev("input[type=hidden]").val("1");
$(link).closest(".fields").hide(); }

function addfields(link, association, content) {
var newid = new Date().getTime();
var regexp = new RegExp("new" + association, "g");
$(link).parent().before(content.replace(regexp, newid));
}

over 1 year ago ·