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.