B?n dang xem chuyên m?c: Rails
Showing posts with label Rails. Show all posts

Wednesday, October 29, 2014

no image

AngularJS on Rails 4 - Part 1

Angular seems to be the big craze as of late. Some may agree and some may not, but AngularJS is one of the next big contenders for being the number one choice of developers. At the time of writing this article, AngularJS is the 12th most watched project on GitHub.

Here I want to create a useful Rails application using Angular. The goal is to have a single-page application which allows us to select a screencast link on the left and view it on the right. An example of this would be found at http://ember101.com.

I had a hard time deciding when I began this project on whether to use a full Rails application or a very lightweight ruby web stack like Sinatra. I also experimented with a middle-ground solution called Rails::API (see Railscast). In the end, I used standard Rails (version 4.0.0.rc1). This gave me the flexibility I want; and for the scope of this tutorial I didn’t want to distract from learning how to use Angular in an Rails application.

Before anything, we need to create a new Rails application called Angular Casts

$ rails new angular_casts...$ cd angular_casts

Our application will be pretty simple. We are only going to worry about storing screencast information in our database. Rails by default comes with a very lightweight database server called SQLite. If you aren’t familiar with this, you can visit http://guides.rubyonrails.org/getting_started.html#configuring-a-database to learn more.

Before we create our model, we need to determine what information we want to store. As a user of this application, I think the most useful would be:

title: What is the name of the screencast?summary: What is the screencast about?duration: How long is the screencast?link: How do I get to the original screencast?published: When was the screencast published?source: Who is the provider of the screencast?

Let’s create a model and controller based on this information. We need to add the video_url field as well, which will be used to embed the video into our app.

$ rails g resource screencast title summary:text duration link published_at:datetime source video_url

By running the resource generator, we now have a model and a controller. The controller will provide our REST API. We also can see that the screencasts resources have been added to our routes:

config/routes.rb AngularCasts::Application.routes.draw do resources :screencasts ...end

Run the migration tasks for both development and test environments:

$ rake db:migrate; rake db:migrate RAILS_ENV=test

For us to have a long-lasting, maintainable application it is imperative that we keep our code tested. This will also help us stay focused on what our application and code is supposed to do.

Start by running the rake task for testing to see make sure the tests run:

$ rake test

If all worked well, you should see something like 0 tests, 0 assertions, 0 failures, 0 errors, 0 skips. This means that the test suite ran, but it didn’t find any tests to run.

There are a couple of things we will want to test:

Make sure that all the required data exists for each screencast.Make sure that we do not have two of the same screencast (duplicates).

Before we write our tests, let’s update our fixtures file with some testable data:

test/fixtures/screencasts.yml 12345678910111213141516171819202122232425262728# Read about fixtures at http://api.rubyonrails.org/classes/ActiveRecord/Fixtures.htmlfast_rails_commands: title: "Fast Rails Commands" summary: "Rails commands, such as generators, migrations, and tests, have a tendency to be slow because they need to load the Rails app each time. Here I show three tools to make this faster: Zeus, Spring, and Commands." duration: "8:06" link: "http://railscasts.com/episodes/412-fast-rails-commands" published_at: "Thu, 04 Apr 2013 00:00:00 -0700" source: "railscasts" video_url: "http://media.railscasts.com/assets/episodes/videos/412-fast-rails-commands.mp4"wizard_forms_with_wicked: title: "Wizard Forms with Wicked" summary: "Creating a wizard form can be tricky in Rails. Learn how Wicked can help by turning a controller into a series of multiple steps." duration: "11:57" link: "http://railscasts.com/episodes/346-wizard-forms-with-wicked" published_at: "Thu, 03 May 2012 00:00:00 -0700" source: "railscasts" video_url: "http://media.railscasts.com/assets/episodes/videos/346-wizard-forms-with-wicked.mp4"sending_html_emails: title: "Sending HTML Email" summary: "HTML email can be difficult to code because any CSS should be made inline. Here I present a few tools for doing this including the premailer-rails3 and roadie gems." duration: "5:42" link: "http://railscasts.com/episodes/312-sending-html-email" published_at: "Mon, 02 Jan 2012 00:00:00 -0800" source: "railscasts" video_url: "http://media.railscasts.com/assets/episodes/videos/312-sending-html-email.mp4"

Open up the auto-generated file test/models/screencast_test.rb and add the following tests. If you are using Rails 3.x, the test file will appear under test/unit/screencast_test.rb.

test/models/screencast_test.rb 1234567891011121314151617181920212223242526272829303132333435require 'test_helper'class ScreencastTest < ActiveSupport::TestCase setup do @screencast_defaults = { title: 'Facebook Authentication', summary: 'This will show how to create a new facebook application and configure it. Then add some authentication with the omniauth-facebook gem and top it off with a client-side authentication using the JavaScript SDK.', duration: '12:09', link: 'http://railscasts.com/episodes/360-facebook-authentication', published_at: Date.parse('Mon, 25 Jun 2012 00:00:00 -0700'), source: 'railscasts', video_url: 'http://media.railscasts.com/assets/episodes/videos/360-facebook-authentication.mp4' } end test "should be invalid if missing required data" do screencast = Screencast.new assert !screencast.valid? [:title, :summary, :duration, :link, :published_at, :source, :video_url].each do |field_name| assert screencast.errors.keys.include? field_name end end test "should be valid if required data exists" do screencast = Screencast.new(@screencast_defaults) assert screencast.valid? end test "should only allow one screencast with the same video url" do screencast = Screencast.new(@screencast_defaults) screencast.video_url = screencasts(:fast_rails_commands).video_url assert !screencast.valid? assert screencast.errors[:video_url].include? "has already been taken" endend

Once this is in place, run the tests again with the command rake test.

123456789101112131415161718$ rake testRun options: --seed 29768# Running tests:F.FFinished tests in 0.048601s, 61.7271 tests/s, 61.7271 assertions/s. 1) Failure:ScreencastTest#test_should_be_invalid_if_missing_required_data [../angular_casts/test/models/screencast_test.rb:18]:Failed assertion, no message given. 2) Failure:ScreencastTest#test_should_only_allow_one_screencast_with_the_same_video_url [../angular_casts/test/models/screencast_test.rb:32]:Failed assertion, no message given.3 tests, 3 assertions, 2 failures, 0 errors, 0 skips

You can see we have have 3 tests with 3 assertions and 2 failures. The reason we didn’t get 3 failures is because the second test, “should be valid if required data exists”, will always pass until we set up some restrictions on the model.

Let’s update the model to make these tests pass.

app/models/screencast.rb class Screencast < ActiveRecord::Base validates_presence_of :title, :summary, :duration, :link, :published_at, :source, :video_url validates_uniqueness_of :video_urlend

Run the tests again to see them pass successfully!

Because we are going to import video feeds from external sites, we need to use a feed parsing library. The best one available is feedzirra. Let’s add it to our Gemfile and remove turbolinks, jbuilder and sdoc. We also remove jquery-rails because we will use it via CDN instead of inside the Asset Pipeline. This will be further explained in part 2.

Gemfile source 'https://rubygems.org'gem 'rails', '4.0.0.rc1'gem 'sqlite3'gem 'sass-rails', '~> 4.0.0.rc1'gem 'uglifier', '>= 1.3.0'gem 'coffee-rails', '~> 4.0.0'gem 'feedzirra'

Now install the gems:

$ bundle install

Now that we have a place to put all of the screencast information, we need to be able to import it from external feeds. Here we are going to create a simple Ruby class that uses the feedzirra gem to grab the feed data, parse it and then add it to our database.

Let’s start off by creating a new class called ScreencastImporter. Paste the following code into lib/screencast_importer.rb.

lib/screencast_importer.rb 12345678910111213141516171819202122232425262728293031323334require 'feedzirra'class ScreencastImporter def self.import_railscasts # because the Railscasts feed is targeted at itunes, there is additional metadata that # is not collected by Feedzirra by default. By using add_common_feed_entry_element, # we can let Feedzirra know how to map those values. See more information at # http://www.ruby-doc.org/gems/docs/f/feedzirra-0.1.2/Feedzirra/Feed.html Feedzirra::Feed.add_common_feed_entry_element(:enclosure, :value => :url, :as => :video_url) Feedzirra::Feed.add_common_feed_entry_element('itunes:duration', :as => :duration) # Capture the feed and iterate over each entry feed = Feedzirra::Feed.fetch_and_parse("http://feeds.feedburner.com/railscasts") feed.entries.each do |entry| # Strip out the episode number from the title title = entry.title.gsub(/^#\d+\s/, '') # Find or create the screencast data into our database Screencast.where(video_url: entry.video_url).first_or_create( title: title, summary: entry.summary, duration: entry.duration, link: entry.url, published_at: entry.published, source: 'railscasts' # set this manually ) end # Return the number of total screencasts for the source Screencast.where(source: 'railscasts').count endend

Note that on lines 17-18, we strip out the episode number from the Railscast title. So “#412 Fast Rails Commands” would become “Fast Rails Commands”. See this Rubular to see how I determined the RegExp pattern.

Now if we were to go into our Rails console, we could trigger this import manually. Give it a shot!

$ rails cLoading development environment (Rails 4.0.0.rc1)>> require 'screencast_importer'=> true>> ScreencastImporter.import_railscasts.... lots ... of ... feedback ....=> 345>> Screencast.count=> 345

Instead of loading up the Rails console and performing the import manually, it would me much easier to have a simple command that we could run to perform the imports. This is where rake tasks come in. Let’s create a rake task that will perform the import.

lib/tasks/screencast_sync.rake require 'screencast_importer'namespace :screencast_sync do desc 'sync all missing screencasts from Railscasts.com' task :railscasts => :environment do total = ScreencastImporter.import_railscasts puts "There are now #{total} screencasts from Railscasts.com" endend

Now that we have our rake task set up, go ahead and run the command:

$ rake screencast_sync:railscastsThere are now 345 screencasts from Railscasts.com

It worked! But no time to celebrate.. let’s move on.

Because we are planning on using Angular for our front-end, we only need to expose our data as JSON. This will allow Angular to talk to the backend via ajax calls.

We are going to only use two calls to the API:

/screencasts.json – returns a full list of episodes/screencasts/ID.json – returns data for a specified screencast (where ID is the unique ID of the screencast in our db)

Because we used the resource generator, we already have our controller and routes.

Let’s do some cleanup to the routes and make sure we only are allowing what we want to use. On top of that, let’s scope our calls to the API with api.

config/routes.rb AngularCasts::Application.routes.draw do scope :api do get "/screencasts(.:format)" => "screencasts#index" get "/screencasts/:id(.:format)" => "screencasts#show" endend

Run the command rake routes to see our changes.

$ rake routesPrefix Verb URI Pattern Controller#Action GET /api/screencasts(.:format) screencasts#index GET /api/screencasts/:id(.:format) screencasts#show

Now update the controller to render the correct JSON data for the two URL’s.

app/controllers/screencast_controller.rb 12345678910111213class ScreencastsController < ApplicationController # GET /screencasts # GET /screencasts.json def index render json: Screencast.all end # GET /screencasts/:id # GET /screencasts/:id.json def show render json: Screencast.find(params[:id]) endend

Now start up your Rails application and visit this link: http://localhost:3000/api/screencasts.json. If all went well, you should see JSON data. You should also be able to view http://localhost:3000/api/screencasts/1.json and see the data belonging to a single screencast.

http://localhost:3000/api/screencasts/1.json 123456789101112{ "id": 1, "title": "Upgrading to Rails 4", "summary": "With the release of Rails 4.0.0.rc1 it's time to try it out and report any bugs. Here I walk you through the steps to upgrade a Rails 3.2 application to Rails 4.", "duration": "12:44", "link": "http://railscasts.com/episodes/415-upgrading-to-rails-4", "published_at": "2013-05-06T07:00:00.000Z", "source": "railscasts", "video_url": "http://media.railscasts.com/assets/episodes/videos/415-upgrading-to-rails-4.mp4", "created_at": "2013-05-21T18:22:29.719Z", "updated_at": "2013-05-21T18:22:29.719Z"}

Of course we are going to test the API! It actually isn’t as complicated as it may seem. I am not going to go over much explanation beyond the inline comments.

Create a new integration test at test/integration/api_screencasts_test.rb.

test/integration/api_screencasts_test.rb 1234567891011121314151617181920require 'test_helper'class ApiScreencastsTest < ActionDispatch::IntegrationTest test "get /api/screencasts.json" do get "/api/screencasts.json" assert_response :success assert body == Screencast.all.to_json screencasts = JSON.parse(response.body) assert screencasts.size == 3 # because there are three fixtures (see screencasts.yml) assert screencasts.any? { |s| s["title"] == screencasts(:fast_rails_commands).title } end test "get /api/screencasts/:id" do screencast = screencasts(:fast_rails_commands) get "/api/screencasts/#{screencast.id}.json" assert_response :success assert body == screencast.to_json assert JSON.parse(response.body)["title"] == screencast.title endend

Go ahead and run your tests:

$ rake test...5 tests, 18 assertions, 0 failures, 0 errors, 0 skips

Looks like our test are passing.

Let’s stop for now. Our next steps will be getting our hands dirty with AngularJS.


Read more »
Advertise

Tuesday, October 28, 2014

AngularJS on Rails 4 - Part 2

AngularJS on Rails 4 - Part 2

Let’s pick up where we left off. If you haven’t already, make sure you go through Part 1 to create your base Rails app with the API setup.

You can either continue using the code you have created on part 1 or you can catch up by checking out the tagged code:

$ git clone https://github.com/cavneb/angular_casts$ cd angular_casts$ git checkout step-1$ bundle install$ rake db:migrate; rake db:migrate RAILS_ENV=test$ rake test$ rake screencast_sync:railscasts

There are a couple of different ways we can add Angular into our application. Ryan Bates suggests using the angular-rails gem. Even though this is an excellent gem which is well maintained, it’s good to know how to do this without a gem.

In our app we are going to link our scripts using a CDN. We can find the CDN for Angular at https://ajax.googleapis.com/ajax/libs/angularjs/1.0.6/angular.min.js. We will also be adding Angular Resource via the CDN as well.

Update your layout file:

app/views/layouts/application.html.erb 12345678910111213141516 Angular Casts <%= stylesheet_link_tag "application", media: "all" %> <%= csrf_meta_tags %> <%= yield %> <%= javascript_include_tag "application" %>

We want to keep our code organized by placing our Angular controllers, filters, services, directives, etc. in the app/assets/javascripts folder. Create the following directories:

app/assets/javascripts/angular/controllersapp/assets/javascripts/angular/directivesapp/assets/javascripts/angular/services

Here’s a shortcut to do this:

$ mkdir -p app/assets/javascripts/angular/controllers \ app/assets/javascripts/angular/directives \ app/assets/javascripts/angular/services

Now let’s create the main javascript file which will drive our Angular application.

app/assets/javascripts/app.js.coffee window.App = angular.module('AngularCasts', ['ngResource'])

In this file we create a new module called AngularCasts and assign it to window.App. We also add the dependency of ngResource which provides simple REST client functionality.

Next, we need to update our JavaScript manifest to include our Angular scripts. The order of these is important due to the latter ones depending on the ones prior to them.

app/assets/javascripts/application.js //= require app//= require_tree ./angular

This is quite a change from what exists in the manifest already. We will add jQuery later, but via CDN. You’ll see why later in this post.

Next, we need to create a controller. This will allow us to set up a route to a view.

$ rails g controller home index

This set up the HomeController and added the action index. Before we modify this view, let’s update our layout to acts as an Angular app. This is done by adding the directive ng-app to our tag:

app/views/layouts/application.html.erb ...

Now let’s update our index view with some simple Angular code:

app/views/home/index.html.erb

Hello {{yourName}}!

Update your routes file to use this view as the root.

config/routes.rb AngularCasts::Application.routes.draw do scope :api do get "/screencasts(.:format)" => "screencasts#index" get "/screencasts/:id(.:format)" => "screencasts#show" end root to: "home#index"end

Note that the line get 'home#index' was removed. This is not needed because the root path directs to it.

Start up your server and open up http://localhost:3000. Type in your name into the text field. If the content changes as you type, it worked! You now have a functional Angular application!

In order for us to tell the page that it should use the App module, we need to add the module name to the ng-app directive. Set the value of the attribute to AngularCasts:

app/views/layouts/application.html.erb 12345678910111213141516171819 Angular Casts <%= stylesheet_link_tag "application", media: "all" %> <%= csrf_meta_tags %>
Angular Casts
<%= yield %> <%= javascript_include_tag "application" %>

Now our view knows to use the AngularCasts module.

Let’s create a controller that will be used to list out the episodes. Create a new coffeescript file at app/assets/javascripts/angular/controllers/screencasts_ctrl.js.coffee

app/assets/javascripts/angular/controllers/screencasts_ctrl.js.coffee App.controller 'ScreencastsCtrl', ['$scope', ($scope) -> $scope.message = "Angular Rocks!"]

On line 1, we create a new Angular controller belonging to App named ScreencastsCtrl. The controller will be referenced in our view as ScreencastsCtrl. For more information on Angular controllers, read http://docs.angularjs.org/guide/dev_guide.mvc.understanding_controller.

Let’s update our view to display the message.

app/views/home/index.html.erb

Message: {{message}}

Here we have bound the contents of the div to the controller ScreencastsCtrl. Refresh the browser and you should see ‘Message: Angular Rocks!’.

Lets add the much needed CSS to our application. Copy the following into app/assets/stylesheets/home.css.scss.

app/assets/stylesheets/home.css.scss 12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879body { font-size: 12px; font-family: Helvetica, sans-serif; background-color: #ddd; margin: 0px;}header { background-color: #4F4F4F; color: #fff; position: absolute; height: 36px; top: 0; left: 0; right: 0; font-size: 18px; line-height: 36px; font-weight: bold; padding-left: 15px;}#screencast-ctrl { background-color: #fff; position: absolute; top: 37px; width: 100%; bottom: 0; overflow: auto;}#screencast-list-container { background-color: #fff; position: absolute; min-height: 700px; width: 300px; top: 37px; left: 0; bottom: 0; overflow: auto; -webkit-overflow-scrolling: touch; ul { margin: 0px; list-style: none; padding: 0px; li { cursor: pointer; border-bottom: 1px solid #ddd; padding: 0 10px; } } h3 { font-size: 14px; small { font-size: 12px; color: #ccc; font-weight: normal; } &.active { color: red; } }}#screencast-view-container { position: absolute; border-left: 1px solid #d0d0d0; top: 37px; left: 300px; right: 0; bottom: 0; background-color: #fff; min-height: 700px; padding: 5px 25px; #player { border: 1px solid #000; max-width: 800px; }}

Refresh the browser. Ooooh!

Our Angular controller is going to access the data from our API using ngResource. ngResource enables interation with RESTful server-side data sources.

Angular services are singletons that carry out specific tasks common to web apps. Services are commonly used to perform the XHR interaction with the server. To learn about the differences between services and factories, read this. Let’s start off by creating a service at screencast.js.coffee:

app/assets/javascripts/angular/services/screencast.js.coffee App.factory 'Screencast', ['$resource', ($resource) -> $resource '/api/screencasts/:id', id: '@id']

Now tell the controller to use this service:

app/assets/javascripts/controllers/screencasts_ctrl.js.coffee App.controller 'ScreencastsCtrl', ['$scope', 'Screencast', ($scope, Screencast) -> $scope.screencasts = Screencast.query()]

Update the index view with the following:

app/views/home/index.html.erb
  • {{screencast.title}} ({{screencast.duration}})

Now refresh the page. If all worked well, you should see a list of screencasts on the left side. When we reloaded the page, a GET request was sent to /api/screencasts, populating the screencasts attribute in our scope. This is the power of Angular ngResource.

We are doing great! Now we have a list of screencasts on the side which are clickable. However, we don’t do anything when they are clicked. What we want to do is show the screencast in the main section along with some additional screencast information.

Start off by adding the HTML code which will be used to display the main content. This is done inside the index.html.erb file:

app/views/home/index.html.erb 123456789101112131415161718
  • {{screencast.title}} ({{screencast.duration}})

{{selectedScreencast.title}}

{{selectedScreencast.summary}}

Published at {{selectedScreencast.published_at | date: 'mediumDate'}} - {{selectedScreencast.link}}

On lines 10-17, we have added a div which shows the screencast title and summary. On line 10, we use the ng-show directive which only displays the div if selectedScreencast exists.

Go ahead and refresh the page. You should not see any changes. Click on a screencast. Still no changes.

In order for us to show the main content with the screencast information, we need to do a few things. The first thing we need to do is add an ng-click directive to our screencast list:

app/views/home/index.html.erb ...
  • {{screencast.title}} ({{screencast.duration}})

...

Now when the list item is clicked the function showScreencast will be triggered with the screencast being passed in to it. Now let’s update our controller with this function:

app/assets/javascripts/angular/controllers/screencasts_ctrl.js.coffee App.controller 'ScreencastsCtrl', ['$scope', 'Screencast', ($scope, Screencast) -> # Attributes accessible on the view $scope.screencasts = Screencast.query() $scope.selectedScreencast = null # Set the selected screencast to the one which was clicked $scope.showScreencast = (screencast) -> $scope.selectedScreencast = screencast]

Refresh your browser and click on a screencast. As my wife would incorrectly say: “Waalah!”

After doing a bit of looking around, I found that Flow Player offered the easiest and cleanest way to show videos. Let’s add the dependent scripts and css links to our layout:

app/views/layouts/application.html.erb 12345678910111213141516171819202122 Angular Casts <%= stylesheet_link_tag "application", media: "all" %> <%= csrf_meta_tags %>
Angular Casts
<%= yield %> <%= javascript_include_tag "application" %>

Remember how we removed jQuery from our Gemfile and javascript manifest? The reason is because we didn’t want it concatenated with the other scripts. FlowPlayer depends on jQuery and so jQuery needs to be available prior to Flow Player being loaded. We also can load it via CDN. Good times for all.

FlowPlayer requires triggering a function flowplayer() to show the video. We could add this into our controller, but we love to learn. Let’s create a directive which listens to the controller and triggers the flowplayer function when showScreencast is called.

Create the directive at app/assets/javascripts/angular/directives/flow_player.js.coffee

app/assets/javascripts/angular/directives/flow_player.js.coffee 123456789101112App.directive 'flowPlayer', -> (scope, element, attrs) -> # Trigger when the selectedScreencast function is called # with a screencast scope.$watch 'selectedScreencast', (screencast) -> if screencast # See http://flowplayer.org/docs/ element.flowplayer playlist: [[mp4: screencast.video_url]] ratio: 9 / 14

Now add the directive into our view:

app/views/home/index.html.erb 1234567891011...

{{selectedScreencast.title}}

{{selectedScreencast.summary}}

Published at {{selectedScreencast.published_at | date: 'mediumDate'}} - {{selectedScreencast.link}}

...

Refresh your browser and go nuts!

One final thing that I would like to see is some sort of indicator which lets us know which video is playing on the screencast list. This can be done via CSS and some simple code.

In our CSS file, we have already added some style for an active screencast. Any H3 tag on the side with the class of active will show as red. Try it out by adding the class to our view:

app/views/home/index.html.erb ...
  • {{screencast.title}} ({{screencast.duration}})

...

Refresh the page. You should now see that every screencast link on the left is red.

For us to make it show for the active screencast only we have to make a few changes to our view and controller. Update the view to use the ng-class directive:

app/views/home/index.html.erb 1234567891011...
  • {{screencast.title}} ({{screencast.duration}})

...

Note that on line 5 we added the 2nd attribute &index. This is available via the ng-repeat directive and is the index value of the array (integer). We also modified line 6 to use the ng-class directive which only shows “active” if the $index is equal to $scope.selectedRow.

Now update the controller to work with these changes:

app/assets/javascripts/controllers/screencasts_ctrl.js.coffee 123456789101112131415App.controller 'ScreencastsCtrl', ['$scope', 'Screencast', ($scope, Screencast) -> # Attributes accessible on the view $scope.selectedScreencast = null $scope.selectedRow = null # Gather the screencasts and set the selected one to the first on success $scope.screencasts = Screencast.query -> $scope.selectedScreencast = $scope.screencasts[0] $scope.selectedRow = 0 # Set the selected screencast to the one which was clicked $scope.showScreencast = (screencast, row) -> $scope.selectedScreencast = screencast $scope.selectedRow = row]

We have added the new param row to the showScreencast function and set this to $scope.selectedRow. Refresh your browser and see how things have changed.

View the working app at http://angular-casts.herokuapp.com/.

I know there are no front-end tests for this tutorial. This was intentional. They were too hard. I spent hours upon hours trying to get the tests working with $httpBackend and service testing, etc. I tried karma and testem. Too much time. Too little benefit. If you are better at this stuff than I am please send me an email or pull request or something with the changes you’ve made to include the tests. I can always add a Part 3 which is about nothing other than testing the frontend.

Thank you all who have patiently waited for me to finish this post. I learned never to release a post before it has been proofread and run through several times. Next time I’m going to do a screencast.. just like the old days.


Read more »

Tuesday, October 7, 2014

AngularJS on Rails 4 - Part 2

AngularJS on Rails 4 - Part 2

Let’s pick up where we left off. If you haven’t already, make sure you go through Part 1 to create your base Rails app with the API setup.

You can either continue using the code you have created on part 1 or you can catch up by checking out the tagged code:

$ git clone https://github.com/cavneb/angular_casts$ cd angular_casts$ git checkout step-1$ bundle install$ rake db:migrate; rake db:migrate RAILS_ENV=test$ rake test$ rake screencast_sync:railscasts

There are a couple of different ways we can add Angular into our application. Ryan Bates suggests using the angular-rails gem. Even though this is an excellent gem which is well maintained, it’s good to know how to do this without a gem.

In our app we are going to link our scripts using a CDN. We can find the CDN for Angular at https://ajax.googleapis.com/ajax/libs/angularjs/1.0.6/angular.min.js. We will also be adding Angular Resource via the CDN as well.

Update your layout file:

app/views/layouts/application.html.erb 12345678910111213141516 Angular Casts <%= stylesheet_link_tag "application", media: "all" %> <%= csrf_meta_tags %> <%= yield %> <%= javascript_include_tag "application" %>

We want to keep our code organized by placing our Angular controllers, filters, services, directives, etc. in the app/assets/javascripts folder. Create the following directories:

app/assets/javascripts/angular/controllersapp/assets/javascripts/angular/directivesapp/assets/javascripts/angular/services

Here’s a shortcut to do this:

$ mkdir -p app/assets/javascripts/angular/controllers \ app/assets/javascripts/angular/directives \ app/assets/javascripts/angular/services

Now let’s create the main javascript file which will drive our Angular application.

app/assets/javascripts/app.js.coffee window.App = angular.module('AngularCasts', ['ngResource'])

In this file we create a new module called AngularCasts and assign it to window.App. We also add the dependency of ngResource which provides simple REST client functionality.

Next, we need to update our JavaScript manifest to include our Angular scripts. The order of these is important due to the latter ones depending on the ones prior to them.

app/assets/javascripts/application.js //= require app//= require_tree ./angular

This is quite a change from what exists in the manifest already. We will add jQuery later, but via CDN. You’ll see why later in this post.

Next, we need to create a controller. This will allow us to set up a route to a view.

$ rails g controller home index

This set up the HomeController and added the action index. Before we modify this view, let’s update our layout to acts as an Angular app. This is done by adding the directive ng-app to our tag:

app/views/layouts/application.html.erb ...

Now let’s update our index view with some simple Angular code:

app/views/home/index.html.erb

Hello {{yourName}}!

Update your routes file to use this view as the root.

config/routes.rb AngularCasts::Application.routes.draw do scope :api do get "/screencasts(.:format)" => "screencasts#index" get "/screencasts/:id(.:format)" => "screencasts#show" end root to: "home#index"end

Note that the line get 'home#index' was removed. This is not needed because the root path directs to it.

Start up your server and open up http://localhost:3000. Type in your name into the text field. If the content changes as you type, it worked! You now have a functional Angular application!

In order for us to tell the page that it should use the App module, we need to add the module name to the ng-app directive. Set the value of the attribute to AngularCasts:

app/views/layouts/application.html.erb 12345678910111213141516171819 Angular Casts <%= stylesheet_link_tag "application", media: "all" %> <%= csrf_meta_tags %>
Angular Casts
<%= yield %> <%= javascript_include_tag "application" %>

Now our view knows to use the AngularCasts module.

Let’s create a controller that will be used to list out the episodes. Create a new coffeescript file at app/assets/javascripts/angular/controllers/screencasts_ctrl.js.coffee

app/assets/javascripts/angular/controllers/screencasts_ctrl.js.coffee App.controller 'ScreencastsCtrl', ['$scope', ($scope) -> $scope.message = "Angular Rocks!"]

On line 1, we create a new Angular controller belonging to App named ScreencastsCtrl. The controller will be referenced in our view as ScreencastsCtrl. For more information on Angular controllers, read http://docs.angularjs.org/guide/dev_guide.mvc.understanding_controller.

Let’s update our view to display the message.

app/views/home/index.html.erb

Message: {{message}}

Here we have bound the contents of the div to the controller ScreencastsCtrl. Refresh the browser and you should see ‘Message: Angular Rocks!’.

Lets add the much needed CSS to our application. Copy the following into app/assets/stylesheets/home.css.scss.

app/assets/stylesheets/home.css.scss 12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879body { font-size: 12px; font-family: Helvetica, sans-serif; background-color: #ddd; margin: 0px;}header { background-color: #4F4F4F; color: #fff; position: absolute; height: 36px; top: 0; left: 0; right: 0; font-size: 18px; line-height: 36px; font-weight: bold; padding-left: 15px;}#screencast-ctrl { background-color: #fff; position: absolute; top: 37px; width: 100%; bottom: 0; overflow: auto;}#screencast-list-container { background-color: #fff; position: absolute; min-height: 700px; width: 300px; top: 37px; left: 0; bottom: 0; overflow: auto; -webkit-overflow-scrolling: touch; ul { margin: 0px; list-style: none; padding: 0px; li { cursor: pointer; border-bottom: 1px solid #ddd; padding: 0 10px; } } h3 { font-size: 14px; small { font-size: 12px; color: #ccc; font-weight: normal; } &.active { color: red; } }}#screencast-view-container { position: absolute; border-left: 1px solid #d0d0d0; top: 37px; left: 300px; right: 0; bottom: 0; background-color: #fff; min-height: 700px; padding: 5px 25px; #player { border: 1px solid #000; max-width: 800px; }}

Refresh the browser. Ooooh!

Our Angular controller is going to access the data from our API using ngResource. ngResource enables interation with RESTful server-side data sources.

Angular services are singletons that carry out specific tasks common to web apps. Services are commonly used to perform the XHR interaction with the server. To learn about the differences between services and factories, read this. Let’s start off by creating a service at screencast.js.coffee:

app/assets/javascripts/angular/services/screencast.js.coffee App.factory 'Screencast', ['$resource', ($resource) -> $resource '/api/screencasts/:id', id: '@id']

Now tell the controller to use this service:

app/assets/javascripts/controllers/screencasts_ctrl.js.coffee App.controller 'ScreencastsCtrl', ['$scope', 'Screencast', ($scope, Screencast) -> $scope.screencasts = Screencast.query()]

Update the index view with the following:

app/views/home/index.html.erb
  • {{screencast.title}} ({{screencast.duration}})

Now refresh the page. If all worked well, you should see a list of screencasts on the left side. When we reloaded the page, a GET request was sent to /api/screencasts, populating the screencasts attribute in our scope. This is the power of Angular ngResource.

We are doing great! Now we have a list of screencasts on the side which are clickable. However, we don’t do anything when they are clicked. What we want to do is show the screencast in the main section along with some additional screencast information.

Start off by adding the HTML code which will be used to display the main content. This is done inside the index.html.erb file:

app/views/home/index.html.erb 123456789101112131415161718
  • {{screencast.title}} ({{screencast.duration}})

{{selectedScreencast.title}}

{{selectedScreencast.summary}}

Published at {{selectedScreencast.published_at | date: 'mediumDate'}} - {{selectedScreencast.link}}

On lines 10-17, we have added a div which shows the screencast title and summary. On line 10, we use the ng-show directive which only displays the div if selectedScreencast exists.

Go ahead and refresh the page. You should not see any changes. Click on a screencast. Still no changes.

In order for us to show the main content with the screencast information, we need to do a few things. The first thing we need to do is add an ng-click directive to our screencast list:

app/views/home/index.html.erb ...
  • {{screencast.title}} ({{screencast.duration}})

...

Now when the list item is clicked the function showScreencast will be triggered with the screencast being passed in to it. Now let’s update our controller with this function:

app/assets/javascripts/angular/controllers/screencasts_ctrl.js.coffee App.controller 'ScreencastsCtrl', ['$scope', 'Screencast', ($scope, Screencast) -> # Attributes accessible on the view $scope.screencasts = Screencast.query() $scope.selectedScreencast = null # Set the selected screencast to the one which was clicked $scope.showScreencast = (screencast) -> $scope.selectedScreencast = screencast]

Refresh your browser and click on a screencast. As my wife would incorrectly say: “Waalah!”

After doing a bit of looking around, I found that Flow Player offered the easiest and cleanest way to show videos. Let’s add the dependent scripts and css links to our layout:

app/views/layouts/application.html.erb 12345678910111213141516171819202122 Angular Casts <%= stylesheet_link_tag "application", media: "all" %> <%= csrf_meta_tags %>
Angular Casts
<%= yield %> <%= javascript_include_tag "application" %>

Remember how we removed jQuery from our Gemfile and javascript manifest? The reason is because we didn’t want it concatenated with the other scripts. FlowPlayer depends on jQuery and so jQuery needs to be available prior to Flow Player being loaded. We also can load it via CDN. Good times for all.

FlowPlayer requires triggering a function flowplayer() to show the video. We could add this into our controller, but we love to learn. Let’s create a directive which listens to the controller and triggers the flowplayer function when showScreencast is called.

Create the directive at app/assets/javascripts/angular/directives/flow_player.js.coffee

app/assets/javascripts/angular/directives/flow_player.js.coffee 123456789101112App.directive 'flowPlayer', -> (scope, element, attrs) -> # Trigger when the selectedScreencast function is called # with a screencast scope.$watch 'selectedScreencast', (screencast) -> if screencast # See http://flowplayer.org/docs/ element.flowplayer playlist: [[mp4: screencast.video_url]] ratio: 9 / 14

Now add the directive into our view:

app/views/home/index.html.erb 1234567891011...

{{selectedScreencast.title}}

{{selectedScreencast.summary}}

Published at {{selectedScreencast.published_at | date: 'mediumDate'}} - {{selectedScreencast.link}}

...

Refresh your browser and go nuts!

One final thing that I would like to see is some sort of indicator which lets us know which video is playing on the screencast list. This can be done via CSS and some simple code.

In our CSS file, we have already added some style for an active screencast. Any H3 tag on the side with the class of active will show as red. Try it out by adding the class to our view:

app/views/home/index.html.erb ...
  • {{screencast.title}} ({{screencast.duration}})

...

Refresh the page. You should now see that every screencast link on the left is red.

For us to make it show for the active screencast only we have to make a few changes to our view and controller. Update the view to use the ng-class directive:

app/views/home/index.html.erb 1234567891011...
  • {{screencast.title}} ({{screencast.duration}})

...

Note that on line 5 we added the 2nd attribute &index. This is available via the ng-repeat directive and is the index value of the array (integer). We also modified line 6 to use the ng-class directive which only shows “active” if the $index is equal to $scope.selectedRow.

Now update the controller to work with these changes:

app/assets/javascripts/controllers/screencasts_ctrl.js.coffee 123456789101112131415App.controller 'ScreencastsCtrl', ['$scope', 'Screencast', ($scope, Screencast) -> # Attributes accessible on the view $scope.selectedScreencast = null $scope.selectedRow = null # Gather the screencasts and set the selected one to the first on success $scope.screencasts = Screencast.query -> $scope.selectedScreencast = $scope.screencasts[0] $scope.selectedRow = 0 # Set the selected screencast to the one which was clicked $scope.showScreencast = (screencast, row) -> $scope.selectedScreencast = screencast $scope.selectedRow = row]

We have added the new param row to the showScreencast function and set this to $scope.selectedRow. Refresh your browser and see how things have changed.

View the working app at http://angular-casts.herokuapp.com/.

I know there are no front-end tests for this tutorial. This was intentional. They were too hard. I spent hours upon hours trying to get the tests working with $httpBackend and service testing, etc. I tried karma and testem. Too much time. Too little benefit. If you are better at this stuff than I am please send me an email or pull request or something with the changes you’ve made to include the tests. I can always add a Part 3 which is about nothing other than testing the frontend.

Thank you all who have patiently waited for me to finish this post. I learned never to release a post before it has been proofread and run through several times. Next time I’m going to do a screencast.. just like the old days.


Read more »

Saturday, October 4, 2014

no image

AngularJS on Rails 4 - Part 1

Angular seems to be the big craze as of late. Some may agree and some may not, but AngularJS is one of the next big contenders for being the number one choice of developers. At the time of writing this article, AngularJS is the 12th most watched project on GitHub.

Here I want to create a useful Rails application using Angular. The goal is to have a single-page application which allows us to select a screencast link on the left and view it on the right. An example of this would be found at http://ember101.com.

I had a hard time deciding when I began this project on whether to use a full Rails application or a very lightweight ruby web stack like Sinatra. I also experimented with a middle-ground solution called Rails::API (see Railscast). In the end, I used standard Rails (version 4.0.0.rc1). This gave me the flexibility I want; and for the scope of this tutorial I didn’t want to distract from learning how to use Angular in an Rails application.

Before anything, we need to create a new Rails application called Angular Casts

$ rails new angular_casts...$ cd angular_casts

Our application will be pretty simple. We are only going to worry about storing screencast information in our database. Rails by default comes with a very lightweight database server called SQLite. If you aren’t familiar with this, you can visit http://guides.rubyonrails.org/getting_started.html#configuring-a-database to learn more.

Before we create our model, we need to determine what information we want to store. As a user of this application, I think the most useful would be:

title: What is the name of the screencast?summary: What is the screencast about?duration: How long is the screencast?link: How do I get to the original screencast?published: When was the screencast published?source: Who is the provider of the screencast?

Let’s create a model and controller based on this information. We need to add the video_url field as well, which will be used to embed the video into our app.

$ rails g resource screencast title summary:text duration link published_at:datetime source video_url

By running the resource generator, we now have a model and a controller. The controller will provide our REST API. We also can see that the screencasts resources have been added to our routes:

config/routes.rb AngularCasts::Application.routes.draw do resources :screencasts ...end

Run the migration tasks for both development and test environments:

$ rake db:migrate; rake db:migrate RAILS_ENV=test

For us to have a long-lasting, maintainable application it is imperative that we keep our code tested. This will also help us stay focused on what our application and code is supposed to do.

Start by running the rake task for testing to see make sure the tests run:

$ rake test

If all worked well, you should see something like 0 tests, 0 assertions, 0 failures, 0 errors, 0 skips. This means that the test suite ran, but it didn’t find any tests to run.

There are a couple of things we will want to test:

Make sure that all the required data exists for each screencast.Make sure that we do not have two of the same screencast (duplicates).

Before we write our tests, let’s update our fixtures file with some testable data:

test/fixtures/screencasts.yml 12345678910111213141516171819202122232425262728# Read about fixtures at http://api.rubyonrails.org/classes/ActiveRecord/Fixtures.htmlfast_rails_commands: title: "Fast Rails Commands" summary: "Rails commands, such as generators, migrations, and tests, have a tendency to be slow because they need to load the Rails app each time. Here I show three tools to make this faster: Zeus, Spring, and Commands." duration: "8:06" link: "http://railscasts.com/episodes/412-fast-rails-commands" published_at: "Thu, 04 Apr 2013 00:00:00 -0700" source: "railscasts" video_url: "http://media.railscasts.com/assets/episodes/videos/412-fast-rails-commands.mp4"wizard_forms_with_wicked: title: "Wizard Forms with Wicked" summary: "Creating a wizard form can be tricky in Rails. Learn how Wicked can help by turning a controller into a series of multiple steps." duration: "11:57" link: "http://railscasts.com/episodes/346-wizard-forms-with-wicked" published_at: "Thu, 03 May 2012 00:00:00 -0700" source: "railscasts" video_url: "http://media.railscasts.com/assets/episodes/videos/346-wizard-forms-with-wicked.mp4"sending_html_emails: title: "Sending HTML Email" summary: "HTML email can be difficult to code because any CSS should be made inline. Here I present a few tools for doing this including the premailer-rails3 and roadie gems." duration: "5:42" link: "http://railscasts.com/episodes/312-sending-html-email" published_at: "Mon, 02 Jan 2012 00:00:00 -0800" source: "railscasts" video_url: "http://media.railscasts.com/assets/episodes/videos/312-sending-html-email.mp4"

Open up the auto-generated file test/models/screencast_test.rb and add the following tests. If you are using Rails 3.x, the test file will appear under test/unit/screencast_test.rb.

test/models/screencast_test.rb 1234567891011121314151617181920212223242526272829303132333435require 'test_helper'class ScreencastTest < ActiveSupport::TestCase setup do @screencast_defaults = { title: 'Facebook Authentication', summary: 'This will show how to create a new facebook application and configure it. Then add some authentication with the omniauth-facebook gem and top it off with a client-side authentication using the JavaScript SDK.', duration: '12:09', link: 'http://railscasts.com/episodes/360-facebook-authentication', published_at: Date.parse('Mon, 25 Jun 2012 00:00:00 -0700'), source: 'railscasts', video_url: 'http://media.railscasts.com/assets/episodes/videos/360-facebook-authentication.mp4' } end test "should be invalid if missing required data" do screencast = Screencast.new assert !screencast.valid? [:title, :summary, :duration, :link, :published_at, :source, :video_url].each do |field_name| assert screencast.errors.keys.include? field_name end end test "should be valid if required data exists" do screencast = Screencast.new(@screencast_defaults) assert screencast.valid? end test "should only allow one screencast with the same video url" do screencast = Screencast.new(@screencast_defaults) screencast.video_url = screencasts(:fast_rails_commands).video_url assert !screencast.valid? assert screencast.errors[:video_url].include? "has already been taken" endend

Once this is in place, run the tests again with the command rake test.

123456789101112131415161718$ rake testRun options: --seed 29768# Running tests:F.FFinished tests in 0.048601s, 61.7271 tests/s, 61.7271 assertions/s. 1) Failure:ScreencastTest#test_should_be_invalid_if_missing_required_data [../angular_casts/test/models/screencast_test.rb:18]:Failed assertion, no message given. 2) Failure:ScreencastTest#test_should_only_allow_one_screencast_with_the_same_video_url [../angular_casts/test/models/screencast_test.rb:32]:Failed assertion, no message given.3 tests, 3 assertions, 2 failures, 0 errors, 0 skips

You can see we have have 3 tests with 3 assertions and 2 failures. The reason we didn’t get 3 failures is because the second test, “should be valid if required data exists”, will always pass until we set up some restrictions on the model.

Let’s update the model to make these tests pass.

app/models/screencast.rb class Screencast < ActiveRecord::Base validates_presence_of :title, :summary, :duration, :link, :published_at, :source, :video_url validates_uniqueness_of :video_urlend

Run the tests again to see them pass successfully!

Because we are going to import video feeds from external sites, we need to use a feed parsing library. The best one available is feedzirra. Let’s add it to our Gemfile and remove turbolinks, jbuilder and sdoc. We also remove jquery-rails because we will use it via CDN instead of inside the Asset Pipeline. This will be further explained in part 2.

Gemfile source 'https://rubygems.org'gem 'rails', '4.0.0.rc1'gem 'sqlite3'gem 'sass-rails', '~> 4.0.0.rc1'gem 'uglifier', '>= 1.3.0'gem 'coffee-rails', '~> 4.0.0'gem 'feedzirra'

Now install the gems:

$ bundle install

Now that we have a place to put all of the screencast information, we need to be able to import it from external feeds. Here we are going to create a simple Ruby class that uses the feedzirra gem to grab the feed data, parse it and then add it to our database.

Let’s start off by creating a new class called ScreencastImporter. Paste the following code into lib/screencast_importer.rb.

lib/screencast_importer.rb 12345678910111213141516171819202122232425262728293031323334require 'feedzirra'class ScreencastImporter def self.import_railscasts # because the Railscasts feed is targeted at itunes, there is additional metadata that # is not collected by Feedzirra by default. By using add_common_feed_entry_element, # we can let Feedzirra know how to map those values. See more information at # http://www.ruby-doc.org/gems/docs/f/feedzirra-0.1.2/Feedzirra/Feed.html Feedzirra::Feed.add_common_feed_entry_element(:enclosure, :value => :url, :as => :video_url) Feedzirra::Feed.add_common_feed_entry_element('itunes:duration', :as => :duration) # Capture the feed and iterate over each entry feed = Feedzirra::Feed.fetch_and_parse("http://feeds.feedburner.com/railscasts") feed.entries.each do |entry| # Strip out the episode number from the title title = entry.title.gsub(/^#\d+\s/, '') # Find or create the screencast data into our database Screencast.where(video_url: entry.video_url).first_or_create( title: title, summary: entry.summary, duration: entry.duration, link: entry.url, published_at: entry.published, source: 'railscasts' # set this manually ) end # Return the number of total screencasts for the source Screencast.where(source: 'railscasts').count endend

Note that on lines 17-18, we strip out the episode number from the Railscast title. So “#412 Fast Rails Commands” would become “Fast Rails Commands”. See this Rubular to see how I determined the RegExp pattern.

Now if we were to go into our Rails console, we could trigger this import manually. Give it a shot!

$ rails cLoading development environment (Rails 4.0.0.rc1)>> require 'screencast_importer'=> true>> ScreencastImporter.import_railscasts.... lots ... of ... feedback ....=> 345>> Screencast.count=> 345

Instead of loading up the Rails console and performing the import manually, it would me much easier to have a simple command that we could run to perform the imports. This is where rake tasks come in. Let’s create a rake task that will perform the import.

lib/tasks/screencast_sync.rake require 'screencast_importer'namespace :screencast_sync do desc 'sync all missing screencasts from Railscasts.com' task :railscasts => :environment do total = ScreencastImporter.import_railscasts puts "There are now #{total} screencasts from Railscasts.com" endend

Now that we have our rake task set up, go ahead and run the command:

$ rake screencast_sync:railscastsThere are now 345 screencasts from Railscasts.com

It worked! But no time to celebrate.. let’s move on.

Because we are planning on using Angular for our front-end, we only need to expose our data as JSON. This will allow Angular to talk to the backend via ajax calls.

We are going to only use two calls to the API:

/screencasts.json – returns a full list of episodes/screencasts/ID.json – returns data for a specified screencast (where ID is the unique ID of the screencast in our db)

Because we used the resource generator, we already have our controller and routes.

Let’s do some cleanup to the routes and make sure we only are allowing what we want to use. On top of that, let’s scope our calls to the API with api.

config/routes.rb AngularCasts::Application.routes.draw do scope :api do get "/screencasts(.:format)" => "screencasts#index" get "/screencasts/:id(.:format)" => "screencasts#show" endend

Run the command rake routes to see our changes.

$ rake routesPrefix Verb URI Pattern Controller#Action GET /api/screencasts(.:format) screencasts#index GET /api/screencasts/:id(.:format) screencasts#show

Now update the controller to render the correct JSON data for the two URL’s.

app/controllers/screencast_controller.rb 12345678910111213class ScreencastsController < ApplicationController # GET /screencasts # GET /screencasts.json def index render json: Screencast.all end # GET /screencasts/:id # GET /screencasts/:id.json def show render json: Screencast.find(params[:id]) endend

Now start up your Rails application and visit this link: http://localhost:3000/api/screencasts.json. If all went well, you should see JSON data. You should also be able to view http://localhost:3000/api/screencasts/1.json and see the data belonging to a single screencast.

http://localhost:3000/api/screencasts/1.json 123456789101112{ "id": 1, "title": "Upgrading to Rails 4", "summary": "With the release of Rails 4.0.0.rc1 it's time to try it out and report any bugs. Here I walk you through the steps to upgrade a Rails 3.2 application to Rails 4.", "duration": "12:44", "link": "http://railscasts.com/episodes/415-upgrading-to-rails-4", "published_at": "2013-05-06T07:00:00.000Z", "source": "railscasts", "video_url": "http://media.railscasts.com/assets/episodes/videos/415-upgrading-to-rails-4.mp4", "created_at": "2013-05-21T18:22:29.719Z", "updated_at": "2013-05-21T18:22:29.719Z"}

Of course we are going to test the API! It actually isn’t as complicated as it may seem. I am not going to go over much explanation beyond the inline comments.

Create a new integration test at test/integration/api_screencasts_test.rb.

test/integration/api_screencasts_test.rb 1234567891011121314151617181920require 'test_helper'class ApiScreencastsTest < ActionDispatch::IntegrationTest test "get /api/screencasts.json" do get "/api/screencasts.json" assert_response :success assert body == Screencast.all.to_json screencasts = JSON.parse(response.body) assert screencasts.size == 3 # because there are three fixtures (see screencasts.yml) assert screencasts.any? { |s| s["title"] == screencasts(:fast_rails_commands).title } end test "get /api/screencasts/:id" do screencast = screencasts(:fast_rails_commands) get "/api/screencasts/#{screencast.id}.json" assert_response :success assert body == screencast.to_json assert JSON.parse(response.body)["title"] == screencast.title endend

Go ahead and run your tests:

$ rake test...5 tests, 18 assertions, 0 failures, 0 errors, 0 skips

Looks like our test are passing.

Let’s stop for now. Our next steps will be getting our hands dirty with AngularJS.


Read more »