Rails Forms microformat

This article has been updated to reflect the latest patterns in Rails 2.3 edge (based mostly on this commit)

If you’ve been relying on Rails form helpers to generate forms, then you may have missed the interesting little microformat used to pass application data to and fro. In case you didn’t know, form data is passed as part of the request body as a set of key/values pairs in plain text (if you’re using get as a method for a form, it’s that url section like this: ?name=widget12&price=22). The name attribute of the form inputs are the keys (here name and price), and the value is whatever the user entered or selected (widget12 and 22).

Most languages/frameworks for the web will reconstitute these pairs as objects accessible to the programmer.

For example

<input name='widget_name' />

is accessed with $_POST["widget_name"] in php, self.request.get("widget_name") on App Engine, and params[:widget_name] in Rails.

This format can only pass a single value for a key, so it can’t represent complex data structures like hashes and arrays. With single values in Rails, our controllers would be like this:

def create
  @widget = Widget.new
  @widget.name  = params[:name]
  @widget.price = params[:price]
  @widget.save!
rescue ActiveRecord::RecordInvalid
  self.render('new') # Rails 2.3 version of self.render({:action => 'new'})
end

We’d have one @widget.some_name = params[:some_name] per attribute we wanted to pass. As far as I can tell, this is what programmers in PHP/App Engine/etc mostly do. In Rails, however, we typically use

def create
  @widget = Widget.new(params[:widget])
  ...
end

and Rails auto assigns attributes correctly for us. If you’ve never examined the output of your helpers you may not know how this happens. Although we can only send key=value over the tubes, Rails is designed to parse this string looking for specific formatting and convert certain patterns into more complex data strictures. Basically, it’s a microfrmat for form inputs.

This parsing is handled by a rack middleware ActionController::ParamsParser in Rails 2.3+, so it’s a trick that could technically be available to any rack-based ruby application.

Having a hash accessible via params[:widget] occurs because all of the inputs for a widget are named with the format "widget[some_attr]". When Rails sees multiple inputs with names in this pattern, it sticks them all into a single data structure.

So if

<input name='name' />
<input name='price' />

becomes

# params
{
  :name => 'widget12,
  :price => '22'
}

then

<input name='widget[name]'  />
<input name='widget[price]' />
<input name='somethingelse[attr1]' />
<input name='somethingelse[attr2]' />

is converted to

# params
{
  :widget => {:name => 'widget12, :price => '22'},
  :somethingelse => {:attr1 => '', :attr2 => ''}
}

Even with the text-to-hash parsing we could still do data assignment long-hand

def create
  @widget = Widget.new
  @widget.name  = params[:widget][:name]
  @widget.price = params[:widget][:price]
  ...
end

However, since ActiveRecord::Base.new takes an optional hash argument we can shorten it to Widget.new(params[:widget])

That’s nifty, but Rails Forms (rForms maybe?) can do much more. Take a gander at these patterns:

Nested Hashes

<!-- form.html -->
<input name='foo[bar][attr1]' value='A'>
<input name='foo[bar][attr2]' value='B'>

# FoosController#some_action
# params
{:foo => {
    :bar => {
      :attr1 => 'A', :attr2 => 'B'
    }
  }
}

Deeply Nested Hashes

<!-- form.html -->
<input name='foo[qux]' value='I am the qux'>
<input name='foo[bar][baz][attr1]' value='A'>
<input name='foo[bar][baz][attr2]' value='B'>

# FoosController#some_action
# params
{:foo => {
    :qux => "I am the qux",
    :bar => {
       :baz =>
          :attr1 => 'A', :attr2 => 'B'
       }
    }
  }
}

Arrays

If you have [] without text inside of it as the final characters of the name attribute, you’ll get an array on the other end:

<!-- form.html -->
<input name='foo[baz]', value='the baz' />
<input name='foo[many_bars][]' value='bar 3' />
<input name='foo[many_bars][]' value='bar 2' />
<input name='foo[many_bars][]' value='bar 1' />
<input name='foo[many_bars][]' value='bar none' />

# FoosController#some_action
# params
{:foo => {
    :baz => "the baz",
    :many_bars => ["bar 3","bar 2","bar 1", "bar none"]
}

Arrays in a hash

<!-- form.html -->
<input name='foo[qux][corge]'      value='the corge' />
<input name='foo[qux][graults][]'  value='grault 1' />
<input name='foo[qux][graults][]'  value='grault 2' />
<input name='foo[qux][graults][]'  value='grault 3' />

# FoosController#some_action
# params
{:foo => {
    :qux => {
      :corge => 'the corge',
      :graults => ["grault 1", "grault 2", "grault 3"]
    }
}

Using It

Now that you’ve seen the the basics of structuring the format, how can we use it to do complex data manipulations in the UI and keep the same tiny @object = ClassName.new(params[:something]) or @object = ClassName.find(params[:id]).update_attributes!(params[:something]) snippets in our controller? Let’s examine belongs_to as an exemplar:

belongs_to

Assuming the following classes

class Widget < ActiveRecord::Base
  belongs_to :creator
end

class Creator < ActiveRecord::Base
  has_many :widgets
end

The simplest way to create a Widget and assign a creator is with a form like this:

<!-- form.html -->
<input name='widget[name]' >
<input name='widget[price]' >
<input name='widget[creator_id]' >

# WidgetsController#create
# params
{:widget =>
  {
    :name => 'widget 10',
    :price => '22',
    :creator_id => '971'
  }
}

And a single form can do attribute assignment and object association (nb: just for demonstration purposes. Doing this with nested routes and calls to Creator#.widgets.build(params[:widget]) is probably more idiomatic and secure).

create new object, new belongs_to

But that only works for an existing Creator object (we need the id). What if we wanted to make a new Widget and associate a new creator all at once? The first step is to tell our model we’ll be expecting nested attributes for assignment. We do this by adding accepts_nested_attributes_for(:relationship_name) where :relationship_name is the name of our relationship (in Rails 2.3 and above).

Go ahead and change

class Widget < ActiveRecord::Base
  belongs_to :creator
end

to

class Widget < ActiveRecord::Base
  belongs_to :creator
  accepts_nested_attributes_for :creator
end

Now our Widget objects have a method creator_attributes (the pattern here is relationship_name_attributes). This method takes a hash and will loop through the hash’s key, assigning values to a nested object. We know that, from the examples above, we can create nested hashes with input names like foo[bar][baz].

Our inputs for single-form widget/creator making will look like this:

<!-- form.html -->
<input name='widget[name]' >
<input name='widget[price]' >

<input name='widget[creator_attributes][name]' >
<input name='widget[creator_attributes][height]' >

# WidgetsController#create
# params
{:widget =>
  {
    :name => 'widget 10',
    :price => '22',
    :creator_attributes => {
      :name => 'John McInventorson',
      :height => '121'
    }
  }
}

Passing params[:widget] to a Widget.create will create both a new Widget object, a new Creator object, and will associate the two.

update existing object, update existing belongs_to

However, if we use that same form later to do an update for the Widget and the creator, we will overwrite the original creator object. For example:

# script/console
params = {
  :id => 4,
  :widget => {
    :name => 'widget 10',
    :price => '22',
    :creator_attributes => {
      :name => 'John McInventorson',
      :height => '121'
    }
  }
}
@widget = Widget.find(params[:id])
@widget.creator.id # => 12
@widget.update_attributes!(params[:widget])
@widget.creator.id # => 92 # OVERWRITTEN!

Why is this? Part of the difficult in doing multiple objects in a single form is signaling intent. When you send _attribute data along with attributes of some base object, which of the following intents does that signal to application?

  • create a new associated object
  • update an existing associated object, creating one if it doesn’t exist

It can only have a single meaning; Rails chooses, in this case, to create and associate a new related object. We can signal our intent to update the attributes of the existing associated object by including its id attribute in the _attributes hash.

    <!-- form.html -->
    <input name='widget[name]' >
    <input name='widget[price]' >

    <input name='widget[creator_attributes][id]' >
    <input name='widget[creator_attributes][name]' >
    <input name='widget[creator_attributes][height]' >

    # WidgetsController#update
    # params
    {:widget =>
      {
        :name => 'widget 10',
        :price => '22',
        :creator_attributes => {
          :id => 12,
          :name => 'John McInventorson',
          :height => '121'
        }
      }
    }
    @widget = Widget.find(params[:id])
    @widget.creator.id # => 12
    @widget.update_attributes!(params[:widget])
    @widget.creator.id # => 12. UPDATED!

update an existing object, remove existing belongs_to

Next question: when we don’t send related _attribute data, which of the following intents does that signal?

  • don’t update the existing associated object.
  • remove the associated object from the database.
  • remove the association, but leave the related object in the database.

Again: there can be only one. In Rails if we don’t included any nested _attributes data it means we’re simply uninterested in the associated object. It will be left in the database, untouched. To delete an associated object, Rails Forms use a special '_delete form input whose value should be anything that evaluates to true coming from a form (that’d be '1', 1, 'true', and true).

    <!-- form.html -->
    <input name='widget[name]' >
    <input name='widget[price]' >

    <input name='widget[creator_attributes][id]' >
    <input name='widget[creator_attributes][_delete]' >

    # WidgetsController#update
    # params
    {:widget =>
      {
        :name => 'widget 10',
        :price => '22',
        :creator_attributes => {
          :id => 12,
          :_delete => '1'
        }
      }
    }

A few notes:

  • For this to work, you must add :allow_destroy => true to your accepts_nested_attributes_for call. This is :false by default and delete requests are silently ignored. In our application, this will be accepts_nested_attributes_for(:creator, :allow_destroy => true).
  • This removes both the association and the associated object from the database. Misusing forms can cost you data. Make sure they do what you intend (test, test, test!).

If you want to remove the association but leave the associated object (useful if that other object is needed elsewhere) send an input with the foreign key set to an empty string:

    <!-- form.html -->
    <input name='widget[name]' >
    <input name='widget[price]' >

    <input name='widget[creator_id]' value=''>

    # WidgetsController#create
    # params
    {:widget =>
      {
        :name => 'widget 10',
        :price => '22',
        :creator_id => ''
      }
    }

From the belongs_to example, we can distill some rules for nested associated objects:

  • Associated object data is sent in a nested *_attributes hash where * is the name of your relationship (e.g. belongs_to(:dog)/dog_attributes, belongs_to(:foo)/foo_attributes).
  • If the hash does not include an id, Rails will create and associate a new object (leaving the old object, if there is one, in the system, unassociated).
  • If the hash includes an id, Rails will update the existing associated object.
  • If the hash includes an id and a _delete set to something truthy the object will be deleted from the database and the association will be removed.
  • If you want to remove the relationship but not the object, set the foreign key (here creator_id) to '' in the form.

has_one or composed_of

We can reuse these patterns for the opposite side of a 1-to-1 data relationship (has_one or composed_of).

Assuming the classes

class Addresss < ActiveRecord::Base
  belongs_to :creator
end

class Creator < ActiveRecord::Base
  has_one :address
  accepts_nested_attributes_for :address
end

creating a new object, new has_one object

We can handle multiple models in one form with

 <!-- form.html -->
<input name="creator[name]" />
<input name="creator[height]" />
<input name="creator[address_attributes][street1]" />
<input name="creator[address_attributes][street2]" />
<input name="creator[address_attributes][city]" />

# CreatorsController#create
# params
{:creator =>
  {
    :name => 'James McInventorson',
    :height => '133',
    :address_attributes => {
      :street1 => '123 Main Street'
      :street2 => 'Office 5b'
      :city    => 'Anywhereville'
    }
  }
}

This will create a new Creator object, create a new Addresss object, and associate the two.

updating and existing object, updating existing has_one

The same caveats for belongs_to exists here. Updating with the same form will overwrite the existing address with a new one. So

# script/console
params = {
 :id => 4,
 :creator => {
     :name => 'James McInventorson',
     :height => '133',
     :address_attributes => {
       :street1 => '123 Main Street'
       :street2 => 'Office 5b'
       :city    => 'Anywhereville'
     }
   }
 }
@creator = Creator.find(params[:id])
@creator.address.id # => 1012
@creator.update_attributes!(params[:creator])
@creator.creator.id # => 3101 # OVERWRITTEN!

We signal to Rails that the current address should be update by including the id of the address in the hash

 <!-- form.html -->
<input name="creator[name]" />
<input name="creator[height]" />
<input name="creator[address_attributes][id]" />
<input name="creator[address_attributes][street1]" />
<input name="creator[address_attributes][street2]" />
<input name="creator[address_attributes][city]" />

# CreatorsController#update
# params
{:creator =>
  {
    :name => 'James McInventorson',
    :height => '133',
    :address_attributes => {
      :id => 1012,
      :street1 => '123 Main Street'
      :street2 => 'Office 5b'
      :city    => 'Anywhereville'
    }
  }
}

updating existing object, deleting existing has_one

Same rules as belongs_to:

<input name="creator[name]" />
<input name="creator[height]" />
<input name="creator[address_attributes][id]" />
<input name="creator[address_attributes][_delete]" />

# CreatorsController#update
# params
{:creator =>
  {
    :name => 'James McInventorson',
    :height => '133',
    :address_attributes => {
      :id => 1012,
      :_delete => '1'
    }
  }
}

From these examples we can distill some rules:

  • Associated object data is sent in a nested *_attributes hash where * is the name of our association (e.g. has_one(:baz)/baz_attributes, has_one(:bar)/bar_attributes).
  • If the hash does not include an id, Rails will create and associate a new object (leaving the old object, if there is one, in the system, unassociated).
  • If the hash includes an id, Rails will update the existing associated object.
  • If the hash includes an id and a _delete set to something truthy the object will be deleted from the database and the association will be removed.

has_many

We can do basic has_many associating by submitting a form like the one below (probably with some javascript that inserts hidden inputs)

 <!-- form.html -->
<input name='creator[name]' >
<input name='creator[height]' >
<input name='creator[widgets_ids][]' >
<input name='creator[widgets_ids][]' >
<input name='creator[widgets_ids][]' >
<input name='creator[widgets_ids][]' >

# CreatorsController#create
# params
{:creator =>
  {
    :name => 'James McInventorson',
    :height => '133',
    :widgets_ids => ["1","45","1323","1231"]
  }
}

That will call the Creator#widgets_ids= method that you get with the has_many relationship and associate the widgets in the same call that assigns the name and height attributes of the creator. Be aware that widgets_ids=(ary) overwrites the previous relationships (so if you have widgets_ids of [10,40,51] and would like to add 87 to that list, you’ll have to send [10,40,51,87], not just [87]). It will look like this.

    <!-- form.html -->
    <input name="creator[name]" />
    <input name="creator[height]" />
    <input name="creator[widget_ids][]" value='10' />
    <input name="creator[widget_ids][]" value='40' />
    <input name="creator[widget_ids][]" value='51' />
    <input name="creator[widget_ids][]" value='87' />

complex has_many

Expressing data manipulation of complex has_many relationships is quite tricky.
Imagine you want to have a single form where a new Creator and his widgets can be entered at once, but also, at a later date, that same form could be used to update existing widgets, add new widgets, or delete old widgets.

That’s a tall order. To do it we need to be able to tell

  • which widgets are being updated
  • which widgets are being added
  • which widgets should be removed (does the lack of a widget on the form indicate we want to remove its association to this creator or remove its entry from the database entirely? Or, does it mean we’re just not interested in manipulating its data right now?)

creating a new object, and new to_many relationships

To add a new to_many relationship in a single submission the microformat uses nested hashes:

 <!-- form.html -->
<input name="creator[name]" />
<input name="creator[height]" />
<input name="creator[widget_attributes][0][name]" />
<input name="creator[widget_attributes][0][price]" />

# CreatorsController#create
# params
{:creator =>
  {
    :name => 'James McInventorson',
    :height => '133',
    :widget_attributes => {
        :0 => {
          :name => 'Basic Confabulator',
          :price => '19'
        }
    }
  }
}

Let’s break down these naming rules for new objects:

creator[widget_attributes][0][price]
  • the outermost key (creator here) can be called anything you like, and will be accessed with params[:creator] (or whatever you called it).
  • widget_attributes follows the pattern of has_many_relationship_name_attributes. So, if your relationship was has_many :dogs, you must use dogs_attributes, has_many :foos uses foos_attributes, has_many :flavors will be flavors_attributes.
  • widgets_attributes will be a hash, so it must be followed by brackets containing a unique identifier. Here we’ve used [0]. Because this identifier must be unique on the form (otherwise the hashes will overwrite each other), if we wanted a form that added two widgets at once we’d need creator[widget_attributes][0] and creator[widget_attributes][foobar] (the exact text doesn’t matter as long as it’s unique).

updating an existing object and existing to_many relationships

To update an existing has_many related object, include its id in the _attributes hash:

    <!-- form.html -->
    <input name="creator[name]" />
    <input name="creator[height]" />

    <input name="creator[widget_attributes][0][id]" />
    <input name="creator[widget_attributes][0][name]" />
    <input name="creator[widget_attributes][0][price]" />

    <input name="creator[widget_attributes][1][id]" />
    <input name="creator[widget_attributes][1][name]" />
    <input name="creator[widget_attributes][1][price]" />

    # CreatorsController#create
    # params
    {:creator =>
      {
        :name => 'James McInventorson',
        :height => '133',
        :widget_attributes => {
            0 => {
              :id => 459,
              :name => 'Advanced Confabulator',
              :price => '23.00'
            },
            1 => {
              :id => 231,
              :name => 'Ectoplasm Inducer',
              :price => '1223.00'
            }
        }
      }
    }

There are some notable features to recognize:

  • Like other relationships, if you have _attributes[n] hash that does not include an id, a new object will be created and associated. Because this is has_many relationship, the number of related objects will be incremented by 1.
  • To create new associations to existing objects use the array format to add the existing objects’ ids to Creator#widgets_ids with inputs names like creator[widgets_ids][]. Again: Be aware that widgets_ids=(ary) overwrites the previous relationships (so if you have widgets_ids of [10,40,51] and would like to add 87 to that list, you’ll have to send [10,40,51,87], not just [87]). It will look like this:
<!-- form.html -->
<input name="creator[name]" />
<input name="creator[height]" />
<input name="creator[widget_ids][]" value='10' />
<input name="creator[widget_ids][]" value='40' />
<input name="creator[widget_ids][]" value='51' />
<input name="creator[widget_ids][]" value='87' />

Removing a existing to_many relationship

To remove an existing has_many related item, include _delete inside its unique hash. The value should be set to anything that will evaluate to true coming form a form (that’s "1", 1, "true", and true).

<!-- form.html -->
<input name="creator[name]" />
<input name="creator[height]" />

<input name="creator[widget_attributes][0][id]" value='1'/>
<input name="creator[widget_attributes][0][_delete]" value='1'/>

{:creator =>
  {
    :name => 'James McInventorson',
    :height => '133',
    :widget_attributes => {
        0 => {:id => 22, :_delete => '1'}
    }
  }
}

Some notes:

  • Deleting this way is turned off by default. You enable it in the model by adding :allow_destroy => true to accepts_nested_attributes_for (e.g. accepts_nested_attributes_for(:widgets, :allow_destroy => true))
  • This method deletes the association and associated object. To delete only the association, send the updated _ids attribute with the ids you don’t want removed: if you have widgets_ids of [10,40,51] and would like to remove 10 and 40 from that list send [51].
<!-- BEFORE: form.html -->
<input name="creator[name]" />
<input name="creator[height]" />
<input name="creator[widget_ids][]" value='10' />
<input name="creator[widget_ids][]" value='40' />
<input name="creator[widget_ids][]" value='51' />

<!-- AFTER: form.html -->
<input name="creator[name]" />
<input name="creator[height]" />
<input name="creator[widget_ids][]" value='51' />

All together now!

Combining these techniques can net us some very sophisticated data manipulations. Below is a form that will update a Creator, his address, updates three existing widgets, adds one widget, and deletes two widgets.

Here’s the form on page-load, before we manipulate it:

<!-- BEFORE: form.html -- >
<input name="creator[name]" />
<input name="creator[height]" />

<!-- address -->
<input name="creator[address][id]" />
<input name="creator[address][street1]" />
<input name="creator[address][street2]" />
<input name="creator[address][city]" />

<!-- widgets -->
<input name="creator[widget_attributes][0][id]" />
<input name="creator[widget_attributes][0][name]" />
<input name="creator[widget_attributes][0][price]" />

<input name="creator[widget_attributes][1][id]" />
<input name="creator[widget_attributes][1][name]" />
<input name="creator[widget_attributes][1][price]" />

<input name="creator[widget_attributes][2][id]" />
<input name="creator[widget_attributes][2][name]" />
<input name="creator[widget_attributes][2][price]" />

<input name="creator[widget_attributes][3][id]" />
<input name="creator[widget_attributes][3][name]" />
<input name="creator[widget_attributes][3][price]" />

<input name="creator[widget_attributes][4][id]" />
<input name="creator[widget_attributes][4][name]" />
<input name="creator[widget_attributes][4][price]" />

and here is the form after manipulation.

<!-- AFTER: form.html -- >
<input name="creator[name]" />
<input name="creator[height]" />

<!-- address -->
<input name="creator[address][id]" />
<input name="creator[address][street1]" />
<input name="creator[address][street2]" />
<input name="creator[address][city]" />

<!-- widgets -->
<input name="creator[widget_attributes][0][id]" />
<input name="creator[widget_attributes][0][name]" />
<input name="creator[widget_attributes][0][price]" />

<input name="creator[widget_attributes][1][id]" />
<input name="creator[widget_attributes][1][name]" />
<input name="creator[widget_attributes][1][price]" />

<input name="creator[widget_attributes][2][id]" />
<input name="creator[widget_attributes][2][name]" />
<input name="creator[widget_attributes][2][price]" />

<input name="creator[widget_attributes][3][id]" />
<input name="creator[widget_attributes][3][_delete]" value='1'/>

<input name="creator[widget_attributes][4][id]" />
<input name="creator[widget_attributes][4][_delete]"  value='1'/>

<input name="creator[widget_attributes][new_1][name]" />
<input name="creator[widget_attributes][new_1][price]" />

About this entry