Rails nested forms with AJAX add and remove

If you’ve found this, no doubt you’ve discovered that there are many solutions to the issue of nested forms in Rails. This is one of those many solutions. The highlights include:

  • supports unlimited nesting levels
  • uses (and requires) accepts_nested_attributes_for; no special model code required
  • new nested objects are instantiated and saved to the database before being exposed in the view
  • adding and removing nested forms happens dynamically in the user interface
  • jQuery UI is used for the JavaScript stuff (though it could be easily re-written to use raw Javascript or another library)

One level of nesting

Let’s start by getting one level of nesting working. I’ll refer to the “parent” model (the one that has_many somethings) and the “child” model (the one that belongs_to the parent). If I use the term “parents” or “children,” I will sometimes want you to specify the plural. This should be obvious by the context.

Set up the model

Give the Parent the usual has_many declaration and the accepts_nested_attributes_for declaration. The latter allows us to add, edit, and delete children “inside” a parent update. (See the API doc.)

  has_many :children
  accepts_nested_attributes_for :children

It’s worth noting that we do not need the :allow_destroy option set on accepts_nested_attributes_for. This is because we will do our own destroys dynamically, and don’t need the method provided by ANAF.

Give the Child model the usual belongs_to declaration, and that’s it for the model.

Give your routes a REST

Be sure you have a route something like the following in routes.rb. The important thing is that you have a route with the parent at the root, having many children.

map.resources :parents, :has_many => :children

Step 2 – controllers

The Parent controller needs no changes at all. It simply needs to do the usual actions to retrieve a Parent object for rendering.

A little AJAX in the Children controller

We need to have our Children controller respond to AJAX requests to add and remove children from the database. I’ll start by showing the code:

# GET /parents/1/children/new (AJAX)
def new
  parent = Parent.find(params[:parent_id])
  @child = parent.children.create
  new_child_form = render_to_string :layout => false
  new_child_form.gsub!("[#{@child.id}]", "[#{Time.now.to_i}]")
  render :text => new_child_form, :layout => false
end

# DELETE /parents/1/children/1 (AJAX)
def destroy
  parent = Parent.find(params[:parent_id])
  unless parent.children.exists?(params[:id])
    render :text => { :success => false, :msg => 'the child was not found.' }.to_json and return
  end
  if parent.children.destroy(params[:id]) // Rails < 2.3.5, if parent.children.destroy(Child.find(params[:id]))
render :text => { :success => true }.to_json
  else
    render :text => { :success => false, :msg => 'something unexpected happened.' }.to_json
  end
end
  • note that this is an AJAX-only controller; I assume that I will always create and delete children via AJAX (and edit them via nested edits from their parent)
  • the new action does two main things: 1) creates a new child object (saving it to the database) and 2) creates a chunk of HTML to send back to the browser to render the nested form for the new child
  • render_to_string renders new.html.erb (see below) to a string so that we can manipulate it in the next statement
  • when Rails renders new.html.erb, it inserts @child.id as the index of children_attributes; not sure why, but I guess since the view is not being rendered in the context of a FormBuilder, it decides that’s the best thing to use as the index; but…
  • …we need a real index, and it needs to be a number greater than a) the indices that Rails has created for children already in the database, and b) any indices that we’ve generate previously on this form
  • so we use Time.now.to_i, which will always be a very big number and, since time waits for no one, will always be larger than the last time we used it
  • the delete action is simpler, and returns a JSON-formatted response to the caller

For your viewing pleasure

Since we just talked about rendering the HTML fragment for the new child record we created, let’s have a look at the view that is rendered (new.html.erb):

<% fields_for 'parent[children_attributes][]', @child do |child_form| %>
 <%= render :partial => '/children/form', :locals => { :f => child_form, :child => @child } %>
<% end %>

Must you render a partial in this view? No, but since you want a newly added child form to look just like one that was rendered the old-fashioned way in the first place, using a partial will keep your views DRY.

What does the partial look like? Like any old partial, with a few mandatory elements to enable our AJAX methods:

<%#
locals:
  f - form context
  child - Child to be displayed
%>

<div class='child' id='<%= child.id %>'>
  <h3>Child</h3>
  <%= f.hidden_field :id %>
  ...
  <p><a href='#' class='child-remove'>Remove the child above</a></p>
</div>

The important thing here are the classes and ids. They are mandatory, and must be specified exactly as shown here.

Careful readers have noted that we’ve set ourselves up here to delete any child, but what about adding children? Well, that is done in the parent view, right? Because while we need a delete on every child record, we only need a single add link. Here’s the relevant chunk of the parent view:

...
<div class='children' id='parent-<%= @parent.id %>'>
  <h2>Children</h2>
  <% parent_form.fields_for :children do |child_form| %>
    <%= render :partial => '/children/form', :locals => { :f => child_form, :child => child_form.object } %>
  <% end %>
  <div id='new-children'>
  </div>
  <p><a href='#' class='child-add'>add a child</a></p>
</div>
...

Pretty straightforward. Again, the important thing here are the classes and ids. Also, note the “new-children” div: it too is required.

Tying it all together

So what ties all this together? A bit of JavaScript, of course. In your parent form, add this at the bottom:

<script>
$(document).ready(function() {
 dynamicAddRemove('parent', 'parents', 'child', 'children');
});
</script>

This sets up the appropriate handlers, calling dynamicAddRemove and telling it that “parent” is the parent object and “child” the child object. (Since JavaScript doesn’t have (direct) access to Rails’ inflectors, I specify both the singular and plural forms as arguments to dynamicAddRemove. There are several possible solutions to this admittedly ugly construct, which are left as exercises for the reader.)

Which brings us to the final piece of this solution: the JavaScript that actually performs the adds and removes. Stick this in your public/javascripts/application.js file:

function dynamicAddRemove(parent, parents, child, children) {
  $('a.' + child + '-add').live('click', function() {
    var parentId = $(this).closest('.' + children).attr('id').replace(parent + '-', '');
    indexData = ''
    // check to see if there is a parent div; this will be the case if we're doing a
    // grandchild (or deeper) insert
    if ($(this).closest('div.' + parent).size() > 0) {
      var matcher = new RegExp (parents + '_attributes\\]\\[(\\d*)\\]');
      var parentIndex = $(this).closest('div.' + parent).find(':input').attr('name').match(matcher)[1];
      indexData = 'index=' + parentIndex;
    }
    $.get('/' + parents + '/' + parentId + '/' + children + '/new', indexData, function(data) {
      $(data).hide().appendTo($('div.' + children + '#' + parent + '-' + parentId + ' div#new-' + children)).fadeIn('slow');
    });
    return false;
  });

  $('a.' + child + '-remove').live('click', function() {
    var childId = $(this).closest('.' + child).attr('id');
    var parentId = $(this).closest('.' + children).attr('id').replace(parent + '-', '');
    $.post('/' + parents + '/' + parentId + '/' + children + '/' + childId, { _method: 'delete' }, function(data, textStatus) {
      var response = JSON.parse(data);
      if (response.success) {
        $('div.' + child + '#' + childId).fadeOut('slow', function() {
          $(this).remove();
        });
      } else {
        alert('The ' + child + ' could not be removed because ' + response.msg);
      }
    });
    return false;
  });
}

That’s all there is to it.

Cheat sheet

Here’s a cheat sheet to help you get all the divs, classes, and ids set up correctly.

(in the parent edit view)

<div class='children' id='parent-nnn'>
<div id='new-children'>
  </div>
  <p><a href='#' class='child-add'>add a child</a></p>
</div>

(in the child partial)

<div class='child' id='nnn'>
  <a href='#' class='child-remove' />
</div>

Of course, the “nnn”s are replace by the id of the appropriate record, as in @parent.id and @child.id.

More layers of nesting

And what about another layer of nesting, as promised?

It’s rather simple, really. Just repeat the above instructions, changing “parent” and “child” appropriately. IOW, if you’re adding a “lower” level, your parent will now be the child from the level above.

When you add the call to dynamicAddRemove, put that bit of code in the original parent view. IOW, all of the calls to dynamicAddRemove will wind up in one place: the highest level view in your hierarchy.

There are two critical differences, one in the child new.html.erb view and one in the child controller.

We need to specify the appropriately nested “path” to the child record we’re maintaining. So, for the next level down, our “grandchild” new.html.erb would look like this:

<% fields_for "grandparent[parent_attributes][#{@parent_index}][child_attributes][]", @child do |child_form| %>
...

And where does @parent_index come from? Well, we need one more line added to the child controller:

@parent_index = params[:index]

Insert it right before the render_to_string.

You can no doubt see how to extend this to greater levels of nesting. Note that if you add a level, you’ll have to edit the JavaScript in application.js and find the “grandparent index” and pass it along to the controller in the add portion of the script.

Credit where credit can be remembered

In arriving at this solution, I looked at many, many solutions; many of which I can no longer find or remember. Certainly a shout out to Ryan Bates for this and this. And thanks to Geof Dagley for guidance.

If I should credit you too, let me know.

Conclusion

Enjoy, and let me know if this works for you. Have a better way?…let me know.

Advertisements

Tags: ,

6 Responses to “Rails nested forms with AJAX add and remove”

  1. Michael Klem Says:

    Just what I was looking for! Thanks

  2. dynamick Says:

    I wrote an article for an ajaxed nested form implementation in rails 3… it could be interesting…
    http://www.dynamick.it/rails-3-ajaxed-nested-form-4721.html

  3. gouravtiwari21 Says:

    very nice trick, still works!

  4. Mike Says:

    Rails 4 routes look like this:

    resources :parents do
    resources :children
    end

  5. Rodolfo Medrano Says:

    I am going to try this good solution to my case. Just after reading it, I came up with a doubt. What happen with the nested object when it is first saved in the DB before rendering. What happen with the possible validations it might have?

    • Steve Crozier Says:

      Rodolfo, this is pretty old now, so I probably need to give you a disclaimer that it hasn’t been tried in Rails 3 or 4! That said, I think that validations are handled gracefully, but honestly, I can’t guarantee that in modern versions of Rails.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s


%d bloggers like this: