#102
Apr 21
Auto-Complete Association
Usually a select menu is used for setting a belongs_to association, but in this episode I will show you how to use a text field with auto completion.
Update: As Stephen Gerstacker pointed out in the comments, the :param_name option is preferred over the :with option. See the code below for an example.
script/plugin install auto_complete
# product.rb def category_name category.name if category end def category_name=(name) self.category = Category.find_or_create_by_name(name) unless name.blank? end # categories_controller.rb def index @categories = Category.find(:all, :conditions => ['name LIKE ?', "%#{params[:search]}%"]) end
<!-- products/_form.html.erb --> <p> <%= f.label :category_name %> <%= text_field_with_auto_complete :product, :category_name, { :size => 15 }, { :url => formatted_categories_path(:js), :method => :get, :param_name => 'search' } %> </p> <!-- categories/index.js.erb --> <%= auto_complete_result @categories, :name %>




Thanks!
Thanks very much - awesome!
Thaks! :P
Hi
Your screencast make programming rails even more easy !
But there is something strange in the convention used for returning the matching categories.
"categories/index.js.erb" assume you returning javascript code ?
How does it works if I need to get real js elsewhere in my project ?
Thanks
Mourad Hammiche: instead of use formatted_categories_path(:js) you can use categories_path and at the :with clause you specify something like "partial=true" or "autocomplete=true", then you handle it in your controller to render a special HTML template for autocomplete. Imao it's better then use the js formatted being not semantic.
@Mourad, great questions, I was wondering if someone would bring this up.
It would be really nice if the auto_complete plugin supported returning javascript (with rjs) instead of a simple HTML list, this way I could provide further instructions for modifying the functionality, however, AFAIK the plugin doesn't support this. Since the HTML list is so closely tied to javascript I find it acceptable to use the "js" extension.
As for your 2nd question, this is more difficult. You could pass a query parameter as Carlos mentioned. Alternatively you could create a new mime type, perhaps call it "autocomplete" and make an "index.autocomplete.erb" template. This should work, but I'm not sure if it's a best practice.
Your best railscast to date! Thanks for digging into the details of auto-completing virtual attributes, it is just what I needed!
Nice, Ryan.
This was a good one.
Hey Ryan! Thanks for the awesome screencast. One request -- when you show tips for associations which are simple has_many, it would be cool if you showed the same tip with how it would be changed with has_many :through -- this is a tripping point for a lot of beginners. Thanks!
Hi Ryan,
Just wanted to say thanks for this awesome screencast, and for all the others on your site. It's great to see an enthousiast like yourself sharing knowledge with the rest of the community.
You screencasts make my monday-mornings considerably more pleasant ;-).
Hey Ryan,
I love RailsCasts but I have two comments around this particular episode:
1) I think that using "js" for the request really is wacky. The content type of the response is "text/javascript" indicating that the content really *is* javascript. Perhaps this is a place to register a new psuedo mime type?
2) You are probably open to escaping bugs when you are passing the search parameter through. What happens with your sample app if you type "?foo=bar" into your category box. You need to url encode the value.
Change element.value to encodeURIComponent(element.value).
The prototype way would be to replace the entire :with value with:
Object.toQueryString({search: element.value})
@Joe, good points. The more I think about it, a custom mime type would probably be a preferred solution here over using js. As mentioned above, ideally I would like to return javascript here with RJS, but AFAIK the plugin/scriptaculous doesn't support this. Someone please correct me if I'm wrong.
I hadn't thought about the escaping issue. I'll change the code in the show notes. Thanks!
Hey Ryan,
This may be beyond the scope of this tutorial, but how do you handle setting virtual attributes that are scoped to another model? Let's say you want to have several categories that belong to a bucket, and want to find_or_create_by_name_AND_BUCKET_ID. You can't pass the bucket info to the virtual attribute in the model. This question is much more about virtual attributes than auto-complete, but I've had it come up a lot in other situations as well. Thanks!
This is just what I was looking for - thank you, thank you.
Great screencast, but it really exposed how hackish the autocomplete plugin is.
Why isn't it a regular form builder like the other form fields? Customizing it shouldn't be so awkward (i.e. passing raw Javascript). Why can't the controller method use the virtual attribute on the model?
To be honest, using unobtrusive Javascript seems more straightforward than jumping through Ruby to accomplish this.
There is a real opportunity for someone to refactor this plugin and become a hero!
[Long time listener, first time caller...] Ryan, I swear I saw you type "field.{tab}" to trigger a snippet of a formated label+field. Is that a stock TextMate function or something you wrote for yourself?
@James, I'm not exactly sure if I understand your question. However, you could create a second virtual attribute for the bucket_id, save it to an instance variable, and use that in the category search.
@Geoffrey, I agree. I think they extracted this out into a plugin for a reason. I would love to see an updated version of it which flows better with modern techniques (such as REST).
@Bryan, it's a custom snippet I made. I use a lot of custom snippets and they are pretty easy to make. I recommend looking into it and developing your own library of snippets.
Ryan,
Sorry I wasn't clear.
When you create a setter method, they can only have one parameter (the value of the virtual attribute). How do you include other values from your controller in these setter methods?
def category_name=(name)
self.category = Category.find_or_create_by_name_and_parent_id(name, parent_id) unless name.blank?
end
Can't seem to figure out how to include additional variables (parent_id in this case) in these setters to keep them from being found if they 'belong_to' a different parent.
Same thing would occur if you were trying to tie a model object to "current_user".
@James, the model will need access to the parent_id somehow, so I recommend creating a 2nd virtual attribute which sets an instance variable.
The problem is there's no way to guarantee the parent_id attribute will be set before the category_name. To solve this, you can use a callback to handle the category setting. Check out this code.
http://pastie.caboo.se/184873
You may want to email me if this doesn't solve your problem: ryan[at]railscasts[dot]com
Ryan, great railscast as always, but I have a question. When I enter "categories.js" into my browser to display the list of autocomplete items, I get "No route matches '/categories.js' with {:method=>:get}". I don't think I have my routes set up properly. Did you add any routes besides the default ones? I'm not well versed in routing yet, so I'm not sure what I need to add.
@Dan - You'll probably want to add "format.js" to your respond_to block in your categories controller.
Thanks James; finally got it working! I had assumed I didn't need it since the file was index.js.erb instead of index.html.erb. Thanks for the help.
Ryan,
Thanks for the help, adding an additional virtual attribute and doing the find_or_create in a callback worked perfectly.
@Dan, Here's what my routes file looks like:
map.resources :products, :categories
This generates the formatted_catgories named route which should do what you want. I didn't need a respond_to block in my controller, but if you're not using Rails 2 you will need one.
Thanks for the screencast, it's great and very useful.
When I was trying this, I found a problem with my controller: It wasn't generating anything in the "/controller.js" step. So I put the following code in the end of the method:
respond_to {|format| format.js}
When I tested it, I was given a 'Template is missing' screen, asking for an 'layouts/application.js.erb'.
So I realized my controller has a unremoved "layout 'application'" line in it.
After I removed it, it all worked.
I don't know what really happened, but it may generate some headache for people who'd really need the "layout 'application'" line.
@Lucas - thanks for this 'semi' fix. I've been having problems with for the last day - no one else mentioned having problems so I thought my copy paste skills needed tuning...
I also have a layout :switch_layout (which is serves up a different layout depending on who's logged in) in my controller which is sending this Railscast off the tracks.
From what I've read of the above comments (Ryan, Joe and Geoffrey) - it would be cool to see a sudo mime type used and also a refactored auto_complete plugin...
Anyway, thanks for the screencast :D
Hey,
I can't find any contact details so I'll try my luck here.
There's hardly any resources or tutorials on developing the navigation for an application with rails.
Any subject you plan on taking on?
Kind regards
Aerpe
I think the main problem is that scriptaculous' Ajax.Autocompleter by default sets the target element's innerHTML to the response of the AJAX call.
Maybe an idea would be to override Ajax.Autocompleter's default onSuccess behavior so that it parses a, say, JSON string and turns it into a list before assigning it to the innerHTML of the target element. This way, it would be reasonable to have a mime type of text/javascript or even application/json because JSON is returned.
@Lucas, you shouldn't need to specify "layout :application" as this will be the default. However, if you do need to specify another layout you can do this:
http://pastie.caboo.se/185520
I would consider this to be a bug in Rails, maybe I'll get around to submitting a ticket or patch.
@Aerpe, thanks for the suggestion. I'll add it to my list. :)
Great screencast! I had this problem in one of the project I am working one. Thanks.
Thanks Ryan for yet another gem of a tutorial. keep up the great work.
This almost works, but it breaks support for tokens.
Instead of specifying :with, I use :param_name. This gives you the correct query string, it escapes the query automatically and tokens are supported.
<%= text_field_with_auto_complete :product, :category_name, { :size => 15 }, { :url => formatted_categories_path(:js), :method => :get, :param_name => 'search' } %>
@Stephen, thanks! I had no idea there was a :param_name option. That is much nicer. I'll update the code in the show notes.
Thanks Ryan, you're doing a great job!
I've got a little question: is there an easy way to combine "comlex forms" and "autocomplete with association"?
What I'm trying to do is a "purchase order" form with differents "purchases" (like a porject with different tasks) and I'd like to autocomplete the name of the product of each purchase
Thanks again
@FJuan, I haven't tried it, but you should be able to do this. The main thing you have to watch out for is that text_field_with_auto_complete can't be called through the form builder. This means you'll somehow have to get the name of the field to match what a normal text field would be. You may need to dig into the source code of text_field_with_auto_complete to figure out how to do this.
Great work Ryan! I'm a rails beginner and this website has been a huge help.
@FJuan, I'm trying to do a similar thing using the autocomplete in conjunction with Ryan's complex forms. I'm going to look into the source code as Ryan suggested, and hope I dont mess things up. Thanks again Ryan!
Thanks for this screencast - a really nice solution to drop down lists!
I'm having some troubles though... when I goto the url http://localhost:3000/categories.js nothing shows up... This is my dev.log
<pre>
Processing CategoriesController#index (for 127.0.0.1 at 2008-05-01 20:25:12) [GET]
Session ID: BAh7BzoMY3NyZl9pZCIlZGU1MWNkNzQ2MTYzYTQ0MmY3ZDZmNDhhMjljYjU2%0ANjYiCmZsYXNoSUM6J0FjdGlvbkNvbnRyb2xsZXI6OkZsYXNoOjpGbGFzaEhh%0Ac2h7AAY6CkB1c2VkewA%3D--9533e6fe70bd20e0155ec58df1e28d241ac42676
Parameters: {"format"=>"js", "action"=>"index", "controller"=>"categories"}
Category Load (0.000386) SELECT * FROM `categories` WHERE (name LIKE '%%')
Completed in 0.00667 (149 reqs/sec) | Rendering: 0.00005 (0%) | DB: 0.00039 (5%) | 406 Not Acceptable [http://localhost/categories.js]
</pre>
Can anyone see something going on?
From your log: "[http://localhost/categories.js]"
It would seem Rails is trying to load from port 80 (default for apache), not port 3000 (default for rails).
Furthermore, I believe categories are stored in {RAILS_ROOT}/public/javascripts/categories.js, not {RAILS_ROOT}/public/category.js where is appears you are trying to find it.
Can you post some of your code?
This is great. Thanks Ryan for an excellent screencast, and congrats on breaking 100 episodes.
I'm a bit confused with how to get this process to work with a model that relies on virtual attributes.
For example, I have a Person with :first_name and :last_name attributes, and I use a virtual attribute :name as a shortcut to concatenate them and make it easier to search.
Unfortunately, find_by_name and its dynamic cousins don't exist because in this case, :name is a virtual attribute of Person.
I was able to figure out what was happening before with the field not auto completing.. but now I'm having another problem.
When I hit the save button the category id isn't being stored in the product database.
the log says that category_name gets passed as a parameter but the relationship isn't saved - a new product is created with a NULL in category_id and a new category is created with a category_name.
Any ideas?
it possible to create more than only one attr_accessor ?? In my case, the "Category equivalent model", has 4 fields.