Rails auto_complete nested list
Yesterday I was implementing auto completion for categories. The idea was to get possibility to write by hand categories but keep them as has_many_through association in database.
After six hours of coding I got this working - that is why I’m against rails, with such big amount of plugins, gems and blogs you have to spent a lot of time to do small things.
This code works with rails 2.3
So first we have to install auto_complete plugin which was quite handy:
script/plugin install auto_complete script/plugin discover
Second we need to define migration:
create_table :book_categories do |t|
t.references :books
t.references :categories
end
create_table :books do |t|
t.string :title
end
create_table :categories do |t|
t.string :name
end
At third step we define models:
class Book < ActiveRecord::Base has_many :book_categories, :dependent => :destroy has_many :categories, :through => :book_categories end class BookCategory < ActiveRecord::Base belongs_to :book belongs_to :category end class Category < ActiveRecord::Base has_many :book_categories, :dependent => :destroy has_many :books, :through => :book_categories end
Time for forth step - faking Book properties:
class Book < ActiveRecord::Base
has_many :book_categories, :dependent => :destroy
has_many :categories, :through => :book_categories
def categories_list
self.categories.map{|c| c.name}*”, ”
enddef categories_list=(list)
list_names = list.split(’,').map{ |e| e.strip }.uniq
list_existings = Category.find(:all, :conditions => [ 'name IN (?)', list_names ], :select => ‘id,name’ )
list_existings_names = list_existings.map { |e| e.name }
list_add = list_names-list_existings_names
list_new = list_existings.map { |e| e.id }
list_old = self.categories.map { |e| e.id }.uniq
list_add.each do |name|
self.categories << Category.new(:name => name)
end
self.categories << Category.find(:all, :conditions => [ 'id IN (?)', list_new-list_old ], :select => ‘id’ )
self.categories.delete Category.find(:all, :conditions => [ 'id IN (?)', list_old-list_new ], :select => ‘id’ )
end
end
Now we can have some rest from coding and play a bit with new models using “script/console”:
>> Category.new(:name => 'a').save
>> Category.new(:name => 'b').save
>> Category.new(:name => 'c').save
>> Category.all.map{|c| [c.id,c.name] }
=> [[1, "a"], [2. "b"], [3, "c"]]
>> b = Book.new(:title=>'some book')
>> b.categories_list='a,b,c'
>> b.categories
=> [#<Category id: 1>, #<Category id: 2>, #<Category id: 3>]
>> b.categories_list='a,c,d'
>> b.categories
=> [#<Category id: 1>, #<Category id: 3>, #<Category id: nil, name: "d">]
>> b.save
>> Category.all.map{|c| [c.id,c.name] }
=> [[1, "a"], [2. "b"], [3, "c"], [4, "d"]]
Fifth step is to write edit/new view:
<% form_for(@book) do |f| %>
<div>
<%= f.label :title, t(:book_label_title) %><br />
<%= f.text_field :title %>
</div>
<div>
<%= f.label :categories_list, t(:book_label_category) %><br />
<%= text_field_with_auto_complete :book, :categories_list, {}, { :method => :get } %>
</div>
<% end %>
Six step is to define action in controller:
class BooksController < ApplicationController
def auto_complete_for_book_categories_list
list = params['book']['categories_list'].split(',').map{ |e| e.strip }
name = list.pop
find_options = {
:conditions => [ "name LIKE ?", '%' + name + '%' ],
:order => "name ASC",
:limit => 10,
:select => 'id, name',
}
@items = Category.find(:all, find_options).select { |e| !list.include?(e.name) }.
map { |e| list.push(e.name); e.name=(list*', '); list.pop(); e }
render :inline => "<%= auto_complete_result @items, 'name' %>"
end
#other actions go here ...
end
The seventh and last step is to add routing:
map.resources :books, :collection => { :auto_complete_for_book_categories_list => :get }
Hope that this will help someone.





