1.6. Building a Recipe Editor

If you fire up the Rails server and look at the recipe input form, you'll see that at this point, it looks something like what is shown in Figure 1-1

Figure 1.1. Figure 1-1

While maintaining the proper amount of reverence to the tool that provided this form for free, it's easy to see that it won't do. Ingredients aren't listed, all the boxes are the wrong size, and basically the thing looks totally generic. Your punch list looks like this:

  • Make the items that need longer data entry into text areas.

  • Clean up the organization to look more like a finished recipe.

  • Add ingredients to the recipe.

Naturally, you'll start by writing some tests.

1.6.1. Adding Ingredients

Test-Driven Development (TDD, sometimes also called Test-First Development) is a practice that first gained widespread attention as one of the core practices of Extreme Programming (or XP). Even if your programming is less extreme, writing automated tests is perhaps the best single way to ensure the quality and stability of your application over time. This is particularly true in Rails, because all kinds of testing goodness have been built into the framework, making powerful tests easy to write.

In this book, I'm going to try where possible to present working tests for the code samples as they are presented. The idea is to give you a sense of strategies for testing various parts of a Rails application, and to reinforce the idea that writing tests for all your Rails code is an achievable and desirable goal.

I'd like to start by reinforcing the previously created tests for the Recipe new form and the create method. For new, I'd like to confirm that the expected elements in the form actually exist, and for create, I'd like to confirm that when those elements are passed to the server, the expected recipe object is created. For both, I'd like to test the ingredient functionality.

1.6.2. Asserting HTML

To test the form, you'll use an extremely powerful feature of the Rails test environment called assert_select, which allows you to test the structure of the HTML sent to the browser. Your first usage of assert_select just scratches the surface of what it can do. The following test is in tests/functional/recipe_controller_test.rb:

def test_should_get_new
    get :new
    assert_response :success
    assert_select("form[action=?]", recipes_path) do
      assert_select "input[name *= title]"
      assert_select "input[name *= servings]"
      assert_select "textarea[name *= ingredient_string]"
      assert_select "textarea[name *= description]"
      assert_select "textarea[name *= directions]"
    end
  end

The strategy in testing these forms is to verify the structure of the form. Writing tests for the visual aspects of the form is likely to be very brittle, especially this early in development, and would add a lot of cost in maintaining the test. However, no matter how it's displayed, the recipe form is likely to have some method for entering a title. You could test based on the CSS class of each form, if your design process was such that those names are likely to be stable. Then you could experiment with the visual display via the CSS file.

Each assert_select test contains a selector, and the job of the test is to validate whether the HTML output of the test has some text that matches the selector. This is roughly equivalent to a regular expression; however, the selectors are specifically structured for validating HTML output. Each selector can contain one or more wildcards denoted with a question mark, and the next argument to the method is a list of the values that would fill in those wildcard spots — similar to the way the find method works with SQL statements. The wildcard entries can either be strings or, if you are determined to make it work, regular expressions.

The first part of a selector element is the type of HTML tag that you are searching for. In the case of your first test, that's a form tag. Without any further adornment, that selector will match against all form tags in your returned HTML. You can then pass a second argument if it's a number or a range, and then the selector tests to see if the number of tags matches. The following tests would pass:

assert_select "form", 1
assert_select "form", 0..5

If the second argument is a string or regular expression, then the selector tests to see if there is a tag of that type whose contents either equal the string or match the regular expression.

The type tag can be augmented in several different ways. Putting a dot after it, as in "form.title", checks to see if there's a form tag that is of the CSS class title. Putting a hash mark after the type "form#form_1" performs a similar test on the DOM ID of the tag. If you're familiar with CSS, you'll note this syntax is swiped directly from CSS selector syntax. If you add brackets to the type, then you are checking for an attribute that equals or nearly equals the value specified. The selector "form[action=?]" tests for the existence of a form tag whose action attribute matches the URL specified in the second argument. The equality test could also use the *= symbols, indicating that the attribute value contains the value being tested as a substring, so your test "input[name *= title]" would pass if there was an input tag whose name attribute contains the substring "title". You can similarly use ^= to test that the value begins with the string or $= to test if the value ends with the string.

You can do some further specifying with a number of defined pseudo-classes. Many of these allow you to choose a specific element from the list, such as form:first-child, form:last-child, form:nth-child(n), and form:nth-last-child(n), each of which matches only elements of that type that have the specified relationship with its parent element.

Finally, you can specify a relationship between two tags. Just putting one tag after the other, as in "form input", matches input tags that are some kind of arbitrarily distant descendent of the form tag. Specifying those relationships can get a bit unwieldy, so you can nest the interior specification inside a block, as is done in the previous test method. Because of the nested block structure, the test only matches input tags that are inside a form tag. The specification can also be written "form>input", in which case the input needs to be a direct child of the form. Alternately "form + input" indicates that the input tag is merely after the form tag in the document, and "form ~ input" would match the reverse case.

Add it all up, and your test is verifying the existence of a form tag that points to the create action. Inside that tag, you are testing for inputs with names that include "title" and "servings," and text areas that include the names "description" and "directions."

With the view as it is, these tests won't pass, because the view doesn't use textarea fields for data yet. Update the app/views/recipes/new.html.erb code as follows:

<% @title = "Enter a Recipe" %>
<%= error_messages_for :recipe %>
<% form_for(@recipe) do |f| %>
  <p>
    <b>Recipe Name:</b><br />
    <%= f.text_field :title, :class => "title", :size => 48 %>
  </p>
  <p>
    <b>Serving Size:</b>
    <%= f.text_field :servings, :class => "input", :size => 10  %>
  </p>
  <p>
    <b>Description (optional):</b><br />

<%= f.text_area :description, :rows => 5, :cols => 55, :class => "input" %>
  </p>
  <p>
    <b>Ingredients:</b><br />
    <%= f.text_area :ingredient_string, :rows => 5, :cols => 55, :class => "input" %>
  </p>
  <p>
    <b>Directions:</b><br />
    <%= f.text_area :directions, :rows => 15, :cols => 55, :class => "input" %>
  </p>
  <p>
    <%= f.submit "Create", :class => "title" %>
  </p>
<% end %>
<%= link_to 'Back', recipes_path %>

There are a couple of changes. The fields that need more text now have text areas, things have been moved around a very little bit, and I've added CSS classes to the input fields that increase the size of the text being input (it bothers me when sites use very small text for user input)

The :ingredient_string accessor used in the preceding form is described in the next section.

1.6.3. Parsing Ingredients

The previous code listing included a bare text area for the user to enter ingredients. However, I'd still like to have the data enter the database with some structure that could enable some useful functionality later on, such as converting from English to metric units. Even so, I felt it was a little cruel to give the user a four-element form to fill out for each ingredient. So I wrote a small parser to convert strings like "2 cups carrots, diced" into ingredient objects. The basic test structure follows — put this code into the ingredient unit test class (test/unit/ingredients.rb):

def assert_parse(str, display_str, hash)
    expected = Ingredient.new(hash)
    actual = Ingredient.parse(str, recipes(:one), 1)
    assert_equal_ingredient(expected, actual)
    display_str ||= str
    assert_equal(display_str, actual.display_string)
  end

The inputs are a string, a second string normalized for expected output, and a hash of expected values. One ingredient object is created from the hash, another is created from the string, and you test for equality. Then you test the display output string — if the input is nil, you assume the incoming string is the same as the outgoing string.

The test cases I started with are described in the following table.

CaseDescription
2 cups carrots, dicedThe basic input structure
2 cups carrotsBasic input, minus the instructions
1 carrots, dicedBasic input, minus the unit
1 cup carrotsSingular unit
2.5 carrots, dicedA test to see whether decimal numbers are correctly handled
1/2 carrots, dicedA test to see that fractions are handled
1 1/2 carrots, dicedA test to see whether improper fractions are handled

Here's what the first two test cases look like in code (again, in test/unit/ingredient_test.rb):

def test_should_parse_basically
    assert_parse("2 cups carrots, diced", nil, :recipe_id => 1, :order_of => 1,
        :amount => 2, :unit => "cups", :ingredient => "carrots",
        :instruction => "diced")
  end

  def test_should_parse_without_instructions
    assert_parse("2 cups carrots", nil, :recipe_id => 1, :order_of => 1,
        :amount => 2, :unit => "cups", :ingredient => "carrots",
        :instruction => "")
  end

These test cases use the assert_parse method defined earlier to associate the test string with the expected features of the resulting ingredient. You should be able to define the remaining tests similarly.

There are, of course, other useful test cases that would make this more robust. Tests for proper error handling in deliberately odd conditions would also be nice. For right now, though, the previous test cases provide a sufficient level of complexity to serve as examples of how to do moderately complex processing on user data.

The way this worked in practice was that I wrote one test, made it work, and then refactored and simplified the code. I wrote the second test, which failed, and then fixed the code with another round of refactoring and code cleanup. By the time I finished the last test, the code was in pretty good shape. Here's a description of the code after that test.

I created a separate class for this called IngredientParser, and placed the code in a new file, /app/models/ingredient_parser.rb. The class starts like this:

class IngredientParser

  UNITS = %w{cups pounds ounces tablespoons teaspoons cans cloves}

attr_accessor :result, :tokens, :state, :ingredient_words,
      :instruction_words

  def initialize(str, ingredient)
    @result = ingredient
    @tokens = str.split()
    @state = :amount
    @ingredient_words = []
    @instruction_words = []
  end

  def parse
    tokens.each do |token|
      consumed = self.send(state, token)
      redo unless consumed
    end
    result.ingredient = ingredient_words.join(" ")
    result.instruction = instruction_words.join(" ")
    result
  end
end

The parse method is of the most interest. After splitting the input string into individual words, the class loops through each word, calling a method named by the current state. The states are intended to mimic the piece of data being read, so they start with :amount, because the expectation is that the numerical amount of the ingredient will start the line. Each state method returns true or false. If false is returned, then the loop is rerun with the same token (presumably a method that returns false will have changed the state of the system so that a different method can attempt to consume the token). After the parser runs out of tokens, it builds up the ingredient and instruction strings out of the lists that the parser has gathered.

The parser contains one method for each piece of data, starting with the amount of ingredient to be used, as follows:

def amount(token)
    if token.index("/")
      numerator, denominator = token.split("/")
      fraction = Rational(numerator.to_i, denominator.to_i)
      amount = fraction.to_f
    elsif token.to_f > 0
      amount = token.to_f
    end
    result.amount += amount
    self.state = :unit
    true
  end

If the input token contains a slash, then the assumption is that the user has entered a fraction, and the string is split into two pieces and a Ruby rational object is created and then converted to a float (because the database stores the data as a float). Otherwise, if it's an integer or rational value, the number is taken as is. The number is added to the amount already in the result (because an improper fraction would come through this method in two separate pieces). The state is changed to :unit, and the method returns true to signify that the token has been consumed.

The unit method actually has provisions not to consume the token. If the token is numerical, the parser assumes it's a continuation of the amount, resets the state, and returns false so that the amount method will take a crack at the same token. For example:

def unit(token)
    if token.to_i > 0
      self.state = :amount
      return false
    end
    if UNITS.index(token) or UNITS.index(token.pluralize)
      result.unit = token.pluralize
      self.state = :ingredient
      return true
    else
      self.state = :ingredient
      return false
    end
  end

If the token is not numerical, then it's checked against the list of known units maintained by the parser. If there's a match, then the token is consumed as the unit. If not, the token is not consumed. In either case, the parser moves on to the ingredient itself. Here's an example of how this works:

def ingredient(token)
    ingredient_words << token
    if token.ends_with?(",")
      ingredient_words[-1].chop!
      self.state = :instruction
    end
    true
  end

The ingredient name is assumed to continue until the parser runs out of tokens, or until a token ends in a comma, as in "carrots, diced". Although none of the test cases expose it at this point, that's easily broken in the case where the ingredient is a list containing a comma. However, this error is handled gracefully by the parser, and is also rather straightforward for the enterer to correct, so I chose not to beef up the parser at this time.

Once you get past the comma, everything else is assumed to be part of the final instruction, as follows:

def instruction(token)
    instruction_words << token
    true
  end

To use this, a class method in Ingredient sets the defaults and invokes the parser like this:

def self.parse(str, recipe = nil, order = nil)
    result = Ingredient.new(:recipe_id => recipe.id,
             :order_of => order, :ingredient => "",

:instruction => "", :unit => "", :amount => 0)
    parser = IngredientParser.new(str, result)
    parser.parse
  end

Finally, the display_string method of Ingredient makes sure everything is in a standard format as follows:

def display_string
    str = [amount_as_fraction, unit_inflected,
           ingredient_inflected].compact.join(" ")
    str += ", #{instruction}" unless instruction.blank?
    str
  end

The compact.join("") construct gets rid of the unit if the unit is not set, and does so without putting an extra space in the output. The amount_as_fraction method converts the decimal amount to a fraction, matching the typical usage of cookbooks. (Although this may later be subject to localization, because metric cookbooks generally don't use fractions.) The inflected methods just ensure that the units and ingredients are the proper singular or plural case to match the amount — because "1 cups carrots" will just make the site look stupid.

1.6.4. Adding a Coat of Paint

At this point, I went to www.freewebtemplates.com and chose the canvass template, also available at www.freecsstemplates.org/preview/canvass. I wanted to spruce up the look of the site with something clean that didn't look like Generic Boring Business Site. The free templates on this site are generally licensed via Creative Commons (although if you use one, check the download to make sure). It's a good place to get ideas and to see how various CSS effects can be managed. Naturally, if you were doing a real commercial site, you'd probably want something more unique and original.

Integrating the template was straightforward. The template download has an HTML file, a CSS file, and a bunch of image files. I copied the image files into the application's public/images directory, and then took the CSS file and copied the entries into the preexisting public/scaffold.css file. Alternately, I could have just copied the entire file and added a link to it in the layout. Then I copied the body elements from the provided HTML file into the app/layouts/recipes.html.erb file so that the main content in the provided file was replaced by the <%= yield => call that will tell Rails to include the content for the action. I also tweaked the text somewhat to make it work for Soups OnLine. Finally, I had to go back into the CSS file and change the relative references to image files (images/img01.gif) to absolute references (/images/img01.gif), so that they would be correctly found. The finished result is shown in Figure 1-2 The final layout and CSS files are a bit long and off-point to be included in the text here, but are available as part of the downloadable source code for this book.

Figure 1.2. Figure 1-2

1.6.5. Asserting Creation

Let's tighten up the remaining recipe controller tests while adding ingredient functionality. The test for creating a recipe asserts that the number of recipes changes, but it doesn't assert anything about the entered data. So, I added the following:

def test_should_create_recipe
    recipe_hash = { :title => "Grandma's Chicken Soup",
        :servings => "5 to 7",
        :description => "Good for what ails you",
        :ingredient_string =>

"2 cups carrots, diced

1/2 tablespoon salt

1 1/3 cups stock",
        :directions => "Ask Grandma"}
    assert_difference('Recipe.count') do
      post :create, :recipe => recipe_hash
    end
    expected_recipe = Recipe.new(recipe_hash)
    new_recipe = Recipe.find(:all, :order => "id DESC", :limit => 1)[0]
    assert_equal(expected_recipe, new_recipe)
    assert_equal(3, new_recipe.ingredients.size)
    assert_redirected_to recipe_path(assigns(:recipe))
  end

In the new test, a hash with potential recipe data is defined, and sent to Rails via the post method. Then two recipes are compared, one created directly from the hash, and the other retrieved from the database where Rails put it (finding the recipe with the highest ID). The code then asserts that the two recipes are equal, and somewhat redundantly asserts that the new recipe has created three ingredients from the ingredients sent.

For that test to work, you also need to define equality for a recipe based on the values and not on the object ID. I created the following (rather ugly) unit test for for the recipe_test.rb file, and then the actual code for recipe.rb:

def test_should_be_equal
    hash = {:title => "recipe title",
      :description => "recipe description", :servings => 1,
      :directions => "do it", }
    recipe_expected = Recipe.new(hash)
    recipe_should_be_equal = Recipe.new(hash)
    assert_equal(recipe_expected, recipe_should_be_equal)
    recipe_different_title = Recipe.new(hash)
    recipe_different_title.title = "different title"
    assert_not_equal(recipe_expected, recipe_different_title)
    recipe_different_dirs = Recipe.new(hash)
    recipe_different_dirs.directions = "different directions"
    assert_not_equal(recipe_expected, recipe_different_dirs)
    recipe_different_description = Recipe.new(hash)
    recipe_different_description.description = "different description"
    assert_not_equal(recipe_expected, recipe_different_description)
    recipe_different_servings = Recipe.new(hash)
    recipe_different_servings.servings = "more than one"
    assert_not_equal(recipe_expected, recipe_different_servings)
  end

  def ==(other)
    self.title == other.title &&
        self.servings == other.servings &&
        self.description == other.description &&
        self.directions == other.directions
  end

This might seem like overkill, to have a unit test for equality, but it took very little time to put together, and it makes me less concerned about the bane of the unit tester — the test that really is failing but incorrectly reports that it passed.

The data for the new ingredients comes in as a raw string via the ingredient text area. It's the responsibility of the recipe object to convert that string into the actual ingredient objects. Therefore, I created unit tests in recipe_test.rb to cover the ingredient-adding functionality. The first test merely asserts that ingredients in the recipe are always in the order denoted by their order_of attribute. To make this test meaningful, the ingredient fixtures are defined in the YAML file out of order, so the test really does check that the recipe object orders them, as you can see here:

def test_ingredients_should_be_in_order
    subject = Recipe.find(1)
    assert_equal([1, 2, 3],
        subject.ingredients.collect { |i| i.order_of })
  end

Making the ingredients display in order is extremely easy. You just add this at the beginning of the Recipe class recipe.rb file:

has_many :ingredients, :order => "order_of ASC",
    :dependent => :destroy

The ingredient.rb file needs a corresponding belongs_to :recipe statement. The :order argument here is passed directly to the SQL database to order the ingredients when the database is queried for the related objects.

The test for the ingredient string takes an ingredient string and three expected ingredients, and compares the resulting ingredient list of the recipe with the expected ingredients. It goes in recipe_test.rb like this:

def test_ingredient_string_should_set_ingredients
    subject = Recipe.find(2)
    subject.ingredient_string =
      "2 cups carrots, diced

1/2 tablespoon salt

1 1/3 cups stock"
    assert_equal(3, subject.ingredients.count)
    expected_1 = Ingredient.new(:recipe_id => 2, :order_of => 1,
        :amount => 2, :unit => "cups", :ingredient => "carrots",
        :instruction => "diced")
    expected_2 = Ingredient.new(:recipe_id => 2, :order_of => 2,
        :amount => 0.5, :unit => "tablespoons", :ingredient => "salt",
        :instruction => "")
    expected_3 = Ingredient.new(:recipe_id => 2, :order_of => 3,
        :amount => 1.333, :unit => "cups", :ingredient => "stock",
        :instruction => "")
    assert_equal_ingredient(expected_1, subject.ingredients[0])
    assert_equal_ingredient(expected_2, subject.ingredients[1])
    assert_equal_ingredient(expected_3, subject.ingredients[2])
  end

To make this work, the Recipe class is augmented with a getter and setter method for the attribute ingredient_string — this is the slightly unusual case where you want a getter and setter to do something genuinely different. The setter takes the string and converts it to ingredient objects, and the getter returns the recreated string:

def ingredient_string=(str)
    ingredient_strings = str.split("
")
    order_of = 1
    ingredient_strings.each do |istr|
      next if istr.blank?
      ingredient = Ingredient.parse(istr, self, order_of)
      self.ingredients << ingredient
      order_of += 1
    end
    save
  end

  def ingredient_string
    ingredients.collect { |i| i.display_string}.join("
")
  end

At this point, the earlier test of the entire form should also pass.

The setter splits the strings on newline characters, and then parses each line, skipping blanks and managing the order count. When all the ingredients have been added, the recipe is saved to the database with the new ingredients. The getter gathers the display strings of all the ingredients into a single string.

Finishing up the testing of the basic controller features in test/functional/recipe_controller_test.rb, the edit and update tests are augmented as follows:

def test_should_get_edit
    get :edit, :id => 1
    assert_response :success
    assert_select("form[action=?]", recipe_path(1)) do
      assert_select "input[name *= title]"
      assert_select "input[name *= servings]"
      assert_select "textarea[name *= ingredient_string]"
      assert_select "textarea[name *= description]"
      assert_select "textarea[name *= directions]"
    end
  end

  def test_should_update_recipe
    put :update, :id => 1,
        :recipe => {:title => "Grandma's Chicken Soup"}
    assert_redirected_to recipe_path(assigns(:recipe))
    actual = Recipe.find(1)
    assert_equal("Grandma's Chicken Soup", actual.title)
    assert_equal("1", actual.servings)
  end

The edit test is changed to be almost identical to the new test, the only difference being the form action itself. The easiest way to make this test pass is to take the form block from the new.html.erb file and put it in a partial file called _form.html.erb, and change the new and edit views to refer to it. The updated edit view would be as follows (the new view is similar):

<h1>Editing recipe</h1>
<%= error_messages_for :recipe %>
<%= render :partial => "form" %>
<%= link_to 'Show', @recipe %> |
<%= link_to 'Back', recipes_path %>

Short and sweet. If you are familiar with the traditional Rails model scaffolding, you know that the _form partial was automatically created by that scaffold to be used in the edit and new forms. There is one slight difference. The older version had the actual beginning and ending of the form in the parent view, and only the insides in partial view. In the RESTful version, @recipe serves as a marker for the action in both cases, Rails automatically determines the URL action from the context. As a result, the form block can more easily be entirely contained in the partial view.

1.6.6. Adding a Little Ajax

At this point, the basic CRUD functionality works for recipes with ingredients. I'd like to add one little piece of in-place Ajax editing, allowing the user to do an in-place edit of the ingredients from the recipe show page. This will allow the user to switch from what is shown in Figure 1-3 to what is shown in Figure 1-4

Figure 1.3. Figure 1-3

Figure 1.4. Figure 1-4

To allow Ajax to work in your Rails application, you must load the relevant JavaScript files by including the following line in the app/views.layouts/recipes.html.erb file. Place the line in the HTML header.

<%= javascript_include_tag :defaults %>

I find the best way to build in-place action like this is to build the action as a standalone first, and then incorporate it into the view where needed. I've made the design decision to leave the existing edit and update actions alone, and instead add new actions called remote_edit and remote_update. Here are the unit tests for them, in ingredient_controller_test.rb:

def test_should_get_remote_edit
    get :remote_edit, :id => 1, :recipe_id => 1
    assert_select("form[action=?]",
    remote_update_recipe_ingredient_path(1, 1)) do
      assert_select "input[name *= amount]"
      assert_select "input[name *= unit]"
      assert_select "input[name *= ingredient]"
      assert_select "input[name *= instruction]"
    end
  end

  def test_should_remote_update_ingredient
    put :remote_update, :id => 1, :ingredient => { :amount => 2 },
        :recipe_id => 1
    assert_equal "2 cups First Ingredient, Chopped", @response.body
  end

The tests are very similar to what you'd use for the normal edit and update, just with different URLs. The response for the update method is the ingredient display string, not a redirect to the show ingredient page, which enables the updated ingredient to be inserted back into place on the recipe page. In the interest of full disclosure among friends, I should reveal that I didn't actually develop this part strictly test-first — I played around with the layout within the recipe page a little bit before going back and writing the test.

Because this is a new action for a RESTful controller, new routes have to be added in the routes.rb file. Modify it as follows:

map.resources :recipes do |recipes|
    recipes.resources :ingredients,
        :member => {:remote_edit => :get, :remote_update => :put}
  end

This creates a new remote_edit route that responds to GET, and a remote_update route that responds to PUT. Each of these routes gets a named method to refer to it: remote_edit_recipe_ingredient_path and remote_update_recipe_ingredient_path. Run the rake routes command for full details.

Both of these methods need controller methods and views. The controller methods are quite simple, and go in app/controller/ingredient_controller.rb as follows:

def remote_edit
    edit
  end

You can't get much simpler than that. The remote_edit method uses the same method of getting its ingredient as edit does, so in the interest of avoiding cut and paste, I just call the other method directly. The next step would be another before_filter, which would make both methods empty.

There's also the following view for remote_edit, keeping things on as few lines as possible:

<% remote_form_for(@ingredient,
    :url => remote_update_recipe_ingredient_path(@recipe, @ingredient),
    :update => "ingredient_#{@ingredient.id}") do |f| %>
  <table>
    <tr>
      <th class="subtle">Amount</th>
      <th class="subtle">Unit</th>
      <th class="subtle">Ingredient</th>
      <th class="subtle">Directions</th>
    </tr>
    <tr>
      <td><%= f.text_field :amount, :size => "5" %></td>
      <td><%= f.text_field :unit, :size => "10" %></td>
      <td><%= f.text_field :ingredient, :size => "25" %></td>
      <td><%= f.text_field :instruction, :size => "15" %></td>
      <td><%= f.submit "Update" %></td>
    </tr>
  </table>
<% end %>

Notice the pathname in the result. This is app/views/ingredients/remote_edit.html.erb.

The following remote_update method in the ingredient controller is a simplification of update (for one thing, I'm not concerned here with responding in formats other than HTML):

def remote_update
    @ingredient = Ingredient.find(params[:id])
    if @ingredient.update_attributes(params[:ingredient])
      render(:layout => false)
    else
      render :text => "Error updating ingredient"
    end
  end

The view for this method is simply this:

<%= h @ingredient.display_string %>

The only rendered output of this method is the display string of the newly constructed ingredient or an error message. The only reason it's in an erb file at all is to allow access to the h method to escape out HTML tags and prevent an injection attack.

Finally, the call to create this form has to be placed in the recipe show.html.erb file. Here's the relevant chunk:

<div class="ingredients">
  <h2>Ingredients</h2>
  <% for ingredient in @recipe.ingredients %>
    <div class="ingredient">
      <span id="ingredient_<%= ingredient.id %>">
        <%= h ingredient.display_string %>
      </span>
      <span class="subtle" id="edit_<%= ingredient.id %>">
        <%= link_to_remote "Edit",
            :url =>
               remote_edit_recipe_ingredient_path(@recipe, ingredient),
            :method => :get,
            :update => "ingredient_#{ingredient.id}"%>
      </span>
    </div>
  <% end %>
</div>

Watch out for the :method parameter of the link_to_remote call. By default, link_to_remote sends its request as a POST, and I already specified that remote_edit was a GET. Other than that, the link_to_remote call is typical. The URL to call is specified using the new name generated by the new route, and the DOM element to update is the preceding span containing the ingredient display string.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
18.189.186.109