AngularJS with Ruby on Rails

Test-Driving the Next Feature

Now that we have Angular setup, including a way to manage front-end assets, run tests, and deploy our application to production, the hard part is done. In this chapter, let's use what we've set up, along with some TDD, to implement the ability to click on a recipe in the results list and view its instructions.

In a classic Rails app, we'd have a method called show in our RecipesController, which would be routed-to from /recipes/:id. We'll do something similar in our Angular app, however we won't add this feature to the existing RecipesController.coffee, but a new controller called RecipeController.

There's no advantage to having the existing RecipesController.coffee handle the viewing of an individual recipe, other than saving us a bit of setup in the test. My feeling is that when there's no advantage over adding code to an existing class or file, it's always better to make a new class or file.

Generally, what we need to do here is:

  1. Create our Angular controller
  2. Create our backend Rails controller
  3. Write a browser-based test for the feature
  4. Create a view

Angular controller

First, we'll update our Angular app config to route /recipes/:recipeId to the yet-to-be-created RecipeController:

diff --git a/app/assets/javascripts/app.coffee b/app/assets/javascripts/app.coffee
index 2801320..729bf3d 100644
--- a/app/assets/javascripts/app.coffee
+++ b/app/assets/javascripts/app.coffee
@@ -11,6 +11,9 @@ receta.config([ '$routeProvider',
       .when('/',
         templateUrl: "index.html"
         controller: 'RecipesController'
+      ).when('/recipes/:recipeId',
+        templateUrl: "show.html"
+        controller: 'RecipeController'
       )
 ])
 

app/assets/javascripts/app.coffee

Now, let's create a bare-bones version of our controller:

controllers = angular.module('controllers')
controllers.controller("RecipeController", [ '$scope', '$routeParams', '$resource',
  ($scope,$routeParams,$resource)->
    Recipe = $resource('/recipes/:recipeId', { recipeId: "@id", format: 'json' })

])

app/assets/javascripts/controllers/RecipeController.coffee

And the boilerplate needed for our test:

describe "RecipeController", ->
  scope        = null
  ctrl         = null
  routeParams  = null
  httpBackend  = null
  recipeId     = 42

  fakeRecipe   =
    id: recipeId
    name: "Baked Potatoes"
    instructions: "Pierce potato with fork, nuke for 20 minutes"

  setupController =(recipeExists=true)->
    inject(($location, $routeParams, $rootScope, $httpBackend, $controller)->
      scope       = $rootScope.$new()
      location    = $location
      httpBackend = $httpBackend
      routeParams = $routeParams
      routeParams.recipeId = recipeId

      ctrl        = $controller('RecipeController',
                                $scope: scope)
    )

  beforeEach(module("receta"))

  afterEach ->
    httpBackend.verifyNoOutstandingExpectation()
    httpBackend.verifyNoOutstandingRequest()

spec/javascripts/controllers/RecipeController_spec.coffee

What RecipeController needs to do is:

Let's get the happy path working first:

diff --git a/spec/javascripts/controllers/RecipeController_spec.coffee b/spec/javascripts/controllers/RecipeController_spec.coffee
index 8028be2..3444a97 100644
--- a/spec/javascripts/controllers/RecipeController_spec.coffee
+++ b/spec/javascripts/controllers/RecipeController_spec.coffee
@@ -18,6 +18,14 @@ describe "RecipeController", ->
       routeParams = $routeParams
       routeParams.recipeId = recipeId
 
+      request = new RegExp("\/recipes/#{recipeId}")
+      results = if recipeExists
+        [200,fakeRecipe]
+      else
+        [404]
+
+      httpBackend.expectGET(request).respond(results[0],results[1])
+
       ctrl        = $controller('RecipeController',
                                 $scope: scope)
     )
@@ -28,3 +36,15 @@ describe "RecipeController", ->
     httpBackend.verifyNoOutstandingExpectation()
     httpBackend.verifyNoOutstandingRequest()
 
+  describe 'controller initialization', ->
+    describe 'recipe is found', ->
+      beforeEach(setupController())
+      it 'loads the given recipe', ->
+        httpBackend.flush()
+        expect(scope.recipe).toEqualData(fakeRecipe)
+    describe 'recipe is not found', ->
+      beforeEach(setupController(false))
+      it 'loads the given recipe', ->
+        httpBackend.flush()
+        expect(scope.recipe).toBe(null)
+        # what else?!

spec/javascripts/controllers/RecipeController_spec.coffee

This is similar to what we had in RecipesController_spec.coffee. Because the HTTP call to our backend happens on controller startup, we create a function setupController() that handles mocking out the HTTP calls. It takes a single parameter—recipeExists—to allow us to control whether or not the backend sends a 404 or a real recipe.

Since none of this is implemented yet, our test should fail. Let's try it:

> rake teaspoon
Starting the Teaspoon server...
Teaspoon running default suite at http://127.0.0.1:58203/teaspoon/default
FFFF...

Failures:

  1) RecipeController controller initialization recipe is found loads the given recipe
     Failure/Error: Error: No pending request to flush ! in http://127.0.0.1:58203/assets/angular-mocks/angular-mocks.js?body=1 (line 1438)

  2) RecipeController controller initialization recipe is found loads the given recipe
     Failure/Error: Error: Unsatisfied requests: GET //recipes/42/ in http://127.0.0.1:58203/assets/angular-mocks/angular-mocks.js?body=1 (line 1471)

  3) RecipeController controller initialization recipe is not found loads the given recipe
     Failure/Error: Error: No pending request to flush ! in http://127.0.0.1:58203/assets/angular-mocks/angular-mocks.js?body=1 (line 1438)

  4) RecipeController controller initialization recipe is not found loads the given recipe
     Failure/Error: Error: Unsatisfied requests: GET //recipes/42/ in http://127.0.0.1:58203/assets/angular-mocks/angular-mocks.js?body=1 (line 1471)

Finished in 0.02200 seconds
7 examples, 4 failures

Failed examples:

teaspoon -s default --filter="RecipeController controller initialization recipe is found loads the given recipe."
teaspoon -s default --filter="RecipeController controller initialization recipe is found loads the given recipe."
teaspoon -s default --filter="RecipeController controller initialization recipe is not found loads the given recipe."
teaspoon -s default --filter="RecipeController controller initialization recipe is not found loads the given recipe."

Sure enough, our test fails exactly how we'd like: $scope.recipe isn't defined for either case, and no HTTP calls were made, despite our expectation that they would be.

Let's make it pass. We'll use Angular's $resource service to create the same resource we did in RecipesController, but use the get method, which does what we want.

diff --git a/app/assets/javascripts/controllers/RecipeController.coffee b/app/assets/javascripts/controllers/RecipeController.coffee
index 8ca9da2..6c55485 100644
--- a/app/assets/javascripts/controllers/RecipeController.coffee
+++ b/app/assets/javascripts/controllers/RecipeController.coffee
@@ -3,4 +3,9 @@ controllers.controller("RecipeController", [ '$scope', '$routeParams', '$resourc
   ($scope,$routeParams,$resource)->
     Recipe = $resource('/recipes/:recipeId', { recipeId: "@id", format: 'json' })
 
+    Recipe.get({recipeId: $routeParams.recipeId},
+      ( (recipe)-> $scope.recipe = recipe ),
+      ( (httpResponse)-> $scope.recipe = null)
+    )
+
 ])

app/assets/javascripts/controllers/RecipeController.coffee

Now, we see if this makes our tests pass:

> rake teaspoon
Starting the Teaspoon server...
Teaspoon running default suite at http://127.0.0.1:58212/teaspoon/default
.....

Finished in 0.02200 seconds
5 examples, 0 failures

It does!

Although our code does technically handle the case of a missing recipe, it doesn't handle it very well. We'd like to pass onto the user some indication that things went wrong. In Rails, we'd use the flash as a way to provide such information.

In Angular, we can certainly create our own flash by just assigning { error: “Recipe not found”} to $scope.flash. Instead, let's use a pre-made module that will handle flash messages, but also allow us to display them in our views. angular-flash is that component, so let's install it.

First, we add it to Bowerfile:

diff --git a/Bowerfile b/Bowerfile
index 1795c30..e14377e 100644
--- a/Bowerfile
+++ b/Bowerfile
@@ -3,4 +3,5 @@ asset 'angular-route'
 asset 'angular-resource'
 asset 'angular-mocks'
 asset 'bootstrap-sass-official'
+asset 'angular-flash'
 # vim: ft=ruby

Bowerfile

Then, install it:

> rake bower:install

To make sure the asset pipeline picks up this new dependency, we'll need to add it to application.js as well:

diff --git a/app/assets/javascripts/application.js b/app/assets/javascripts/application.js
index ea7e7dd..199bd8a 100644
--- a/app/assets/javascripts/application.js
+++ b/app/assets/javascripts/application.js
@@ -15,4 +15,5 @@
 //= require angular/angular
 //= require angular-route/angular-route
 //= require angular-resource/angular-resource
+//= require angular-flash/dist/angular-flash
 //= require_tree .

app/assets/javascripts/application.js

Note the slightly different path to the file we want–this is the lack of standardization across front-end components rearing its ugly head.

Finally, we add it as a module dependency to our app. angular-flash comes with two modules, one for the flash data itself, and another for the view components.

diff --git a/app/assets/javascripts/app.coffee b/app/assets/javascripts/app.coffee
index 729bf3d..117380d 100644
--- a/app/assets/javascripts/app.coffee
+++ b/app/assets/javascripts/app.coffee
@@ -3,6 +3,8 @@ receta = angular.module('receta',[
   'ngRoute',
   'ngResource',
   'controllers',
+  'angular-flash.service',
+  'angular-flash.flash-alert-directive'
 ])
 
 receta.config([ '$routeProvider',

app/assets/javascripts/app.coffee

We'll see how the view components work a bit later, but for now, our controller can depend on a component called flash. flash allows us to set errors, warnings, informational messages, and success messages.

Back to our test, we want to assert that the flash receives an error message that the recipe couldn't be found.

diff --git a/spec/javascripts/controllers/RecipeController_spec.coffee b/spec/javascripts/controllers/RecipeController_spec.coffee
index 3444a97..3455f0d 100644
--- a/spec/javascripts/controllers/RecipeController_spec.coffee
+++ b/spec/javascripts/controllers/RecipeController_spec.coffee
@@ -3,6 +3,7 @@ describe "RecipeController", ->
   ctrl         = null
   routeParams  = null
   httpBackend  = null
+  flash        = null
   recipeId     = 42
 
   fakeRecipe   =
@@ -11,12 +12,13 @@ describe "RecipeController", ->
     instructions: "Pierce potato with fork, nuke for 20 minutes"
 
   setupController =(recipeExists=true)->
-    inject(($location, $routeParams, $rootScope, $httpBackend, $controller)->
+    inject(($location, $routeParams, $rootScope, $httpBackend, $controller, _flash_)->
       scope       = $rootScope.$new()
       location    = $location
       httpBackend = $httpBackend
       routeParams = $routeParams
       routeParams.recipeId = recipeId
+      flash = _flash_
 
       request = new RegExp("\/recipes/#{recipeId}")
       results = if recipeExists
@@ -47,4 +49,4 @@ describe "RecipeController", ->
       it 'loads the given recipe', ->
         httpBackend.flush()
         expect(scope.recipe).toBe(null)
-        # what else?!
+        expect(flash.error).toBe("There is no recipe with ID #{recipeId}")

spec/javascripts/controllers/RecipeController_spec.coffee

Notice that we're taking advantage of Angular's alternate dependency injection naming convention. We want our test to use an object called flash to make assertions, but since this component isn't provided by Angular, its name—for dependency injection purposes—is also flash, meaning we'd need to use a different name for the flash in our tests. Angular allows us to name the parameter with leading and trailing underscores, e.g. _flash_. When we do this, Angular understands that the object flash should be injected. This means that the name of the object that “escapes” the closure can be named flash. Ah, JavaScript!

Now, when we run the test, we should see a simple expectation failure on the message.

> rake teaspoon
Starting the Teaspoon server...
Teaspoon running default suite at http://127.0.0.1:58223/teaspoon/default
.F...

Failures:

  1) RecipeController controller initialization recipe is not found loads the given recipe
     Failure/Error: Expected undefined to be 'There is no recipe with ID 42'.

Finished in 0.03100 seconds
5 examples, 1 failure

Failed examples:

teaspoon -s default --filter="RecipeController controller initialization recipe is not found loads the given recipe."

With a clearly failing test, we just need to add the flash as a dependency, and use it.

diff --git a/app/assets/javascripts/controllers/RecipeController.coffee b/app/assets/javascripts/controllers/RecipeController.coffee
index 6c55485..3c79735 100644
--- a/app/assets/javascripts/controllers/RecipeController.coffee
+++ b/app/assets/javascripts/controllers/RecipeController.coffee
@@ -1,11 +1,14 @@
 controllers = angular.module('controllers')
-controllers.controller("RecipeController", [ '$scope', '$routeParams', '$resource',
-  ($scope,$routeParams,$resource)->
+controllers.controller("RecipeController", [ '$scope', '$routeParams', '$resource', 'flash',
+  ($scope,$routeParams,$resource,flash)->
     Recipe = $resource('/recipes/:recipeId', { recipeId: "@id", format: 'json' })
 
     Recipe.get({recipeId: $routeParams.recipeId},
       ( (recipe)-> $scope.recipe = recipe ),
-      ( (httpResponse)-> $scope.recipe = null)
+      ( (httpResponse)->
+        $scope.recipe = null
+        flash.error   = "There is no recipe with ID #{$routeParams.recipeId}"
+      )
     )
 
 ])

app/assets/javascripts/controllers/RecipeController.coffee

We add flash to the list of injected dependencies, and then set the error message in our failure callback. Sure enough, the test passes:

> rake teaspoon
Starting the Teaspoon server...
Teaspoon running default suite at http://127.0.0.1:58233/teaspoon/default
.....

Finished in 0.03300 seconds
5 examples, 0 failures

Our Angular controller is done! We still need a view, a browser-based test, and the backend. Let's do the Rails backend next.

Rails controller

In the Rails world, it is canonical to have the same controller have the code for both index and show, so what we need to do here is implement show.

First, let's add the new route to config/routes.rb:

diff --git a/config/routes.rb b/config/routes.rb
index 7524ef7..612e169 100644
--- a/config/routes.rb
+++ b/config/routes.rb
@@ -1,5 +1,5 @@
 Receta::Application.routes.draw do
   root 'home#index'
 
-  resources :recipes, only: [:index]
+  resources :recipes, only: [:index, :show]
 end

config/routes.rb

We'll add an empty show method to the controller as well:

diff --git a/app/controllers/recipes_controller.rb b/app/controllers/recipes_controller.rb
index 9eadd20..ac3e415 100644
--- a/app/controllers/recipes_controller.rb
+++ b/app/controllers/recipes_controller.rb
@@ -6,4 +6,7 @@ class RecipesController < ApplicationController
                  []
                end
   end
+
+  def show
+  end
 end

app/controllers/recipes_controller.rb

Finally, we'll write tests for when the recipe exists and for when it doesn't:

diff --git a/spec/controllers/recipes_controller_spec.rb b/spec/controllers/recipes_controller_spec.rb
index 36966e7..20e3ad0 100644
--- a/spec/controllers/recipes_controller_spec.rb
+++ b/spec/controllers/recipes_controller_spec.rb
@@ -42,4 +42,30 @@ describe RecipesController do
     end
 
   end
+
+  describe "show" do
+    before do
+      xhr :get, :show, format: :json, id: recipe_id
+    end
+
+    subject(:results) { JSON.parse(response.body) }
+
+    context "when the recipe exists" do
+      let(:recipe) { 
+        Recipe.create!(name: 'Baked Potato w/ Cheese', 
+               instructions: "Nuke for 20 minutes; top with cheese") 
+      }
+      let(:recipe_id) { recipe.id }
+
+      it { expect(response.status).to eq(200) }
+      it { expect(results["id"]).to eq(recipe.id) }
+      it { expect(results["name"]).to eq(recipe.name) }
+      it { expect(results["instructions"]).to eq(recipe.instructions) }
+    end
+
+    context "when the recipe doesn't exit" do
+      let(:recipe_id) { -9999 }
+      it { expect(response.status).to eq(404) }
+    end
+  end
 end

spec/controllers/recipes_controller_spec.rb

This should result in a failing test, which it does:

> rspec spec/controllers/recipes_controller_spec.rb
.....FFFFF

Failures:

  1) RecipesController show when the recipe exists 
     Failure/Error: xhr :get, :show, format: :json, id: recipe_id
     ActionView::MissingTemplate:
       Missing template recipes/show, application/show with {:locale=>[:en], :formats=>[:json], :handlers=>[:erb, :builder, :raw, :ruby, :jbuilder, :coffee]}. Searched in:
         * "/Users/davec/Projects/angular-rails-book/git_repos/receta/app/views"
         * "/Users/davec/.rvm/gems/ruby-2.1.0@angular-rails-book/gems/teaspoon-0.7.9/app/views"
     # ./spec/controllers/recipes_controller_spec.rb:48:in `block (3 levels) in <top (required)>'

  2) RecipesController show when the recipe exists 
     Failure/Error: xhr :get, :show, format: :json, id: recipe_id
     ActionView::MissingTemplate:
       Missing template recipes/show, application/show with {:locale=>[:en], :formats=>[:json], :handlers=>[:erb, :builder, :raw, :ruby, :jbuilder, :coffee]}. Searched in:
         * "/Users/davec/Projects/angular-rails-book/git_repos/receta/app/views"
         * "/Users/davec/.rvm/gems/ruby-2.1.0@angular-rails-book/gems/teaspoon-0.7.9/app/views"
     # ./spec/controllers/recipes_controller_spec.rb:48:in `block (3 levels) in <top (required)>'

  3) RecipesController show when the recipe exists 
     Failure/Error: xhr :get, :show, format: :json, id: recipe_id
     ActionView::MissingTemplate:
       Missing template recipes/show, application/show with {:locale=>[:en], :formats=>[:json], :handlers=>[:erb, :builder, :raw, :ruby, :jbuilder, :coffee]}. Searched in:
         * "/Users/davec/Projects/angular-rails-book/git_repos/receta/app/views"
         * "/Users/davec/.rvm/gems/ruby-2.1.0@angular-rails-book/gems/teaspoon-0.7.9/app/views"
     # ./spec/controllers/recipes_controller_spec.rb:48:in `block (3 levels) in <top (required)>'

  4) RecipesController show when the recipe exists 
     Failure/Error: xhr :get, :show, format: :json, id: recipe_id
     ActionView::MissingTemplate:
       Missing template recipes/show, application/show with {:locale=>[:en], :formats=>[:json], :handlers=>[:erb, :builder, :raw, :ruby, :jbuilder, :coffee]}. Searched in:
         * "/Users/davec/Projects/angular-rails-book/git_repos/receta/app/views"
         * "/Users/davec/.rvm/gems/ruby-2.1.0@angular-rails-book/gems/teaspoon-0.7.9/app/views"
     # ./spec/controllers/recipes_controller_spec.rb:48:in `block (3 levels) in <top (required)>'

  5) RecipesController show when the recipe doesn't exit 
     Failure/Error: xhr :get, :show, format: :json, id: recipe_id
     ActionView::MissingTemplate:
       Missing template recipes/show, application/show with {:locale=>[:en], :formats=>[:json], :handlers=>[:erb, :builder, :raw, :ruby, :jbuilder, :coffee]}. Searched in:
         * "/Users/davec/Projects/angular-rails-book/git_repos/receta/app/views"
         * "/Users/davec/.rvm/gems/ruby-2.1.0@angular-rails-book/gems/teaspoon-0.7.9/app/views"
     # ./spec/controllers/recipes_controller_spec.rb:48:in `block (3 levels) in <top (required)>'

Finished in 0.54523 seconds
10 examples, 5 failures

Failed examples:

rspec ./spec/controllers/recipes_controller_spec.rb:60 # RecipesController show when the recipe exists 
rspec ./spec/controllers/recipes_controller_spec.rb:63 # RecipesController show when the recipe exists 
rspec ./spec/controllers/recipes_controller_spec.rb:62 # RecipesController show when the recipe exists 
rspec ./spec/controllers/recipes_controller_spec.rb:61 # RecipesController show when the recipe exists 
rspec ./spec/controllers/recipes_controller_spec.rb:68 # RecipesController show when the recipe doesn't exit 

Randomized with seed 45406

To make this pass, we'll fetch the recipe:

diff --git a/app/controllers/recipes_controller.rb b/app/controllers/recipes_controller.rb
index ac3e415..574f98d 100644
--- a/app/controllers/recipes_controller.rb
+++ b/app/controllers/recipes_controller.rb
@@ -8,5 +8,6 @@ class RecipesController < ApplicationController
   end
 
   def show
+    @recipe = Recipe.find(params[:id])
   end
 end

app/controllers/recipes_controller.rb

and implement a JBuilder view that uses our existing _recipe.json.jbuilder partial:

json.partial! 'recipe', recipe: @recipe

app/views/recipes/show.json.jbuilder

To handle the case of a non-existent recipe, we'll let the ActiveRecord::RecordNotFound leak out of our controller, and use rescue_from in ApplicationController to handle that. This way, we never have to worry about translating this error into a 404 again.

diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb
index d83690e..4b24b0c 100644
--- a/app/controllers/application_controller.rb
+++ b/app/controllers/application_controller.rb
@@ -2,4 +2,10 @@ class ApplicationController < ActionController::Base
   # Prevent CSRF attacks by raising an exception.
   # For APIs, you may want to use :null_session instead.
   protect_from_forgery with: :exception
+
+  rescue_from ActiveRecord::RecordNotFound do
+    respond_to do |type|
+      type.all  { render :nothing => true, :status => 404 }
+    end
+  end
 end

app/controllers/application_controller.rb

Now, everything passes!

> rspec spec/controllers/recipes_controller_spec.rb
..........

Finished in 0.47687 seconds
10 examples, 0 failures

Randomized with seed 7268

Let's bring it all together with a browser-based test.

Browser-based test

Our browser-based test will simulate a user using our app, so we'll first do a search, and then navigate to a specific recipe. We'll then check that the resulting view shows the title and instructions. We'll also navigate back to our results, assuming the existence of a “back” button.

require 'spec_helper.rb'

feature "Viewing a recipe", js: true do
  before do
    Recipe.create!(name: 'Baked Potato w/ Cheese', 
           instructions: "nuke for 20 minutes")

    Recipe.create!(name: 'Baked Brussel Sprouts',
           instructions: 'Slather in oil, and roast on high heat for 20 minutes')
  end
  scenario "view one recipe" do
    visit '/'
    fill_in "keywords", with: "baked"
    click_on "Search"

    click_on "Baked Brussel Sprouts"

    expect(page).to have_content("Baked Brussel Sprouts")
    expect(page).to have_content("Slather in oil")

    click_on "Back"

    expect(page).to     have_content("Baked Brussel Sprouts")
    expect(page).to_not have_content("Slather in oil")
  end
end

spec/features/view_spec.rb

Running the test fails:

> rspec spec/features/view_spec.rb
F

Failures:

  1) Viewing a recipe view one recipe
     Failure/Error: expect(page).to have_content("Baked Brussel Sprouts")
       expected to find text "Baked Brussel Sprouts" in "Find Recipes Keywords Search"
     # ./spec/features/view_spec.rb:18:in `block (2 levels) in <top (required)>'

Finished in 6.45 seconds
1 example, 1 failure

Failed examples:

rspec ./spec/features/view_spec.rb:11 # Viewing a recipe view one recipe

Randomized with seed 54433

If you recall, we just used an href of # around the recipe name in the search results. Clicking that essentially clears the search and starts over.

We'll need to change that a to route us to /recipes/:recipeId, as well as actually build out the “show” view.

First, we'll change the a:

diff --git a/app/assets/javascripts/templates/index.html b/app/assets/javascripts/templates/index.html
index fd7a6f1..d830331 100644
--- a/app/assets/javascripts/templates/index.html
+++ b/app/assets/javascripts/templates/index.html
@@ -18,7 +18,7 @@
   <ul class="list-unstyled">
     <li ng-repeat="recipe in recipes">
       <section class="well col-md-6 col-md-offset-3">
-        <h1 class="h3 col-md-6 text-right" style="margin-top: 0"><a href="#">{{recipe.name}}</a></h1>
+        <h1 class="h3 col-md-6 text-right" style="margin-top: 0"><a href ng-click="view(recipe.id)" >{{recipe.name}}</a></h1>
         <div class="col-md-6">
           <button class="btn btn-info">Edit</button>
           <button class="btn btn-danger">Delete</button>

app/assets/javascripts/templates/index.html

Notice that we removed the argument to href, which will keep the browser from changing its location and reloading our view. Instead, we used ng-click to trigger the view() method that we'll now add to RecipesController:

diff --git a/app/assets/javascripts/controllers/RecipesController.coffee b/app/assets/javascripts/controllers/RecipesController.coffee
index b9cc5b3..bfff8fd 100644
--- a/app/assets/javascripts/controllers/RecipesController.coffee
+++ b/app/assets/javascripts/controllers/RecipesController.coffee
@@ -8,4 +8,6 @@ controllers.controller("RecipesController", [ '$scope', '$routeParams', '$locati
       Recipe.query(keywords: $routeParams.keywords, (results)-> $scope.recipes = results)
     else
       $scope.recipes = []
+
+    $scope.view = (recipeId)-> $location.path("/recipes/#{recipeId}")
 ])

app/assets/javascripts/controllers/RecipesController.coffee

All that's left is to create the view in show.html:

<section class="col-md-6 col-md-offset-3">
  <article class="panel panel-info">
    <header class="panel-heading"><h1>{{recipe.name}}</h1></header>
    <p class="panel-body">
      {{recipe.instructions}}
    </p>
  </article>
  <button ng-click="back()" class="btn btn-default">
    &larr; Back
  </button>
</section>

app/assets/javascripts/templates/show.html

We'll also add a method to make the “Back” button work:

diff --git a/app/assets/javascripts/controllers/RecipeController.coffee b/app/assets/javascripts/controllers/RecipeController.coffee
index 3c79735..daba393 100644
--- a/app/assets/javascripts/controllers/RecipeController.coffee
+++ b/app/assets/javascripts/controllers/RecipeController.coffee
@@ -1,6 +1,6 @@
 controllers = angular.module('controllers')
-controllers.controller("RecipeController", [ '$scope', '$routeParams', '$resource', 'flash',
-  ($scope,$routeParams,$resource,flash)->
+controllers.controller("RecipeController", [ '$scope', '$routeParams', '$resource', '$location', 'flash',
+  ($scope,$routeParams,$resource,$location, flash)->
     Recipe = $resource('/recipes/:recipeId', { recipeId: "@id", format: 'json' })
 
     Recipe.get({recipeId: $routeParams.recipeId},
@@ -11,4 +11,6 @@ controllers.controller("RecipeController", [ '$scope', '$routeParams', '$resourc
       )
     )
 
+    $scope.back = -> $location.path("/")
+
 ])

app/assets/javascripts/controllers/RecipeController.coffee

Now, everything works:

> rspec spec/features/view_spec.rb
.

Finished in 5.11 seconds
1 example, 0 failures

Randomized with seed 28477

We added the back button and function specifically to call out a gap between what we get with Angular and what we get with Rails, with respect to view and controller implementation.

We've already seen that Angular's router requires more explicit configuration than Rails'. We can also now see that we don't get convenient methods like recipes_path or recipe_path(recipe) to generate routes for us. There doesn't seem to be a canonical way to do this at this time.

There's one last thing to do, and that's integrate the flash message.

Flash message

Although we hope to not generate links to non-existent recipes, it's still possible it could happen and, unlike a web app where a 404 will send us to a special page, since our Angular app is using AJAX requests, we'll have to do something if we get an error from the backend. We test-drove setting an error message in the flash, so now we just need to show it in our view.

The angular-flash module we installed has two parts. The first, which we've already seen, is a place to store flash messages. The second is to allow your view to “subscribe” to them, which means that you can arrange for markup to be shown if there is a flash message.

Because this is not something a user will ever be intended to see, and is also very simple, we're not going to write a test for it. If there were more complex logic around the flash, and its message, a test would be more useful, but for this case, it's not really worth it.

First, we'll add the necessary markup to show.html:

diff --git a/app/assets/javascripts/templates/show.html b/app/assets/javascripts/templates/show.html
index 5e846a7..df041fb 100644
--- a/app/assets/javascripts/templates/show.html
+++ b/app/assets/javascripts/templates/show.html
@@ -1,3 +1,8 @@
+<aside class="flash row">
+  <article flash-alert duration="0" active-class="alert" class="col-md-6 col-md-offset-3 col-sm-12">
+    {{flash.message}}
+  </article>
+</aside>
 <section class="col-md-6 col-md-offset-3">
   <article class="panel panel-info">
     <header class="panel-heading"><h1>{{recipe.name}}</h1></header>

app/assets/javascripts/templates/show.html

Our article tag has three special attributes, provided by angular-flash:

The second thing we need to do is to configure angular-flash so that it knows about the various alert classes that Bootstrap provides. We do this in the app config in app.coffee:

diff --git a/app/assets/javascripts/app.coffee b/app/assets/javascripts/app.coffee
index 117380d..00b5df2 100644
--- a/app/assets/javascripts/app.coffee
+++ b/app/assets/javascripts/app.coffee
@@ -7,8 +7,14 @@ receta = angular.module('receta',[
   'angular-flash.flash-alert-directive'
 ])
 
-receta.config([ '$routeProvider',
-  ($routeProvider)->
+receta.config([ '$routeProvider', 'flashProvider',
+  ($routeProvider,flashProvider)->
+
+    flashProvider.errorClassnames.push("alert-danger")
+    flashProvider.warnClassnames.push("alert-warning")
+    flashProvider.infoClassnames.push("alert-info")
+    flashProvider.successClassnames.push("alert-success")
+
     $routeProvider
       .when('/',
         templateUrl: "index.html"

app/assets/javascripts/app.coffee

What this configuration means is that if there is, for example, a value in flash.error, angular-flash will add the class alert-danger to the flash element. Since we also configured that element to add alert to any flash message, our markup will be styled with the class alert alert-danger which is what Bootstrap needs to show an alert.

Now, let's navigate to a non-existent recipe and see it in action:

Flash Message

Perfect!

Now that we've seen how we can use TDD for all aspects of our feature, let's add the create, update, and destroy features. We'll do this very quickly as a way to demonstrate what the code looks like. You probably wouldn't add all of these at the same time in your “real” application.