Rails: Caching and Autocomplete with Scriptaculous
I have seen some really great autocompleter tutorials around, but they all require constant database access, which is no good for me.
I have a table with customer’s info in it, there will never be more than a 100 or so, and I don’t add new ones that often.
I have a search method that lets me pull up any customers details simply by typing all or part of his/her name, I want to add an autocompleter to this field, here is what I did:
#helpers/application_helper.rb def autocompleter(args) args[:div] ||= args[:field] + '_div' args[:class] ||= 'auto_complete' scriptaculous = "Event.observe(window, 'load', function() {new Autocompleter.Local('#{args[:field]}', '#{args[:div]}', #{args[:values]});});" return "<script>\n#{scriptaculous}\n</script>\n <div id=\"#{args[:div]}\" class=\"#{args[:class]}\"></div>" end #views/customers/list.rhtml <% cache "customer_autocomplete_list" do %> <script> var customer_names = [ <% Customer.find(:all).each do |customer| %> '<%= customer.name %>', <% end %> '']; </script> <% end %> #or, alternatively more ruby-like, you could also turn this into a helper...: <% cache "customer_autocomplete_list" do values = Customer.find(:all).map do |c| "'#{c.name}'" end %> <script> var customer_names = [ <%= values.join(',') %> ]; </script> <% end %> #Then, I create the form <% form_tag :action => 'search' do %> <input type="text" name="search" value="" id="search_field"> <%= autocompleter :field => 'search_field', :div => 'search_names', :values => 'customer_names', :class => 'auto_complete' %> <%= submit_tag 'Search' %> <% end %>
Explanation:
First I create a js helper method in the application_helper.rb file, it’s generic, so
I can resuse it for anything I like, then in the view, I create a cached js fragment called
customer_autocomplete_list, then I create the form and call the helper method.
In my controller, when I add a new customer, I can simply use:
def create @customer = Customer.new(params[:customer]) expire_fragment "customer_autocomplete_list" end
And it will be regenerated the next time the page is loaded.
Now even with 1000 names, this should still be a viable solution, hell,
for 10000 names, you are still better off loading a fragment than generating
that list from live queries, or on each load.
Anyway, for my small project it works, it’s great, and it makes the client happy
to have one of those gravy effects.
Just to be thorough, here is the auto_complete css:
div.auto_complete { width: 350px; background: #fff; } div.auto_complete ul { border:1px solid #888; margin:0; padding:0; width:100%; list-style-type:none; } div.auto_complete ul li { margin:0; padding:3px; } div.auto_complete ul li.selected { background-color: #ffb; } div.auto_complete ul strong.highlight { color: #800; margin:0; padding:0; }
Earlier on, I said you could convert it into a helper, that is the mapping thing, well, I decided to come back and add that
in so you can see how it is done:
#in application_helper.rb def single_field_map(args) args[:left] ||= "'" args[:right] ||= "'" values = args[:collection].map do |obj| "#{args[:left]}#{obj.send(args[:field])}#{args[:right]}" end return values end #views/customers/list.rhtml <% cache "customer_autocomplete_list" do %> <script> var customer_names = [ <%= single_field_map(:field => 'name',:collection => Customer.find(:all)).join(',') %> ]; </script> <% end %>
Now, that’s more concise, of course, that is in addition to the first helper and view code…just an addition
and a replacement, more ruby like.
It’s hard for me to not think like a PHP programmer, I am getting there, but remember,
working code is better than working theory. In Arnis we have a saying, “If it works, it’s Arnis.”
In a fight, if you win, you’re right, in coding, if your software works like you planned it, then
it’s right.
I have tested all of this code to make sure it works, however, I don’t guarantee anything.
The golden rule of code copying is, if you don’t understand it, don’t copy it. Figure it out first.