How to do an jQuery ajax call from a dropdown using Rails

6 Comments

Here is a simple example I got going that refreshes a text box based on the user changing the selection from a drop down box.

1. Setup jQuery

Download or add a link to the jQuery CDN as shown below.

application.html.erb

<!DOCTYPE html>
<html>
<head>
  <title>Blog</title>
  <%= stylesheet_link_tag    "application" %>
  <%= javascript_include_tag "application" %>
  <%= javascript_include_tag "http://ajax.googleapis.com/ajax/libs/jquery/1.6.4/jquery.min.js" %>
  <%= csrf_meta_tags %>
</head>
<body>

<%= yield %>

</body>
</html>

2. Create a view.

This view will populate a textbox with a ‘hello’ message whenever it’s selection has changed.

/views/foo/bar.html.erb

<div class="field">
  <label>Category</label><br />

  <select name="category[id]" id="category_id">
    <option value="1">art</option>
    <option value="2">bikes</option>
  </select>
</div>

<input type="text" size="57" id="category_textbox">

3. Create a controller.

Our controller doesn’t do much at the moment.

class FooController < ApplicationController
  def bar

  end
end

But to get it to work we need to add one route to our route table.

routes.rb

match ':controller(/:action(/:id(.:format)))'

Once we do that we should see this:

3. Add the ajax.

To detect the change in the drop down, we will use jQuery to register for the select ‘change’ event. Once caught, we will then do an ajax GET to the backend to retrieve our JSON response.

/views/foo/bar.html.erb

 <script type="text/javascript">
jQuery(function($) {
  $("#category_id").change(function() {
    var state = $('select#category_id :selected').val();
    if(state == "") state="0";
    jQuery.getJSON('/foo/bar/' + state, function(data){
        $("#category_textbox").val(data.message);
    })
    return false;
  });
})
</script>

<div class="field">
  <label>Category</label><br />

  <select name="category[id]" id="category_id">
    <option value="1">art</option>
    <option value="2">bikes</option>
  </select>
</div>

<input type="text" size="57" id="category_textbox">

Note: This isn’t my preferred method of embedding javascript and I read that if I used the jQuery live method to register for events I should be able to extract this code into it’s own javascript file but I wasn’t able to get it going. Would love it if someone could show me what I am doing wrong – more information here (http://www.petermac.com/rails-3-jquery-and-multi-select-dependencies/).

4. Send back the response.

Now we just need to configure our controller to send back a json response, as well as handle the original html response.

class FooController < ApplicationController
  def bar
    @msg = { "success" => "true", "message" => "hello"}

    respond_to do |format|
      format.html
      format.json { render json: @msg }
    end
  end
end

Once it’s all working, your show see something like this using Google Developer Tools:

It’s not perfect, but this example at least shows how to hook up jQuery and make an ajax call to a rails backend. I will update this once I learn more about the rails Asset Pipeline and why I wasn’t able to put the javascript into it’s own file.

Advertisements

Rails drop downs

6 Comments

Rails helpers for drop downs were initially confusing to me. I didn’t get how to deal with all the options and variations.

Once I got a few examples going however, they didn’t see that bad. Here’s what I have learned.

collection_select

<%= collection_select(:category, :id, Category.all, :id, :name) %>

produces

<select id="category_id" name="category[id]">
   <option value="1">art</option>
   <option value="2">books</option>
   <option value="3">computers</option>
</select>

This one is handy when you know what your resource is (in this case categories) and you just want to populate it into a drop down.

collection_select(object, method, collection, value_method, text_method, options = {}, html_options = {})

object – is basically the rails resource you are going to get the id and value from for each of your drop down options

method – is the way rails gets an instance of your object. It calls this ‘method’ and whatever object is returned is the instance it uses to populate the drop down options.

collection – is the resource you are going to use to populate the drop down. You can either call it here (Category.all) or have it populated in a controller and have it set as an instance variable (i.e. @categories = Category.all in your controller).

value_method & text_method – are the methods called on your object instance to populate the value and text elements of the drop down.

If you wanted to set some options you can also:

collection_select(:category, :id, Category.all, :id, :name,  {:include_blank => "Please select"}, {:class=>'my-custom-class'})

How do I extract selected drop down value on the backend?

One handy way to see what values are being passed to your controller is to raise an exception and inspect the parameters:

class MyController  < ApplicationController
    def index
      raise params.inspect
      ...
    end
end

When you do this for the above example you see the parameters come to us looking like this:

So we have a category hash with includes the name/value pair id => 1.

To extract that in our view we’d go:

      id = params[:category][:id]

Which you could check by going:

      raise id.inspect

What if my drop down is part of a form post?

Same thing as above. Only this time your drop down is going to be tied to the resource you are posting as.

<%= form_for @listing do |f| %>
  <div class="field">
    <%= f.label :category %><br />
    <%= f.collection_select(:category_id, @categories, :id, :name, :include_blank => "Please select") %>
  </div>
  <div class="actions">
    <%= f.submit "Submit" %>
  </div>
<% end %>

Here that f.collection_select puts the parameters into a listing hash which looks something like this:

Same thing, only now the select drop down value is associated with the posting (listing) hash which you can extract as”

params[:listing][category_id]

For more information on rails drop downs checkout:

collection_select api
http://guides.rubyonrails.org/form_helpers.html#making-select-boxes-with-ease

How to add a drop down resource to an existing rails model

3 Comments

For my pet rails project I need to add a category (drop down) to a listings page.

Step 1: Create the category resource.

Using the rails scaffolding, I create a brand new resources and it’s respectful Restful services like this:

$ rails generate scaffold Category name:string

This gives me a simple categories table with an id and name lookup.

Step 2: Create the migration

I now want to connect the listing to the category through the category_id.

$ rails generate migration AddCategoryToListings category_id:int
class AddCategoryToListings < ActiveRecord::Migration
  def change
    add_column :listings, :category_id, :int
  end
end

> rake db:migrate
> rake db:test:prepare

Note: this is not a database enforced foreign key constraint. Just a rails active record connection.

Step 3: Update the models

Now I need to make it clear that a listing belongs_to a category, and that a category has_many listings a the model level.

I also need to make the new category_id variable available for mass assignment by adding it to the attr_accessible.

/models/listing.rb

class Listing < ActiveRecord::Base
  attr_accessible :title, :description, :category_id

  belongs_to  :category

/models/category.rb

class Category < ActiveRecord::Base
  has_many :listings
end

Step 4: Setup some test factories.

To make sure things are working at database level, I need some factories to give me some test data.

Here I define a factory for a user, listing, and category.

/spec/factories.rb

Factory.define :user do |user|
  user.name                  "Michael Hartl"
  user.email                 "mhartl@example.com"
  user.password              "foobar"
end

Factory.define :listing do |listing|
  listing.title "Title"
  listing.description "Description"
  listing.association :category
  listing.association :user
end

Factory.sequence :category_name do |n|
  "#{n}"
end

Factory.define :category do |c|
  c.name  Factory.next :category_name
end

Step 5: Test the model.

/spec/models/listing_spec.rb

require 'spec_helper'

describe Listing do

  before(:each) do
    @user = Factory(:user)
    @category = Factory(:category)
    @attr = { :title => "some title",
              :description => "some description",
              :category_id => @category.id }
  end

  it "should create a new instance given valid attributes" do
    @user.listings.create!(@attr)
  end

  describe "associations" do
    
    it "should have a category attribute" do
      @category =  @listing.should respond_to(:category)
    end

  end

  describe "validations" do

    before(:each) do
      @attr = { :title => "some title",
                :description => "some description",
                :category_id => @category.id }
    end

    it "should build a listing" do
      @user.listings.build(@attr).should be_valid
    end

    it "should require a category" do
      @user.listings.build(@attr.merge(:category_id => nil)).should_not be_valid
    end

  end

end

A few words about the controller and the view

Before we can test the controller and the model, it’s important to understand what’s going on.

The controller set’s things up for the view. That is, it sets up the the @listing attributes, so the view knows how to render the html necessary to create the view. And it populates the @categories object so the collection_select has all the category information it needs to populate a fully loaded category drop down.

/controllers/listings_controller.rb

class ListingsController < ApplicationController

  before_filter :authenticate, :only => [:create, :destroy, :new]
  before_filter :prepare_categories

  def new
    @listing = Listing.new
    @user = current_user
  end

  def create
    #raise params.inspect

    @listing  = current_user.listings.build(params[:listing])

    begin
      @listing.save!
      flash[:success] = "Listing created!"
      redirect_to root_path
    rescue Exception => e
      render :action => 'new'
    end
  end

  def destroy
  end

  # add the @categories = Category.All to the before action so avail for all actions

  private
    def prepare_categories
      @categories = Category.all
    end

end

Once the controller does this, the view is good to go.

/view/listings/new.html.erb

<%= form_for @listing do |f| %>
  <%= render 'shared/error_messages', :object => f.object %>
  <div class="field">
    <%= f.label :title %><br />
    <%= f.text_field :title, :size => 57 %>
  </div>
  <div class="field">
    <%= f.label :description %><br />
    <%= f.text_area :description, :class => 'description' %>
  </div>
  <div class="field">
    <%= f.label :category %><br />
    <%= f.collection_select(:category_id, @categories, :id, :name, :include_blank => "Please select") %>
  </div>
  <div class="actions">
    <%= f.submit "Submit" %>
  </div>
<% end %>

Step 6: Test the controller

/spec/controllers/listings_controller_spec

require 'spec_helper'

describe ListingsController do
  render_views
  
  describe "GET 'new'" do

    it "should be successful" do
      get :new
      response.should be_success
    end

    it "should prepare categories" do
      get :new
      assigns(:categories).should_not be_nil
    end
  end


  describe "POST 'create'" do

     describe "failure" do

       before(:each) do
         @attr = { :title => "", :description => "", :category_id => "" }
       end

       it "should not create a listing" do
         lambda do
           post :create, :listing => @attr
         end.should_not change(Listing, :count)
       end

       it "should render the home page" do
         post :create, :listing => @attr
         response.should render_template('listings/new')
       end
     end

     describe "success" do

       before(:each) do
         @attr = { :title => "Lorem ipsum",
                   :description => "Lorem ipsum",
                   :category_id => '1' }
       end

       it "should create a listing" do
         lambda do
           post :create, :listing => @attr
         end.should change(Listing, :count).by(1)
       end

       it "should redirect to the home page" do
         post :create, :listing => @attr
         response.should redirect_to(root_path)
       end

       it "should have a flash message" do
         post :create, :listing => @attr
         flash[:success].should =~ /listing created/i
       end
     end
  end
end

Step 7: Test the view.

/spec/requests/listing_spec.rb

require 'spec_helper'

describe "When creating a new listing" do

  it "should create" do

    # signin
    user = Factory(:user)
    visit signin_path
    fill_in "Email",    :with => user.email
    fill_in "Password", :with => user.password
    click_button

    lambda do
      visit '/listings/new'
      fill_in "Title",          :with => "SomeTitle"
      fill_in "Description",    :with => "SomeDescription"
      select "books",            :from => "listing_category_id"
      click_button
    end.should change(Listing, :count).by(1)
  end

end

Note: This test used webrat, and had some test fixture data loaded into the test database via

test/fixtures/categories.yml

one:
  name: books

two:
  name: computers
$ rake db:fixtures:load RAILS_ENV=test

Just a heads up – this example may not work out of the box. It’s just something I through up to remind myself what the steps were for the next time I did this.

To learn more about what’s going on behind the scenes I recommend Michael Hartl’s Ruby Tutorial.

Also apologies for any spelling/typos.

%d bloggers like this: