I am currently working on a small project and intend to do an auto-complete for a field. To make it friendly, it’d be great if a user could choose an existing value via the auto-complete, or have a new value saved. For instance, if you have say, our friend the blog, and that has Post which belongs_to Category. My goal is a text field for category (as opposed to the usual select) that will allow both an existing value, or a new one.
I also wanted it to be safe. My first attempts at this ended rather poorly, and just didn’t feel right. I tried both some madness on the models, and some transactions in the controller when saving. The transactions do work, but you wind up with a lot of book keeping for such a small thing.
The approach I settled on (as you will see) is using nested attributes, but in reverse. All the examples I found assumed you wanted the has_many end of the relation in the form. Some time in irb later, I nailed it.
I am not going to run through all the steps, but assume you’ve set up a rails project and have the models Post and Category. (See the github repo for a runnable example.)
Our model code, for Post:
# title =>string # contents => text # category_id => integer class Post < ActiveRecord::Base validates :category, :presence => true belongs_to :category accepts_nested_attributes_for :category end
And for the Category:
# name => string class Category < ActiveRecord::Base validates :name, :presence => true, :uniqueness => true has_many :posts end
That done, let us move to our controller. This is MIGHTY stripped down of course – I am only including enough to show you what’s what.
The Post controller:
class PostsController < ApplicationController respond_to :html def show @posts = Post.find params[:id] end def new @post = Post.new @post.category = Category.new end def create category = Category.find_by_name params['post']['category_attributes']['category'] if category params['post']['category_id'] = category.id params['post'].delete 'category_attributes' end @post = Post.new params['post'] @post.save respond_with(@post) end end
Aside from some Rails 3 goodness, what’s going on here you need to know?
The new action sets up the Post object, and builds out the nested resource, the category. You can’t use Post.category.build as the category is still nil.
The real magic is in the create action.
category = Category.find_by_name params['post']['category_attributes']['category']
This finds the category by name if it already exists.
if category params['post']['category_id'] = category.id params['post'].delete 'category_attributes' end
If we find the existing category, we assign it to the params hash via its id. This wires it in as if we’d done a select or something. To avoid trying to re-create the category, we delete the nested attributes from the parameters.
If the category doesn’t exist, the params with the nested attributes fall through, and the category gets created along side the Post.
In the view, we write up the nested attributes.
<%= form_for @post do |f| > <% f.fields_for :category do |c_f| %> <p><%= f.label :title %> <%= f.text_field :title %></p> <p><%= c_f.label :category %><%= c_f.text_field :name %></p> <p><%= f.submit 'Submit' %></p> <% end %> <% end %>
Viola! Again, if you want to play with a small working example on github.