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.
http://railscasts.com/episodes/102-auto-complete-association,
I used this screencast to implement and it took 15 minutes to make it work. Anyway documentation in rails is very less but, it has some of the best plugins and gems which can make our life easier.