views:

3798

answers:

5

The dropdown-menu (built by the select_tag) in my application should call the filter-category-action as soon as the user changes the value in the dropdown-menu AND hits the 'Go' button.

Now I'd like to get rid of the 'Go' button and have an observer (observe_field?) call the filter-category-action as soon as the user changes the value in the dropdown-menu.

Below you see the code I've written. It works using the 'Go'-Button but doesn't work by just changing the value in the dropdown-menu. What's wrong with my observe_category_select-helper?

View-partial with dropdown-menu and project list

    <!-- drop down menu -->
    <% form_tag(filter_category_path(:id), :method => :post, :class => 'categories') do %>
       <label>Categories</label>
       <%= select_tag(:category, options_for_select(Category.all.map {|category| [category.name, category.id]}, @category_id)) %>
       <!-- i would like to get rid of this button -->
       <%= submit_tag "Go" %>
     <% end %>

   <!-- list of projects related to categories chosen in drop down menu -->
   <ul class="projects">
     <% @projects.each do |_project| %>
       <li>
         <%= link_to(_project.name, _project) %>
       </li>
     <% end %>
   </ul>

   <%= observe_category_select -%>

HelperMethod

  def observe_category_select
    observe_field(
                  :category,
                  :url        =>  filter_category_path(:id),
                  :with       =>  'category',
                  :on         =>  'change'
    )
  end

Javascript-Output of the HelperMethod

<script type="text/javascript">
//<![CDATA[
   new Form.Element.EventObserver('category', function(element, value) {
     new Ajax.Request('/categories/id/filter', {asynchronous:true, evalScripts:true, parameters:'category=' + encodeURIComponent(value) + '&authenticity_token=' + encodeURIComponent('edc8b20b701f72285068290779f7ed17cfc1cf8c')})
   }, 'change')
//]]>
</script>

Categories controller

class CategoriesController < ApplicationController
  def show
    @category = Category.find(params[:id])
    @category_id = @category.id
    @projects = @category.projects.find(:all)

    respond_to do |format|
      format.html # index.html.erb
    end
  end

  def index
    @projects = Category.find(params[:id]).projects.find(:all)

    respond_to do |format|
      format.html # index.html.erb
    end
  end

  def filter
    @category = Category.find(params[:category])
    @category_id = @category.id
    @projects = @category.projects.find(:all)

    respond_to do |format|
      format.html # index.html.erb
    end    
  end

end

Output of 'rake routes | grep filter'

             filter_category POST   /categories/:id/filter                   {:controller=>"categories", :action=>"filter"}
   formatted_filter_category POST   /categories/:id/filter.:format           {:controller=>"categories", :action=>"filter"}
+1  A: 

My first thought was it may be some scoping issues. I assume that filter_category_path is one of your route path helpers - the id or the category value (in :with) may not be in scope in the helper method.

When you view the page, can you see the JavaScript that is output by the call to observe_field?

Using Firebug, can you see any Ajax requests being made?

Does anything appear in your development.log?

Toby Hede
Yes, filter_category_path is one of my route path helpers: I just added a more detailed description of my code, check it out (not a lot of charachters left in the comment window. :-)).
Javier
If I run it on my localhost, that is what I see on the server-console:Processing CategoriesController#filter (for 127.0.0.1 at 2009-02-22 23:29:29) [POST] Parameters: {"category"=>"3", "authenticity_token"=>"edc8b20b701f72285068290779f7ed17cfc1cf8c", "id"=>"id"}...etc.
Javier
...but the projects in the list below the dropdown menu don't change!
Javier
+5  A: 

Your filter controller action needs to respond to Javascript instead of just to a normal HTTP request.

def filter
  @category = Category.find(params[:category])
  @category_id = @category.id
  @projects = @category.projects.find(:all)

  respond_to do |format|
    format.js # filter.rjs
  end    
end

Or, if you want that action to respond in either context, put both in the block:

respond_to do |format|
  format.html # filter.html.erb
  format.js # filter.rjs
end

This requires you to have a view file filter.rjs that will look something like:

page.replace_html :id_of_element_to_replace_html, :partial => "name_of_partial"
Ian Terrell
The problem still persists. According to Firebug the AJAX request is perfectly OK: Posts the category-value to .../categories/id/filter
Javier
I did run the rails-debugger on localhost and the whole filter-action works as it should. Everything seems to work well, but the list with the projects below the dropdown doesn't change as expected. Any other hint?
Javier
...although @category, @category_id and @projects do have the values they should before the response.
Javier
If that doesn't work, your view isn't doing what it should. Are you sure you're updating the list in your app/views/categories/filter.rjs? Can you post that as a code sample?
Ian Terrell
I've updated the answer to show you a little bit of what you can do, and need to do, in your RJS view.
Ian Terrell
I think the fjs-file was the missing link! I thought it would just re-render the filter.html.rb! Well, I'll try it out as soon as I get home and tell you if that was the solution. Anyway, thanks a lot for your help!
Javier
Great, that was the solution! Thanks a lot Ian!
Javier
A: 

I had the same problem in my actual project. I solved it with the onchange attribute in the select tag:

<%= select_tag @procuct, 
        options_for_select(
            @product.properties.map {|property| [property.name, property.id]}),
        :onchange => "HERE_A_CALL_TO_YOUR_JS_FUNCTION();" %>

UPDATE: I put the Ajax Call in a JS Function, instead you can put everything in the onchange statement, I think you can also put "native Rails" JS methods in there.

Stefan Koenig
Thanks, but that is just overriding RJS and using plain Javascript. Not what I'm looking for.
Javier
I agree Ian Terrell's method is much more elegant. I will adapt his method, too.
Stefan Koenig
+1  A: 
  def observe_category_select
    observe_field(
                  :category,
                  :url        =>  filter_category_path(:id),
                  :with       =>  :category_id,
                  :on         =>  :onchange,
                  :update     =>  :projects
    )
  end

The :onchange is the gotcha - took me a long time to sort it out. EDIT: Looks like you worked that out.

If you want to reset the dropdown and project display state include a function like this in the rendered partial:

<%= link_to_function "close category", :title => 'close category' do |page| 
  page.select("#category").each do |element|
   element.value = "Choose Category"
   page << "window.fireEvent($(\"category\"), 'change');"
  end
  page.replace_html :projects, "<li></li>" 
 end-%>

That will set the select list to the default value and remove all of the list items related to the last displayed category and get the ul ready for the new li's to be displayed from the default. The fireEvent has to be called or the observer won't work if you select the same category that was just hidden.

Since the observer is being fired the controller action has to deal with it:

 def filter
  begin        
    @category = Category.find(params[:category])
    @category_id = @category.id
    @projects = @category.projects.find(:all)
  rescue
    @category = "choose"
  end
  render :layout => false
 end

If no category is found set @category to a string and test in the view partial

<% if @category.eql?('choose') %>
  <li>choose category</li>
<% else %>
 # loop through projects returning them wrapped in li tags
<% end %>

Not pretty but it works.

inkdeep
A: 

FWIW - I'm still pretty new to rails and I stumbled on the fact that the new custom action needs to be defined in your routes.rb file (kinda obvious in hindsight, but I had never had to do this before).

If you're using restful routes, then just tack a :collection => {:filter => :get} to the end of your resource route line (e.g. map.resource :categories, :collection => {:filter => :get}).

Hope that helps somebody out.

Bobby B