Rails Lesson #5: AJAX

6 May 2008

Today we want to change our application so we can add recipes to our cookbook right from the cookbooks/show page. First we’ll do it without AJAX (asynchronous javascript and XML) then we’ll add it in.

Let’s copy the new recipe form from app/views/recipes/new.html.erb and paste it into app/views/cookbooks/show.html.erb so that it looks like the code below. I also passed another parameter to the f.text_area methods (:rows=>5) because those text areas were just too tall.

<%= error_messages_for :recipe %>

<h2>Cookbook: <%=h @cookbook.title %></h2>
<p>
  <b>Author:</b>
  <%=h @cookbook.author %>

</p>

<h3>Recipes:</h3>
<ul>
<% @cookbook.recipes.each do |recipe| %>
	<li><%= link_to recipe.name, cookbook_recipe_path(@cookbook,recipe) %></li>
<% end %>

</ul>
<br />
<%= link_to "Add new recipe", new_cookbook_recipe_path(@cookbook) %>

<!-- =========== BEGIN Form for new recipes =============  -->
<!-- =========== BEGIN Form for new recipes =============  -->

<%= error_messages_for :recipe %>

<% form_for([@cookbook,@recipe]) do |f| %>
  <p>
    <b>Name</b><br />
    <%= f.text_field :name %>
  </p>

  <p>
    <b>Ingredients</b><br />
    <%= f.text_area :ingredients,:rows=>5 %>
  </p>

  <p>
    <b>Description</b><br />
    <%= f.text_area :description,:rows=>5 %>
  </p>

  <p>

    <b>Number of servings</b><br />
    <%= f.text_field :number_of_servings %>
  </p>

  <p>
    <%= f.submit "Create" %>

  </p>
<% end %>
<!-- =============== END Form for new recipes ==================  -->
<!-- =============== END Form for new recipes ==================  -->

<hr style="margin-top:25px;"/>

<%= link_to 'Edit', edit_cookbook_path(@cookbook) %> |

<%= link_to 'List', cookbooks_path %>

<% end %>

Notice the form_for passes an array containing @cookbook and a @recipe. We need to instanciate @recipe. That code belongs in the cookbooks controller:


  def show
    @cookbook = Cookbook.find(params[:id])
    #instanciate @recipe for the form
    @recipe = Recipe.new
    
    respond_to do |format|
      format.html # show.html.erb
      format.xml  { render :xml => @cookbook }
    end
  end

At this point you can go ahead and use the form to add a recipe to your cookbook. After you create the recipe it takes you to the recipe’s show action. Let’s change that to redirect us back to the cookbook page. This code is in the create action of the recipe’s controller. Change the redirect to redirect_to([cookbook,recipe]) to redirect_to(@cookbook).

       format.html { redirect_to(@cookbook) }

Now it basically behaves how we will want. We just need to sprinkle some Ajax magic. Rails makes it pretty easy to do. We’ll just include some javascript libraries, change our form tag and then code in Ruby how we want our output.

Add this line to your views/layouts/application.html.erb right under the style sheet tag:

	<%= javascript_include_tag :defaults %>

Now to make a form submit asynchronous we call the form_remote_for method instead of the form_for. So change views/cookbooks/show.html.erb:

<!-- ============== BEGIN Form for new recipes ================  -->

<%= error_messages_for :recipe %>

<% form_remote_for([@cookbook,@recipe]) do |f| %>

  ...

If we were to fill out the form and click submit at this point it would look as though nothing happened but it would actually send a request to the server to the recipes controller’s create method and create a record in the database. That was easy, right? Now all we have to do is send an XML response back to the client.

Remember that “respond_to do |format|” block we saw a few lessons ago? We can represent our models as xml as well as html. This time we need to respond to a javascript request. Add “format.js {}” to your create action so that it looks like the following:

respond_to do |format|
      if @recipe.save
        flash[:notice] = 'Recipe was successfully created.'
        format.html { redirect_to(@cookbook) }
        format.js   #this will use views/recipes/create.rjs
        format.xml  { render :xml => @recipe, :status => :created, :location => @recipe }
      else
        format.html { render :action => "new" }
        format.xml  { render :xml => @recipe.errors, :status => :unprocessable_entity }
      end

As you can see from the comment when rails sees the format.js it will look for an rjs file named views/model/action.rjs. RJS stands for Rails JavaScript (might be “Ruby” or “Remote”). In this case it will be views/recipes/create.rjs. Go ahead and create an empty create.rjs file.

We want the web page to do the following once we click submit:

  1. Add the recipe to the list
  2. Clear the form
  3. Erase the flash message

The RJS file will contain ruby code that will be translated into JavaScript by Rails. In order for us to add dynamic content like an item to the list we need to give the containing HTML element an id. The ul tag in this case (cookbooks/show.html.erb):

<ul id="recipe_list">
<% @cookbook.recipes.each do |recipe| %>
	<li><%= link_to recipe.name, cookbook_recipe_path(@cookbook,recipe) %></li>

<% end %>
</ul>

Now for the rjs file. We’ll clean this up a little bit in the next lesson but here is the contents of the create.rjs file which will take care of items 1-3 above in that order:

page.insert_html :bottom, "recipe_list",'&lt;li&gt;' + link_to(@recipe.name,cookbook_recipe_path(@cookbook,@recipe)) + '&lt;/li&gt;'
page["new_recipe"].reset
flash.discard

First we use the insert_html method to add a list item to the “bottom” of the “recipe_list”. Then we reset the “new_recipe” form and then call flash.discard to erase the flash message. Next time we’ll get rid of that unsightly HTML in the create.rjs file by putting the list item into a partial as well as make it possible to delete recipes from cookbooks.