Create or Select a value via Nest Resources

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.

Post a Comment

Your email is never published nor shared. Required fields are marked *

You may use these HTML tags and attributes: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <s> <strike> <strong>

*
*

This site uses Akismet to reduce spam. Learn how your comment data is processed.