Awesome Nested Fields

In Rails, you can create forms that have fields from nested models. For example, if a person has many phone numbers, you can easily create a form that receives data from the person and from a fixed number of phones. However, when you want to allow the person to insert multiple, indefinite phones, you're in trouble: it's much harder than it should be. Well, not anymore.

Installation

Rails 3.1

  1. Add the gem to your Gemfile and run bundle install to make sure the gem gets installed.

    gem 'awesome_nested_fields'
    
  2. Add this line to app/assets/javascripts/application.js (or where you prefer) so the javascript dependency is added to the asset pipeline. Be sure to include this line after jQuery and jQuery UJS Adapter.

    //= require jquery.nested-fields
    
  3. Rock with your awesome nested models.

Rails 3.0

  1. Add the gem to your Gemfile and run bundle install to make sure the gem gets installed. Be sure to include it after jquery-rails so the javascript files are added in the correct order at the templates.

    gem 'awesome_nested_fields'
    
  2. Copy the javascript dependency to public/javascripts by using the generator.

    rails generate awesome_nested_fields:install
    
  3. (Optional) The javascript dependency will be added automatically to the defaults javascript files. If you don't use javascript_include_tag :defaults in your templates for some reason, require the file manually.

    <script src="/javascripts/jquery.nested-fields.js" type="text/javascript"></script>
    
  4. Now you're ready to rock with your awesome nested models. It will be so fun as in Rails 3.1, I promise.

Basic Usage

Model

First, make sure the object that has the has_many or has_and_belongs_to_many relation accepts nested attributes for the collection you want. For example, if a person has_many phones, we'll have a model like this:

class Person < ActiveRecord::Base
  has_many :phones
  accepts_nested_attributes_for :phones, allow_destroy: true
end

The accepts_nested_attributes_for is a method from Active Record that allows you to pass attributes of nested models directly to its parent, instead of instantiate each child object separately. In this case, Person gains a method called phones_attributes=, that accepts data for new and existing phones of a given person. The allow_destroy option enables us to also delete child objects. To know more about nested attributes, check out the ActiveRecord::NestedAttribute class.

View

The next step is set up the form view with the nested_fields_for method. It receives the association/collection name, an optional hash of options (humm, a pun) and a block with the nested fields. Proceeding with the person/phones example, we can have a form like this:

<%= form_for(@person) do |f| %>
  <% # person fields... %>

  <h2>Phones</h2>
  <div class="container">
    <%= f.nested_fields_for :phones do |f| %>
      <fieldset class="item">
        <%= f.label :number %>
        <%= f.text_field :number %>

        <a href="#" class="remove">remove</a>

        <%= f.hidden_field :id %>
        <%= f.hidden_field :_destroy %>
      </fieldset>
    <% end %>
  </div>
  <a href="#" class="add">add phone</a>

  <% # more person fields... %>
<% end %>

The nested_fields_for method lists the phones this person has and also adds an empty template to the page for creating new phones. (Actually, there is too much code inside the block. If you're not working with a simple example like this you better extract this code into a partial and call just render :phones inside the block. Good coding practices, you know.)

If you're paying attention, you noticed the key elements are marked with special class names. We need this for the javascript code, so it knows what to do with each HTML element: the one that have the children must have the class container; each child must be marked with the class item; inside an item, the link for removal must have the class remove; and the link to add new items must have the class add. We can change the names later, but these are the default choices. Finally, don't forget to add the id field, as it is needed by AR to identify whether this is an existing or a new element, and the _destroy field to activate deletion when the user clicks on the remove link.

Javascript

This is the easiest part: just activate the nested fields actions when the page loads. We can put this in the application.js file (or in any other place that gets executed in the page):

$(document).ready(function(e) {
  $('FORM').nestedFields();
});

Now enjoy your new nested model form!

Reference

View Options

There are some view options, but most are internal. There is just one you really need to know about; for the others, go to the code.

show_empty

Sometimes you want to show something when the collection is empty. Just set show_empty to true and prepare the block to receive nil when the collection is empty. Awesome nested fields will take care to show the empty message when there are no elements and remove it when one is added. To implement this on the basic example, do something like:

<%= f.nested_fields_for :phones, show_empty: true do |f| %>
  <% if f %>
    <% fields code... %>
  <% else %>
    <p class="empty">There are no phones.</p>
  <% end %>
<% end %>

And yeah, you need to mark it with the class empty or any other selector configured via javascript.

render_template

When nested_fields_for is called, it also includes a <script> tag with the html template of a new item, so the javascript code knows what to insert. But sometimes it is not possible to put the template just after the items. For example, you can be inside a table (tables cannot have script elements inside it) or have multi-level nested items (the templates would be recursively repeated). In these cases you need to render the template manually.

To do this, just set the render_template option to false and use the nested_fields_template helper to put the templates anywhere on the page.

<%= f.nested_fields_for :phones, render_template: false do |f| %>
  <% nested field code %>
<% end %>
<!-- some lines after -->
<%= nested_fields_templates %>

Keep in mind that you can call the templates only after nested_fields_for and inside the DOM element you apply the nestedFields() javascript, so it still can find the templates.

Javascript Options

Selectors

To make nested fields work dynamically, the JS code needs to know what elements to use. By default, this is made by marking key elements with CSS classes, but you can use other selectors (any valid jQuery selector will do). The available options are shown below.

  • itemSelector marks each item from the collection (.item by default)
  • containerSelector marks the element that contains the items (.container by default)
  • addSelector marks the element that will add a new item to the container when clicked (.add by default)
  • removeSelector marks the element inside an item that will remove it when clicked (.remove by default)
  • emptySelector marks the element that is shown when there are no items; used in conjunction with show_empty option (.empty by default)

For example, if you are using nested fields inside a table, you can do:

element.nestedFields({
  containerSelector: 'tbody',
  itemSelector: 'tr'
});

Callbacks

Actions can be executed before or after items get inserted or removed. There are four callbacks available: beforeInsert, afterInsert, beforeRemove and afterRemove. All of them receive the item as the first parameter, so you can query or modify it before the operation.

element.nestedFields({
  beforeInsert: function(item) {
    item.css('color', 'red'); // Make some operation
    console.log(item + ' will be inserted.')
  },
  afterRemove: function(item) {
    console.log(item + ' was removed.');
  }
});

The before callbacks also allow you to control when the element will be inserted or removed, so you can perform async operations (ajax, of course!) or choose to not insert or remove the element at all if some condition is not met. Just receive a second parameter as the handler function.

element.nestedFields({
  beforeInsert: function(item, insert) {
    $.get('/ajax_function', function() {
      insert();
    });
  }
});

Javascript API

It is possible to control nested fields programmatically using a jQuery-style API.

element.nestedFields('insert', function(item) {
  // Make some operation with item
}, {skipBefore: true});

The code above inserts a new item and does not execute the beforeInsert callback function. The complete list of available methods is shown below.

  • insert(callback, options) inserts a new item in the container. The callback function is executed just before the item is inserted. There are two available options: skipBefore and skipAfter. Both arguments are optional.
  • remove(element, options) removes element from the container. There are two available options: skipBefore and skipAfter. The last argument is optional.
  • removeAll(options) removes all elements from the container. There are two available options: skipBefore and skipAfter. The argument is optional.
  • items() returns a list of items on the container.
  • destroy() deactivates nested fields for the element.

These methods can be called from the element where nested fields are applied (e.g. a form) or from any element inside it (e.g. an input or the container itself).

Multiple Nested Fields

It is easy to have multiple nested fields on the same page. Instead of applying nestedFields() to the form, put the elements (items, container, add, remove) inside a wrapper and apply nested fields to it.

<!-- ERB Code -->
<h2>Phones</h2>
<div id="phones">
  <div class="container">
    <%= f.nested_fields_for :phones do |f| %>
      <% ... %>
    <% end %>
  </div>
  <a href="#" class="add">add phone</a>
</div>

<h2>Addresses</h2>
<div id="addresses">
  <div class="container">
    <%= f.nested_fields_for :addresses do |f| %>
      <% ... %>
    <% end %>
  </div>
  <a href="#" class="add">add address</a>
</div>

// JS Code
$('#phones, #addresses').nestedFields();

Demo

There is a live demo at http://phonebook.guava.com.br/.
You can find the demo code at https://github.com/lailsonbm/awesome_nested_fields_demo.

Compatibility

awesome_nested_fields works only with jQuery and Rails 3.x. Sorry, Rails 2.x users.

TODO

  • Write tests
  • Make sure it can degrade gracefully
  • Implement jQuery autoload
  • Make nested_fields_for works without a block (looking for partials)

Copyleft

Copyleft (c) 2011 Lailson Bandeira (http://lailsonbandeira.com/). See LICENSE for details.