Riding Rails with AngularJS
Riding Rails with AngularJS
Ari Lerner
©2013 Ari Lerner
Tweet This Book!
Please help Ari Lerner by spreading the word about this book on Twitter!
The suggested hashtag for this book is #angularjs-rails.
Find out what other people are saying about the book by clicking on this link to search for this
hashtag on Twitter:
https://twitter.com/search?q=#angularjs-rails
Contents
Introduction . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1
About the author . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1
About this book . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1
Organization of this book . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 2
Additional resources . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 2
Conventions used in this book . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 3
Development environment . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 4
Audience
This book assumes you have a basic knowledge of AngularJS and Ruby on Rails. Although we’ll
briefly cover topics as we introduce them throughout the book, the book is explicitly geared toward
helping you get up to speed with the two technologies.
For an in-depth book on AngularJS, check out our ng-book: The Complete Book on AngularJS book,
available at ng-book.com³.
¹http://angularjs.org
²http://google.com
³http://ng-book.com
Introduction 2
Additional resources
• AngularJS docs⁴
• ng-newsletter.com⁵
• thinkster.io⁶
• ng-book: The Complete Book on AngularJS⁷
AngularJS
We’ll refer to the official documentation on the AngularJS⁸ website. The official AngularJS docu-
mentation is a great resource, and we’ll be using it quite often.
We suggest that you take a look at the AngularJS API documentation, as it gives you direct access to
the recommended methods of writing AngularJS applications. Of course, it also gives you the most
up-to-date documentation available.
This book will work with the latest version of AngularJS: 1.2.0-rc.2.
Rails
There are many great resources available for Ruby on Rails, the foremost of which are the guides
available at guides.rubyonrails.org⁹. Michael Hartl’s Ruby on Rails tutorial¹⁰ book is also a good
resource for learning Rails.
⁴http://docs.angularjs.org/
⁵http://www.ng-newsletter.com/
⁶http://www.thinkster.io/
⁷http://ng-book.com
⁸http://angularjs.org
⁹http://guides.rubyonrails.org/
¹⁰http://ruby.railstutorial.org/ruby-on-rails-tutorial-book
Introduction 3
Other online classes, such as bloc.io¹¹ and onemonthrails.com¹², are also good resources.
This book is written specifically for Rails 4.
1 $ ls -la
Any command in the developer console in Chrome (the browser with which we will primarily be
developing) will look like this (with the > character denoting the command):
When we want to highlight a certain part of a file of which we have already described a portion,
we’ll substitute the contents of the file with ellipses. For instance:
1 angular.module('myApp.services', ['ngResource'])
2 // ...
3 .factory()
Development environment
We’ll be working in two different environments in this book: the Rails environment and the
AngularJS environment. For both of these development processes, we’ll need a text editor. It’s
important to use a text editor that you feel comfortable working in, as we’ll do a lot of our work
there. We suggest Sublime Text 3¹³.
We’ll refer to the text editor as your editor throughout the book, while we’ll refer to the browser
as the browser.
For this book, we highly recommend you download the Google Chrome browser, as it provides a
great development environment using the developer tools.
We’ll only need a few libraries installed to get going. To run the tests, we’ll need the Karma library
and nodejs.
It’s also a good idea to have git installed, although this is not a strict requirement.
This book won’t cover how to install NodeJS. Visit nodejs.org¹⁴ for more information.
Once you’ve installed a version of Node higher than 0.8 at minimum, install the Karma library:
We’ll also need to make sure we have Rails installed. We won’t cover how to install Ruby itself,
a dependency of the Ruby on Rails framework. Once you have Ruby installed, installing Rails is a
cinch:
¹³http://www.sublimetext.com/3
¹⁴http://nodejs.org
Setting up our Rails app
In this chapter, we will walk through setting up your Rails app and have it running in minutes. We
will address integrating with AngularJS in subsequent chapters, so hang tight, buckle up, and get to
your keyboard.
We’re going to create a Rails news feed reader app with sharing; we’ll call it Shareup. This quickstart
will feature specifics about the app we’re building, but the process of integrating AngularJS and Rails
together is the same, no matter the app.
For this app, we’ll cover the following topics:
The Rails generator creates an entire Rails application stub in the shareup directory (or
whatever you decide to name your app) and will install local dependencies.
Once this step is complete, we change into the directory and start building our app.
Setting up our Rails app 6
Sqlite3¹⁵ is a file-based database that requires zero configuration and yet implements
the SQL language. Although it’s great for prototyping, it is not great for production
applications.
We’re going to set up a very basic user authorization system. Our authentication system will support
login through multiple providers so we can support login through Twitter and our local database
(this way, users can sign up without logging in through other providers).
In this book, we’ll focus on supporting Twitter; the process for setting up other providers will require
a similar process. Omniauth lists all of the providers it supports at https://github.com/intridea/omniauth/wiki/List-
of-Strategies¹⁶.
All of the modules supported by Devise can be found in the Devise README
https://github.com/plataformatec/devise¹⁷
To include Devise in our Rails project, we need to include it as a dependency in our Gemfile. The
Rails project comes with a Gemfile that’s generated for us at the root of our project.
Bundler is a dependency management system for Ruby and comes pre-baked into Rails.
You can find more information about Bundler on the Bundler website at bundler.io¹⁸.
¹⁵http://www.sqlite.org/
¹⁶https://github.com/intridea/omniauth/wiki/List-of-Strategies
¹⁷https://github.com/plataformatec/devise
¹⁸http://bundler.io/
Setting up our Rails app 7
1 source 'https://rubygems.org'
2 # ...
3 gem 'devise'
4 gem 'omniauth'
5 gem 'omniauth-twitter'
6 gem 'uuidtools'
Once our gem dependencies have been defined, let’s update our gems by running bundle install:
1 $ bundle install
Next, we’ll need to install Devise in the app. Devise comes with its own Rails generator, so this step
is really easy. It will install the initializer that describes all of the basic configuration options.
Upon the completion of this process, you may receive some manual setup instructions in your
terminal; ignore them for now – we will take care of these steps later.
Now that we have Devise and Omniauth installed, we can generate our models to build our app
with user and authentication. Generating a Devise model is equally as easy with the Rails generator:
From this point, we’ll have a user model and a migration that will be representative of our user
model. In order to support multiple Omniauth providers, we’ll add one more model that represents
our user authorizations called authorization:
Now in our generated migration, which will be located in the directory db/migrate/, we’ll make
sure we have the following attributes:
Setting up our Rails app 8
This code will be generated for you, almost in its entirety, so the only addition is that we’ve added
the :name attribute to our user. Some providers will give us back our user’s name; we’ll store that
attribute so that we can display it to the user when they log in.
These are the most basic settings that we’ll use on the user. Feel free to modify the
configuration settings to fit your needs.
1 $ rake db:migrate
Setting up our Rails app 9
Now, let’s go into our authorization model and add a Rails relationship (this relationship is already
set in our database above, as we’ve created the relationship with the user_id:integer attribute in
the database):
Next, we’ll configure Devise to work with Omniauth. To get set up with Twitter, we’ll need to head
to their developer center¹⁹ and create a new app.
Developer center
Sign in (or sign up if you don’t have a Twitter account) and click on My Applications to bring us
to the list of applications.
¹⁹http://dev.twitter.com
Setting up our Rails app 10
My applications
Create a new app with a unique name (Twitter will tell you if your name is not unique). Add a
description, and any URL will work right now. You can change this later, but Twitter requires that
you fill in this field.
Very important: Make sure you put in a callback URL. It should match the domain of the website
URL. It doesn’t matter if it’s going to be your final production URL, but it needs to be filled out.
For instance, if you enter http://andnowwefeed.com in the “Website” field, enter http://andnowwefeed.com/callba
in the “Callback URL” field.
Setting up our Rails app 11
Your keys
Once you’ve completed that form, you’ll find your Twitter OAuth keys available in the OAuth tool
section.
To configure Devise, we’ll edit the config/initializers/devise.rb file and add the following line,
where the “CONSUMERKEY” and “CONSUMERSECRET” are the two keys that twitter provided us
with.
In a production environment, we’ll want to create these keys either as environment variables or in
a separate config file. To do that, we’ll update the above initializer to add loading of a YAML file.
Add the following to a new initializer in config/initializers/app_config.rb with the content:
1 require 'ostruct'
2 require 'yaml'
3
4 config = YAML.load_file(
5 File.join(Rails.root, 'config', 'app_config.yml')) || {}
6 AppConfig = OpenStruct.new(config[Rails.env] || {})
Now, create the config/app_config.yml file and add this content to it:
Setting up our Rails app 12
1 defaults: &defaults
2 twitter:
3 clientId: "CONSUMERKEY"
4 clientSecret: "CONSUMERSECRET"
5
6 development:
7 <<: *defaults
8
9 test:
10 <<: *defaults
11
12 production:
13 <<: *defaults
Finally, change the config/initializers/devise.rb file to use these new values instead of the
hardcoded ones:
Let’s move on and create a callback controller that will handle our Omniauth callbacks. Since we
won’t need any views, helpers, or any other sugar that Rails gives us, we’ll create this controller
manually.
1 $ mkdir app/controllers/users/
2 $ touch app/controllers/users/omniauth_callbacks_controller.rb
Inside this controller, we have a single method named: twitter. This method handles all of our
Omniauth callbacks. Of course, we need to configure Rails so that it knows how to do that.
To set up Rails to route requests to our controller, we’ll need to configure the routes within the
config/routes.rb file. The Devise generator adds a default route to the file for us, but we’ll modify
the routes and add the omniauth_callbacks option:
Setting up our Rails app 13
1 devise_for :users,
2 :controllers => {
3 :omniauth_callbacks => "users/omniauth_callbacks"
4 }
Finally, we’ll need to set the user model to handle working with Omniauth and Devise. Devise makes
this step really simple, as well, by having Rails helpers on the model.
Edit the app/models/user.rb file and add the following lines:
Once we’ve taken care of that setup, we can start filling out our Omniauth handler. This Omniauth
handler will receive the oauth callback from the providers containing user data. We’ll use this data
to create an authorization and associate it with a user.
Inside the app/controllers/users/omniauth_callbacks_controller.rb file, update the twitter
method with the following:
Setting up our Rails app 14
43 end
44 end
45 end
This handler will essentially take the raw Twitter info object and parse it into interesting parts. We’ll
then look in our local user database to see if a user has already authenticated using Twitter. If they
have, then we’ll have a user in our database that is already associated with the Twitter account.
In that case, we’ll simply sign them in, updating their authorization with the latest login info from
Twitter. Then we’ll redirect the user to their original page that they tried to fetch.
With our oauth handler in place, we’re now ready to support user authorization.
Dealing with email confirmation is outside the scope of this book; however, Devise makes
the necessary support easy. Check the Devise wiki²⁰ for examples.
Our sharing model will need to know about the following information:
With that set, we’ll create a Rails resource that reflects these fields. Use the Rails generator again to
create our sharing resource:
²⁰https://github.com/plataformatec/devise/wiki
Setting up our Rails app 16
The resource generator will create a model and a controller for us. The controller will provide a
RESTful API that we can call from our AngularJS app. Additionally, the resource generator will add
a route in our config/routes.rb file.
We’ll need to make a quick modification to the routes so that we can isolate the Rails functionality.
Modify the config/routes.rb file to put the resources :shares route declaration into the :api
namespace:
1 Shareup::Application.routes.draw do
2 namespace :api do
3 resources :shares
4 end
5 # ...
1 $ rake db:migrate
Now, open the app/models/user.rb and add the has_many relationship to create the relationships
between the User and the Share:
We’ll now add the inverse relationship on the Share model. Open the model at app/models/share.rb
and add the belongs_to:
Finally, we’ll create another controller, a welcome controller that will handle public requests and
serve HTML. We’ll use the Rails generator again:
Setting up our Rails app 17
We’ll need to add a route in our config/routes.rb file to support the dashboard route.
With our AngularJS app, we’re only going to use a single controller to support displaying HTML
(this method will work for both styles of integrating AngularJS into our app). We’ll return to filling
in these controller methods when we get to integrating the front end. For the time being, we can
leave them empty.
In-depth testing is outside of the scope of this tutorial, so we’ll only discuss how to set our
testing up for our Rails app.
To set up testing in our app, we need to add the appropriate Devise helpers into our controller tests.
Devise comes with two built-in test helper methods – sign_in and sign_out – for our controller
tests.
These tests are available in the Devise::TestHelpers module. To include this module in our
controller tests, add the following to the test/test_helper.rb file:
1 class ActionController::TestCase
2 include Devise::TestHelpers
3 end
Setting up our Rails app 18
With this module in place, we can start setting up our controller tests. In our controller tests, we’re
interested in the WelcomeController logic, which:
• Renders the index view with the application template when there is no logged-in user
• Renders the dashboard view with an Angular template
• Redirects a non-logged-in user from dashboard to the index view
As you can see, we have three different actions we want to ensure that our application executes.
If we ran these tests for these actions right now, they’d fail. Create these two new views to make
them pass: app/views/welcome/dashboard.html.erb and app/views/welcome/index.html.erb.
As part of the first way we will integrate our AngularJS app with Rails, we will use a different layout
to show our AngularJS app. For the purposes of setting up our testing, we’ll only need to create a
second layout, but don’t need to fill it out quite yet.
Create the file app/views/layouts/angular.html.erb. For now, it doesn’t matter what goes into it.
Testing WelcomeController
In the file test/controllers/welcome_controller_test.rb, we’re going to add a test for each of
the three actions:
1 require 'test_helper'
2
3 class WelcomeControllerTest < ActionController::TestCase
4 setup do
5 @user = users(:one)
6 end
7
8 test "should render the index view without a user" do
9 get :index
10 assert_response :success
11 end
12
13 test "should render the dashboard view with the angular template" do
14 sign_in @user
15 get :dashboard
16 assert_template :dashboard
17 assert_template layout: "layouts/angular"
18 end
19
Setting up our Rails app 19
Now, let’s run our new tests and we’ll see that they will fail. We haven’t quite set the logic in the
WelcomeController yet.
1 $ rake test
Upon running this test, you’ll see that because we’ve added features to our app, we need to
reconfigure our default tests. You will receive an error stating “column email is not unique”; remove
the empty hashes in our /test/fixtures/users.yml file and replace each set with a unique email
address (one address for each fixture, “one” and “two”).
1 one:
2 name: 'ari'
3 email: [email protected]
4 two:
5 name: 'nate'
6 email: [email protected]
To add this logic, we’ll need to use the Devise helper :authenticate_user!. This helper ensures
that a user is logged in before directing them along a certain route. We’ll need to specify this helper
to run only on our dashboard route.
Secondly we’ll need to tell the WelcomeController to pick the appropriate layout for the view. To
do this, we’ll create a layout method that will choose the “right” layout for the view.
Let’s change our app/controllers/welcome_controller.rb file to match our logic:
10
11 def choose_layout
12 user_signed_in? ? "angular" : "application"
13 end
14 end
In both methods, we’ll need to add a component of security checking. By default, AngularJS provides
a mechanism to combat cross-site scripting attacks. Any request that uses the $http service will read
a token set in a cookie by the key of XSRF-TOKEN and set it as a header X-XSRF-TOKEN. Since browsers
only enable a site by the same domain the ability to read the cookie, it will ensure that the request
comes from HTML running on the same domain.
To enable this protection in our AngularJS app, we’ll need to add some middleware in our Rails app.
We’ll add an after_filter to our ApplicationController:
In order to fetch this on the front end, we’ll need to add this route to our config/routes.rb file:
Setting up our Rails app 22
1 devise_scope :user do
2 get '/api/current_user' => 'users/sessions#show_current_user'
3 end
1 $ rails server
Don’t forget to restart your server any time we modify the routes or any files outside of
the app.
At this point, if you were to run the app using the Rails server and request the current user route
(/api/current_user) and you were not logged in, you’d get this json object:
1 {
2 "success":false,
3 "info":"Unauthorized"
4 }
If you were logged in, you’d get back the current_user details.
Remember to write tests for this functionality. Check the code that is included with this
chapter for an example of how to write them.
10 end
11 private
12
13 def reject_if_not_authorized_request!
14 warden.authenticate!(
15 scope: resource_name,
16 recall: "#{controller_path}#failure")
17 end
18 end
With this controller in place, we only need to add a route to this controller in our config/routes.rb
file:
1 # ...
2 devise_scope :user do
3 get '/api/current_user' => 'users/sessions#show_current_user', as: 'show_curren\
4 t_user'
5 post '/api/check/is_user' => 'users/users#is_user', as: 'is_user'
6 end
7 # ...
Now that we have our Rails app set up, let’s start integrating AngularJS into it.
Angular in the Rails asset pipeline
In this first integration method, we’re going to use the sprockets asset pipeline to deliver our
Angular app. This ability comes baked into Rails already, so we don’t need to do any special
configuration to support Angular.
Using the asset pipeline is a particularly good way to deliver Rails apps that need login capabilities.
We’ll never have to worry about authentication on the client side, since the Angular app will only
be delivered to the client after a user has signed in.
We can also render custom data inside of our JavaScript files (using Rails’s embedded erb) to pass
in configuration data. The Angular app can launch with details about the server-side environment
without needing to make a request.
Inside of this approach, we’ll do our work in the directories as follows:
• app/assets/ - We’ll write all of our custom JavaScript, stylesheets, images, etc., here.
• lib/assets - We’ll place all of our libraries in here.
• vendor/assets - This directly is where we’ll place all of our assets that we’re loading from
other authors, such as Twitter Bootstrap and custom Angular libraries.
In order to set up our app such that it is ready to deliver Angular apps, we can either download
and embed the necessary JavaScripts in our vendor/assets directory or we can use the Rails
angularjs-rails gem, which is a thin wrapper around the Angular libraries.
In this approach, we’ll use the second method, including the library in our Gemfile. Add it as a gem
dependency in the Gemfile. We’ll also include the ngmin-rails gem that will take care of running
the pre-minifier for us.
Angular in the Rails asset pipeline 25
Feel free to include other style gems in your Gemfile as well, such as Twitter’s Bootstrap or the Zurb
Foundation gem:
If the rubygem turbolinks is listed in your Gemfile, make sure you remove it. Turbolinks
will affect the Angular app development, so it’s just easier to not deal with the feature. If
you do want to use turbolinks, then you’ll have to manually bootstrap your AngularJS
app, which is outside the scope of this book.
1 $ bundle install
Once the above is all set, we’re ready to start developing our Angular app. We need to set up our
custom layout to bootstrap the Angular app.
In the app/views/layouts/angular.html.erb, we’ll embed our AngularJS app:
Angular in the Rails asset pipeline 26
1 <!DOCTYPE html>
2 <html lang="en" data-ng-app="myApp">
3 <head>
4 <meta charset="utf-8">
5 <meta http-equiv="X-UA-Compatible" content="IE=Edge,chrome=1">
6 <meta name="viewport" content="width=device-width, initial-scale=1.0">
7 <title><%= content_for?(:title) ? yield(:title) : "shareup" %></title>
8 <%= csrf_meta_tags %>
9 <%= stylesheet_link_tag "application", :media => "all" %>
10 </head>
11 <body>
12 <div class="container">
13 <h1>Text!</h1>
14 <div data-ng-view></div>
15 </div> <!-- /container -->
16 <%= javascript_include_tag "application" %>
17 </body>
18 </html>
We need to include Angular in our JavaScript. The Rails asset pipeline makes this task easy, as we’ll
only need to reference the Angular libraries in a comment in the application.js file:
Notice that our app is bootstrapped with the ng-app directive in the HTML tag. Now, when we load
a page that the user has been authorized to load, she will get the Angular app (which currently
contains the heading “Text!”), while the non-authorized user will only see the default HTML page
(no “Text!”).
Lastly, let’s build our Angular app. We’ll build our app in its own directory in the assets folder so
we can keep our Angular app isolated from the rest of our non-Angular app: Create the directory
app/assets/javascripts/app/.
Add the file app/assets/javascripts/app/app.js to your new directory and place the following
content inside of it:
Angular in the Rails asset pipeline 27
1 angular.module('myApp', ['ngRoute']);
We’re including ngRoute as a dependency for our app, so we can support routing.
Now, if you start your Rails app and navigate to http://localhost:3000/users/sign_in (or
/sign_up), you’ll see that you can log in and register new users into your app. Importantly, you
can also see that your Angular app boots up after you log in (you should see “Text!”).
1 $ rails server
1 angular.module('myApp', ['ngRoute'])
2 .config(function($routeProvider) {
3 $routeProvider.when('/', {
4 templateUrl: '/templates/dashboard.html',
5 controller: 'HomeController'
6 })
7 .otherwise({redirectTo: '/'});
8 });
1 <h2>Dashboard page</h2>
In accordance with AngularJS best practices, to create a HomeController, we need to create a new
JavaScript file and a new Angular module. Create a new file and place the following content inside
File: app/assets/javascripts/app/controllers.js
Angular in the Rails asset pipeline 28
1 angular.module('myApp.controllers', [])
2 .controller('HomeController', function($scope) {
3 });
Note that, since we’re using the ngmin pre-minifier, we do not need to inject the dependen-
cies into our app.
Lastly, we need to make sure our new module is a dependency for our app module. Let’s edit our
app/assets/javascripts/app/app.js to include it:
At this point, let’s create a link to the sign-in page on the home page. In our index.html.erb file,
let’s add:
Now, if you are not signed in and you load the page using the Rails server, you will see the index
page, which contains a link to the sign-in page (obviously, we will fill in our index.html further in
a bit).
If you are signed in and load the page using the Rails server, at this point, the AngularJS app will
load, and the browser will render the contents of the dashboard.html page.
Fetching articles
As we mentioned previously, we’re building an app that will share articles loaded by the front end.
To fetch our articles on the front end, we’ll build an articles service that loads the latest articles and
a sharing service that communicates back with our Rails API.
First, let’s build the sharing service. In this sharing service, we’re going to load articles from the
Huffington Post’s latest RSS feed. Using Google’s feed service, we can fetch articles without needing
to build a feed loader on our back end.
Just like we did with our controllers file, let’s save a new file in app/assets/javascripts/app/services.js
and create a new Angular module:
1 angular.module('myApp.services', []);
As before, we need to make sure we include this module as a dependency in our app, as well. Let’s
edit the app/assets/javascripts/app/app.js to include it:
Angular in the Rails asset pipeline 29
Great! Now, to build out the ArticleService, we can simply load the feed:
1 angular.module('myApp.services', [])
2 .factory('ArticleService', function($http, $q) {
3 var service = {
4 getLatestFeed: function() {
5 var d = $q.defer();
6 $http.jsonp('http://ajax.googleapis.com/ajax/services/feed/load' +
7 '?v=1.0&num=50&callback=JSON_CALLBACK&q='+
8 encodeURIComponent(
9 'http://feeds.huffingtonpost.com/huffingtonpost/raw_feed'
10 )
11 ).then(function(data, status) {
12 // Huffpost data comes back as
13 // data.data.responseData.feed.entries
14 if (data.status === 200)
15 d.resolve(data.data.responseData.feed.entries);
16 else
17 d.reject(data);
18 });
19
20 return d.promise;
21 }
22 };
23
24 return service;
25 });
Now, inside of our controller we can fetch the latest articles and store them on our $scope. In the
app/assets/javascripts/app/controllers.js:
Angular in the Rails asset pipeline 30
1 angular.module('myApp.controllers', [])
2 .controller('HomeController',
3 function($scope, ArticleService) {
4 ArticleService.getLatestFeed()
5 .then(function(data) {
6 $scope.articles = data;
7 })
8 });
Let’s update our HTML so we can see our service working. Edit your public/templates/dashboard.html
so that it includes a list of articles, with their headlines and links to the original article:
1 <div>
2 <ul>
3 <li ng-repeat="article in articles | orderBy:publishedDate:false">
4 <div class="article-listing">
5 <div class="large-11 small-11 columns">
6 <h2>
7 <a
8 href="{{ article.link }}"
9 title="{{ article.title }}">
10 {{ article.title }}
11 </a>
12 </h2>
13 <p>
14 {{ article.publishedDate | date:'M/d/yy h:mm:ss a' }}
15 </p>
16 </div>
17 </div>
18 </li>
19 </ul>
20 </div>
Now, when you load the Rails app page, you’ll see that the page loads with a list of the latest
Huffington Post articles.
Angular in the Rails asset pipeline 31
First launch
1 angular.module('myApp', ['ngRoute',
2 'ngResource',
3 'myApp.controllers',
4 'myApp.services' ])
5 // ...
Now, let’s create our Share resource in app/assets/javascripts/app/services.js. We’ll also need
to attach a current user to the request, so let’s build out our SessionService as well:
Angular in the Rails asset pipeline 32
1 angular.module('myApp.services', [])
2 .factory('ArticleService', function($http, $q) {
3 // ArticleService from above
4 })
5 .factory('Share', function($resource) {
6 })
7 .factory("SessionService", function() {
8 });
In this service, we’ll use the $http and $q services, which are built into AngularJS by default. Update
the SessionService in app/assets/javascripts/app/services.js like so:
1 angular.module('myApp.services', ['ngResource'])
2 // ...
3 .factory("SessionService", function($http, $q) {
4 var service = {
5 getCurrentUser: function() {
6 if (service.isAuthenticated()) {
7 return $q.when(service.currentUser);
8 } else {
9 return $http.get('/api/current_user').then(function(resp) {
10 return service.currentUser = resp.data;
11 });
12 }
13 },
14 currentUser: null,
15 isAuthenticated: function() {
16 return !!service.currentUser;
17 }
18 };
19 return service;
20 });
Angular in the Rails asset pipeline 33
Now we can either load this current user when the controller is instantiated or we can load it when
the controller loads. Since we’ll need it when the controller loads anyway, we’ll set it to resolve upon
the resolution of the route.
To do that, we’ll update the app/assets/javascripts/app/app.js like so:
Now, when our HomeController boots up, we can ensure that the currentUser from our SessionService
will be injected into it. In this case, we can fetch the result of the service request in the HomeController
by using the injected value. In app/assets/javascripts/app/controllers.js:
1 angular.module('myApp.controllers', [])
2 .controller('HomeController',
3 function($scope, session, SessionService, ArticleService, Share) {
4 $scope.user = session.user;
5 // ...
If we update our view, we can see the user is populated for us:
We’ll add a link in the view that allows the user to click on it and share it with a friend, either to a
registered user on the site or to a non-user via email.
Let’s first create the view. We’ll simply add a link that reveals a div containing an input field. This
input field binds to a share model in our controller:
To see the entire dashboard.html page, check out the example source distributed with this
book.
Notice that we’re binding the input to the recipient property of the newShare object that we’ll bind
on our scope. It’s good practice to bind an input value to a specific property on an object rather than
as a value, on account of how JavaScript passes by value or by reference.
When the form is submitted, we’ll call the share() method on the controller. This method on the
HomeController looks like:
Angular in the Rails asset pipeline 35
1 angular.module('myApp.controllers', [])
2 .controller('HomeController',
3 function($scope,
4 session,
5 SessionService,
6 ArticleService,
7 Share) {
8 ArticleService.getLatestFeed()
9 .then(function(data) {
10 $scope.articles = data;
11 });
12 $scope.user = session.user;
13 $scope.newShare = {recipient: ''};
14 $scope.share = function(recipient, article) {
15 var share = new Share({
16 url: article.link,
17 from_user: $scope.user.id,
18 user: recipient
19 });
20 share.$save();
21 $scope.newShare.recipient = '';
22 }
23 });
As you can see, we’re going to use the Share service to save our new share. This Share service will
wrap an AngularJS $resource that we will use to talk to our Rails resource. We’re also clearing the
newShare variable so that the next time the user clicks on the share button, they’ll have an empty
recipient input box.
In our app/assets/javascripts/app/services.js file, let’s fill out the Share service:
1 angular.module('myApp.services', [])
2 // ...
3 .factory('Share', function($resource) {
4 var Share = $resource('/api/shares/:id.json',
5 {id: '@id'},
6 {}
7 );
8 return Share;
9 })
10 // ...
The $resource service creates a resource object that makes it incredibly easy to interact with RESTful
Angular in the Rails asset pipeline 36
server-side data sources. This resource object is an ideal abstraction for working with Rails resources
and helps us hide away complexities when dealing with Rails resources.
As you can see, we’re setting up our Share resource to interact with the Rails route at /api/shares/:id.json.
In order for this setup to work on the Rails side, we’ll only need to add the methods we want to
support to the SharesController.
Now, when we submit the form (or click share), we’re creating our share, and we get back an id from
the user.
Client-side validation
We can do a lot better in terms of client-side form validation for the user. The behavior that we
want is for our app to support the user’s ability to input a username (that’s in our database) or an
email and react under the circumstances:
• If the user exists, visually show the user that it is a known user in our database.
• If the user does not exist, then visualize this outcome for them or show them that the email
they have entered is valid.
Note that what we’re describing only supports client-side validation. On the back end, we’ll want
to enforce that only emails are stored in the Share model. To do that, we’ll use the built-in Rails
validations on our Share model:
In app/models/share.rb, make sure to add the content:
Now, on the front end we’ll need to show the user whether they have, in fact, typed in a valid
username or email address; we’ll create a custom validation directive.
Creating a directive
As with services.js and controllers.js, create a new file in app/assets/javascripts/app/
called directives.js and add the following content:
1 angular.module('myApp.directives', []);
Again, we need to make sure that we include this module as a dependency for the app in
app/assets/javscripts/app/app.js:
Angular in the Rails asset pipeline 38
1 angular.module('myApp', ['ngRoute',
2 'myApp.controllers',
3 'myApp.services', 'myApp.directives'])
4 // ...
Now we’re going to create a live check for whether a user is actually a user in our local database.
We’ll use this directive to check in with the Rails server and see if the input field contains the value
of a real user or not.
We’ll also use this directive to check if the input, if not a user, is a valid email address.
1 angular.module('myApp.directives', [])
2 // Our is-user-or-email validation
3 .directive('isUserOrEmail', function($http, $timeout, $filter, $q) {
4 // we're checking using the api `is_user` if the user
5 // input is already a user
6 var isUser = function(input) {
7 // We're returning a deferred promise
8 var d = $q.defer();
9
10 if (input) {
11 $http({
12 url: '/api/check/is_user',
13 method: 'POST',
14 data: { 'name': input }
15 }).then(function(data) {
16 if (data.status == 200){
17 d.resolve(data.data);
18 } else {
19 d.reject(data.data);
20 }
21 });
22 } else {
23 d.reject("No input");
24 }
25
26 return d.promise;
Angular in the Rails asset pipeline 39
27 };
28
29 var checking = null,
30 emailRegex = /^[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,4}$/;
31 return {
32 restrict: 'A',
33 require: 'ngModel',
34 link: function(scope, ele, attrs, ctrl) {
35 // Anytime that our ngModel changes, we're going to check if the
36 // value is a user with the function above
37 // If it is a user, then our field will be valid, if it's not
38 // check if the input is an email
39 scope.$watch(attrs.ngModel, function(v) {
40 if (checking) clearTimeout(checking);
41
42 var value = scope.ngModel.$viewValue;
43
44 checking = $timeout(function() {
45 isUser(value).then(function(data) {
46 if (data.success) {
47 // Is a user
48 checking = null;
49 ctrl.$setValidity('isUserOrEmail', true);
50 } else {
51 // Is an email
52 if (emailRegex.test(value)) {
53 checking = null;
54 ctrl.$setValidity('isUserOrEmail', true);
55 } else {
56 checking = null;
57 ctrl.$setValidity('isUserOrEmail', false);
58 }
59 }
60 });
61 // Delay this check by 200 milliseconds to give
62 // the keyboard time to settle down
63 }, 200);
64 });
65 }
66 };
67 });
Angular in the Rails asset pipeline 40
Let’s break this code down: We’ve created an isUser function that will check if the user is a registered
user using our /api/check/is_user route. When the response comes back, we’ll check to see if
success returns as true. If so, the user is registered.
If the API returns success as false, then we’ll check to see if the input is at least an email, using an
email regex. If this regex passes as true, then the input matches an email, and we can set the form
as valid. If it doesn’t pass the email test, then the form is invalid.
Now we can add the is-user-or-email attribute on our input field in our form. This directive will
set the form as valid or invalid. We can control the disabled status of the submit button using the
ng-disabled directive that comes with Angular out of the box.
With this directive in place, the form will only validate when encountering users that are registered
in the database or valid emails.
We leave it as an exercise for you to create a nicer user experience by incorporating an auto-complete
and creating CSS classes, etc.
Now, we can finally update our public/templates/dashboard.html to include our article listing
and our share functionality:
Angular in the Rails asset pipeline 41
1 <nav class="top-bar">
2 <ul class="title-area">
3 <!-- Title Area -->
4 <li class="name">
5 <h1><a href="#">Welcome back {{ user.name }} </a></h1>
6 </li>
7 <li class="toggle-topbar menu-icon"><a href="#"><span>Menu</span></a></li>
8 </ul>
9 <section class="top-bar-section">
10 <!-- Right Nav Section -->
11 <ul class="right">
12 <li class="divider show-for-small"></li>
13 <li>
14 <a href="/users/sign_out">Sign out</a>
15 </li>
16 </ul>
17 </section>
18 </nav>
19 <div class="container">
20 <div class="row">
21 <ul class="large-12 columns">
22 <li class="row"
23 data-ng-repeat="article in articles | orderBy:publishedDate:false">
24 <div class="article-listing">
25 <div class="large-10 small-10 columns">
26 <h2><a href="{{ ngModel.link }}" title="{{ ngModel.title }}">
27 {{ ngModel.title }}
28 </a></h2>
29 <p>
30 {{ ngModel.publishedDate | toDate |date:'M/d/yy h:mm:ss a' }}
31 </p>
32 </div>
33 <div class="small-2 large-2 columns">
34 <div data-ng-hide="showShareBox">
35 <a ng-click="showShareBox=!showShareBox">Share with a friend</a>
36 </div>
37 <div data-ng-show="showShareBox">
38 <form name="shareForm">
39 <label>Share with a friend</label>
40 <input type="text"
41 placeholder="email or username"
42 ng-model="newShare.recipient"
Angular in the Rails asset pipeline 42
43 is-user-or-email="">
44 <input type="button" class="btn"
45 value="Cancel"
46 data-ng-click="showShareBox = !showShareBox">
47 <input type="submit"
48 class="btn"
49 ng-click="showShareBox=!showShareBox;share()"
50 ng-disabled="shareForm.$invalid"
51 value="Share">
52 </form>
53 </div>
54 </div>
55 </div>
56 </li>
57 </ul>
58 </div>
59 </div>
Latest launch
Angular using Rails as an API
In the second integration, we’re going to go rails-free in delivering our browser-side assets. We’re
going to free ourselves from the constraints of the Rails pipeline and create the app independently.
The advantages of this approach include:
In using this style of development, our delivered app for both public and private use will be our
Angular app.
In this method, we’re going to concern ourselves with our Rails app’s controllers and models. In
fact, we can even remove the app/assets, app/helpers, and app/views folders from the Rails app
we built in the “Setting up our Rails app” chapter. We will build on top of that same Rails app.
1 $ rm -rf app/{assets,helpers,views}
To get started using Yeoman, we’ll need to install it. Make sure that you have Node.js²² installed.
With that, we’ll use the npm package manager:
1 $ npm install -g yo
We’ll also need to install the Angular generator that comes packaged as a separate component:
Once we have the requirements installed, we can generate our client-side view. We’ll run the Yeoman
Angular generator in our app. Create a directory in your Rails (Shareup, or whatever else you named
the app we’re building) folder called client/. This client/ directory is where we’ll work with our
client app.
If you prefer to have two completely independent directories, one for your Rails app and
one for your client app, you can do that too. In the interest of dealing with a single directory,
we’ll be working in a single directory.
Now, run the generate command inside your new client/ directory:
1 $ yo angular shareup
If you get an error such as command not found: yo when running the yo command, check
that your PATH includes the proper npm paths. Take a look at the docs for the default paths
on your system. In OSX, make sure your PATH includes the /usr/local/share/npm/bin
directory.
The generator will ask you a few questions, such as whether you want to include Twitter Bootstrap
and other Angular resources in this app.
²²http://nodejs.org
Angular using Rails as an API 45
Once the plugin has been installed, we’ll need to modify a few different parts of our Gruntfile.js
file in order to use the proxy.
First, we need to load the proxySnippet. At the top of the Gruntfile.js file, below ‘var mount-
Folder’, add the line:
Angular using Rails as an API 46
1 // ...
2 var lrSnippet = require('connect-livereload')({ port: LIVERELOAD_PORT });
3 var mountFolder = function (connect, dir) {
4 return connect.static(require('path').resolve(dir));
5 };
6
7 var proxySnippet = require('grunt-connect-proxy/lib/utils').proxyRequest;
8 // ...
Next, we need to add a configuration section to the connect configuration in our Gruntfile. Doing
so will create a middleware that will forward all requests from /api onward to the server we list.
Modify the section so that it includes the proxies snippet like so:
1 // ...
2 connect: {
3 options: {
4 port: 9000,
5 // change this to '0.0.0.0' to access the server from outside
6 hostname: 'localhost'
7 },
8 proxies: [
9 {
10 context: '/api',
11 host: 'localhost',
12 port: 3000
13 }
14 ],
15 livereload: {
16 // ...
We’ll want to use the proxy in our livereload middleware, so we’ll just need to add our
proxySnippet in the livereload middleware configuration object. Modify the livereload section
like so:
Angular using Rails as an API 47
1 // ...
2 livereload: {
3 options: {
4 middleware: function (connect) {
5 return [
6 proxySnippet,
7 lrSnippet,
8 mountFolder(connect, '.tmp'),
9 mountFolder(connect, yeomanConfig.app)
10 ];
11 // ...
Lastly, we’re going to want to tell Grunt to configure our proxies alongside the Grunt tasks that run
when we start working with our app. Add the 'configureProxies' line in the following two places
in the task definition:
1 // ...
2 grunt.task.run([
3 'clean:server',
4 'concurrent:server',
5 'configureProxies', // Add this line
6 'autoprefixer',
7 'connect:livereload',
8 'open',
9 'watch'
10 ]);
11 });
12
13 grunt.registerTask('test', [
14 'clean:server',
15 'configureProxies', // and add this line
16 'concurrent:test',
17 'autoprefixer',
18 'connect:test',
19 'karma'
20 ]);
21 // ...
Great! Now we can make requests from the client-side app, and they will be forwarded to our Rails
app.
Angular using Rails as an API 48
1 $ rails server
1 $ grunt server
If you haven’t already installed Grunt’s CLI, you’ll want to do that now:
You’ll notice that Grunt opens your web browser for you when it starts up. Additionally, Yeoman
comes with livereload by default, which reloads the page any time that a change on any of the
files in the client/app directory is detected (such as after a file is saved or deleted).
Angular using Rails as an API 49
Developing Shareup
When using Yeoman and the Angular generator, all of our dependencies are already installed, so we
are ready to start developing immediately.
The index.html template contains direct references to all of our dependencies. We will only need to
touch this page when we’re adding dependencies, changing meta tags, and handling other general
configuration.
The Angular template already includes a <div ng-view></div> tag, so we will be spending most of
our time in the client/app/views directory.
Open the client/app/views/main.html file and you’ll find the default Angular content that loads
up in the first view of the page.
We’re implementing login through using a client-side auth_token. Although this will
work for clients using the same pattern, it’s not the securest method of authentication
possible. For simplicity, we will stick with token authorization.
As we’re working with our client on our own domain, CORS (cross-origin resource sharing) is not
entirely necessary; however, if you are going to host your Rails app on a different server, we’ll need
to implement it.
If you are going to host the client on the same domain, you do not need to implement
CORS and can skip to the next section.
1 defaults: &defaults
2 client:
3 origin: http://localhost
4 twitter:
5 clientId: "CONSUMERKEY"
6 clientSecret: "CONSUMERSECRET"
7
8 development:
9 <<: *defaults
10
11 test:
12 <<: *defaults
13
14 production:
15 <<: *defaults
These changes will set the proper HTTP headers for the response from our server. When you’re
ready to deploy this site in production, it will be important to add the eventual domain in the config
in order to enable CORS for your production domain.
Angular using Rails as an API 51
OPTIONS
The OPTION action is really just a way for the client to see whether the server has CORS enabled.
We’re really just going to render an empty page. It’s important that we implement it, but it’s not
important what we return, so we’ll just return a string of some kind.
We’re including a before filter that returns an empty response if the request method is :options.
Authentication
Devise includes the :token_authenticatable extension by default, but we need to tell Devise to
use it.
We’ll create a new migration to add an authentication token to our user model:
It’s a good idea to add an index to our new field, as we’ll be searching the database for this value.
Open db/migrations/[date]_add_auth_token_to_users.rb, and add add_index line as in below:
Now run the migrations with rake, and we’re done modifying our database:
1 $ rake db:migrate
In order to enable the extension with Devise, we’ll need to tell Devise about it. In our config/initial-
izers/devise.rb, add this line, which enables the token_authentication_key:
Angular using Rails as an API 52
1 Devise.setup do |config|
2 # ...
3 config.token_authentication_key = :auth_token
4 # ...
We’ll also want to make sure that the authentication_token actually generates when the user is
saved, so we’ll add a before filter that ensures token generation for us. Open app/models/user.rb
and ensure that it matches the listing below:
Now that we have token authentication up and ready to go, we’re going to extend Devise to enable
us to deliver the same Devise experience in the front end as we have in the back end.
First, we’ll need to create two new controllers, one to extend registrations (signing up new users)
and one to manage login and logout.
Now we’ll need to add this registrations controller in the routes as the controller that’s responsible
for handling the registrations for Devise:
1 Shareup::Application.routes.draw do
2 scope '/api' do
3 devise_for :users,
4 :controllers => {
5 omniauth_callbacks: "users/omniauth_callbacks",
6 registrations: "users/registrations"
7 }
8 end
9 end
10 end
Angular using Rails as an API 54
1 // ...
2 .config(function ($routeProvider) {
3 $routeProvider
4 .when('/', {
5 templateUrl: 'views/main.html',
6 controller: 'MainCtrl'
7 })
8 .when('/login', {
9 templateUrl: 'views/login.html',
10 controller: 'LoginCtrl'
11 })
12 .otherwise({
13 redirectTo: '/'
14 });
15 })
16 // ...
1 angular.module('shareupApp')
2 .controller('LoginCtrl', function() {});
Now, whenever our user visits /login, they’ll see the contents of our login.html. This login.html
view has two actions, a sign up and a login action. Our app calls the login() action when the form
is submitted, while it calls the signup() action when the user clicks on the Sign up button.
To create the signup functionality, we’ll create an action in our LoginCtrl controller. It will POST to
the API endpoint with our user’s credentials. If they are correct, then we’ll send the user to the root
route. If they are not, then we’ll handle those and display the error to the user.
In the LoginCtrl, let’s add the signup method:
1 // ...
2 .controller('LoginCtrl', function ($location, $scope, $http) {
3 $scope.signup = function() {
4 $http({
5 url: '/api/users',
6 method: 'POST',
7 data: {
8 user: $scope.user
9 }
10 }).success(function(data) {
11 $scope.$broadcast('event:authenticated');
12 $location.path('/');
13 }).error(function(reason) {
14 $scope.user.errors = reason;
15 });
16 };
17 // ...
Angular using Rails as an API 56
Now, we’ll need to keep our auth_token around for every request we make back to the server, so
we’ll need a service to hold on to it for us.
Create a new directory at client/app/scripts/services/ and add the file token_handler.js. Add
the following into it:
1 angular.module('shareupApp')
2 .factory('tokenHandler', function($rootScope, $http, $q, $location) {
3 var token = null,
4 currentUser;
5
6 var tokenHandler = {
7 set: function(v) { token = v; },
8 get: function() {
9 if (!token)
10 $rootScope.$broadcast('event:unauthorized');
11 else
12 return token
13 }
14 };
15
16 return tokenHandler;
17 });
We’ll need to include the token handler in our index.html layout, so we can use it. Open
client/app/index.html and add the line:
Now, upon signup, we’ll want to set that token as the one we use in the rest of our requests. Modify
the LoginCtrl at client/app/scripts/controllers/login.js with:
1 // ...
2 }).success(function(data) {
3 tokenHandler.set( data.auth_token );
4 $location.path('/');
5 // ...
Now, our auth token will be waiting for us when we need to make an authenticated request.
Angular using Rails as an API 57
1 # app/controllers/users/sessions_controller.rb
2 class Users::SessionsController < Devise::SessionsController
3
4 skip_before_filter :verify_authenticity_token
5
6 before_filter :authenticate_user!, except: [:create]
7 respond_to :json
8
9 def create
10 resource = User.find_for_database_authentication(email: params[:user][:email])
11 return failure unless resource
12 return failure unless resource.valid_password?(params[:user][:password])
13
14 render status: 200,
15 json: {
16 success: true,
17 info: "Logged in",
18 data: {
19 auth_token: current_user.authentication_token
20 }
21 }
22 end
23
24 def failure
25 warden.custom_failure!
26 render status: 200,
27 json: {
28 success: false,
29 info: "Login failed",
30 data: {}
31 }
32 end
33 end
We’ll make the following modifications to our config/routes.rb: Add this controller as the
sessions controller for Devise, and place the shares resource under the API scope. Your file should
now look like:
Angular using Rails as an API 58
1 Thirdshareup::Application.routes.draw do
2 scope '/api' do
3 resources :shares
4
5 devise_for :users,
6 :controllers => {
7 omniauth_callbacks: "users/omniauth_callbacks",
8 registrations: "users/registrations",
9 sessions: "users/sessions"
10 }
11 end
12
13 get '/dashboard' => 'welcome#dashboard'
14 root to: 'welcome#index'
15 end
Now that we’re using Rails for the sole purpose of serving our API, we need to make some
changes to our routes; hence, we are nesting all of our routes under API, as above, and no
longer need the namespace we previously had in place.
Once our functionality is defined, we’ll need to inject the $location service to handle redirects, the
$http service to make the HTTP request and handle fulfilling promises, and our tokenHandler.
Let’s modify the login controller at client/app/scripts/controllers/login.js to include these
injected services:
Angular using Rails as an API 59
1 angular.module('shareupApp')
2 .controller('LoginCtrl',
3 function($scope, $location, $http, tokenHandler) {
4 });
Now that our services are injected into our controller, we can use them in our login() function:
1 angular.module('shareupApp')
2 .controller('LoginCtrl', function ($scope, $location, $http, tokenHandler) {
3 $scope.login = function() {
4 $http({
5 url: '/api/users/sign_in',
6 method: 'POST',
7 data: {
8 user: $scope.user
9 }
10 }).success(function(data) {
11 if (data.success) {
12 $scope.ngModel = data.data.data;
13 tokenHandler.set(data.data.auth_token);
14 $location.path('/');
15 } else {
16 $scope.ngModel = data;
17 $scope.user.errors = data.info;
18 }
19 }).error(function(msg) {
20 $scope.user.errors =
21 "Something is wrong with the service. Please try again";
22 });
23 };
24 });
This action will call the /api/users/sign_in call. If we’re unsuccessful, then we’ll display those
errors to the user.
At this point, our users can still reach / without being logged in, which is not very secure, so let’s
take care of that.
To handle redirecting the user upon any forbidden requests, we’ll create a $http interceptor. This
object will intercept all requests that are sent back with a 401 response code.
In this case, we’re going to send Angular events across our app so we can handle these actions
appropriately.
Angular using Rails as an API 60
Using events will make it easy to change the behavior across the system based upon specific
conditions. In this case, we’re only going to send a redirect, but it’s possible that we can
handle unauthenticated requests in a completely different fashion.
Let’s create the interceptor. In the client/app/scripts/app.js file, add a new config section like
so:
1 // ...
2 .config(function($httpProvider) {
3 var interceptor = ['$rootScope', '$location', '$q',
4 function($scope, $location, $q) {
5 var success = function(resp) { return resp; },
6 err = function(resp) {
7 if (resp.status == 401) {
8 var d = $q.defer();
9 $scope.$broadcast('event:unauthorized');
10 return d.promise;
11 };
12 return $q.reject(resp);
13 };
14
15 return function(promise) {
16 return promise.then(success, err);
17 }
18 }];
19 $httpProvider.responseInterceptors.push(interceptor);
20 })
Any request we send that comes back with a 401 will send the event:unauthorized event tumbling
through our app.
We can also take action on this event by attaching a listener to it that will redirect the user to the
/login page. We’ll create this in a run block in the same file (client/app/scripts/app.js):
1 $ rails server
1 $ grunt server
Open your browser to http://localhost:9000/, and you’ll see that nothing has changed yet. Since
we haven’t fetched any API calls to the server, we haven’t actually received a 401 response yet.
To make an API call when the page launches (in this app, we only want to serve authenticated
requests), we’ll need to launch a request upon fetching the page. We’ll use Angular’s resolve
property in our routes to resolve our auth_token upon page load.
Let’s modify our $routeProvider in the file client/app/scripts/app.js to include the resolve
property, like so:
1 // ...
2 .config(function ($routeProvider) {
3 $routeProvider
4 .when('/', {
5 templateUrl: 'views/main.html',
6 controller: 'MainCtrl',
7 resolve: {
8 token: function(tokenHandler) {
9 return tokenHandler.get();
10 }
11 }
12 })
13 .when('/login', {
14 templateUrl: 'views/login.html',
15 // ...
Now, if you refresh your browser (Grunt will refresh it for you) you’ll see that you are actually at
the login page.
Angular using Rails as an API 62
Login page
To make an authenticated API call with the token, we’ll update the isUserOrEmail directive we
created in the previous chapter. If you don’t completely recall (or skipped the last chapter), this
directive will validate the input of our share input to ensure that the input is either a known user
in the system or that it is a valid email address. The source of our isUserOrEmail directive is given
below.
Create a new controller at app/controllers/users/users_controller.rb. This controller will
handle all normal user interaction in our API (it comprises just one call right now).
Add the content:
Angular using Rails as an API 63
And we’ll need to add a route that points to that action at /api/check/is_user:
1 # ...
2 devise_scope :user do
3 post '/check/is_user' => 'users/users#is_user',
4 as: 'is_user'
5 end
6 # ...
Note that we are no longer posting to api/check/is_user, as we are already within our
‘/api’ scope.
Now, on the front end, we will need to show the user whether she has, in fact, typed in a valid
username or email address. To do that, we’ll create our custom validation directive, as previously
mentioned.
As we did with client/app/scripts/controllers/ and client/app/scripts/services/, let’s
create a new client/app/scripts/directives/ directory and add a file to it called is_user_or_-
email.js. Open that file, and let’s create our directive inside of our Angular module:
1 angular.module('shareupApp')
2 .directive('isUserOrEmail', function($http, $timeout, $filter, $q)
Now we’ll want to create a live check to determine whether a user is actually a user in our local
database. This directive works by checking in with the Rails server to see whether the input field
contains a value of a real user. We’ll also use this directive to check if the input, if not a username,
is a valid email address.
Add this content to your client/app/scripts/directives/is_user_or_email.js file:
Angular using Rails as an API 64
1 'use strict';
2
3 angular.module('shareupApp')
4 // Our is-user-or-email validation
5 .directive('isUserOrEmail', function($http, $timeout, $filter, $q) {
6 // we're checking using the api `is_user` if the user
7 // input is already a user
8 var isUser = function(input) {
9 // We're returning a deferred promise
10 var d = $q.defer();
11
12 if (input) {
13 $http({
14 url: '/api/check/is_user',
15 method: 'POST',
16 data: { 'name': input }
17 }).then(function(data) {
18 if (data.status == 200){
19 d.resolve(data.data);
20 } else {
21 d.reject(data.data);
22 }
23 });
24 } else {
25 d.reject("No input");
26 }
27
28 return d.promise;
29 };
30
31 var checking = null,
32 emailRegex = /^[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,4}$/;
33 return {
34 restrict: 'A',
35 require: 'ngModel',
36 link: function(scope, ele, attrs, ctrl) {
37 // Anytime that our ngModel changes, we're going to check if the
38 // value is a user with the function above
39 // If it is a user, then our field will be valid, if it's not
40 // check if the input is an email
41 scope.$watch(attrs.ngModel, function(v) {
42 if (checking) clearTimeout(checking);
Angular using Rails as an API 65
43
44 var value = scope.ngModel.$viewValue;
45
46 checking = $timeout(function() {
47 isUser(value).then(function(data) {
48 if (data.success) {
49 // Is a user
50 checking = null;
51 ctrl.$setValidity('isUserOrEmail', true);
52 } else {
53 // Is an email
54 if (emailRegex.test(value)) {
55 checking = null;
56 ctrl.$setValidity('isUserOrEmail', true);
57 } else {
58 checking = null;
59 ctrl.$setValidity('isUserOrEmail', false);
60 }
61 }
62 });
63 // Delay this check by 200 milliseconds to give
64 // the keyboard time to settle down
65 }, 200);
66 });
67 }
68 };
69 });
The method for checking whether the user is a registered user won’t work in our scheme yet, because
we haven’t attached an auth_token to the request.
To attach the auth_token in the request, we can inject our tokenHandler service in the directive and
attach it as a parameter. Simply modify the $http service to set the token as a param:
1 angular.module('shareupApp')
2 // Our is-user-or-email validation
3 .directive('isUserOrEmail', function($http, $timeout, $filter, $q) {
4 // we're checking using the api `is_user` if the user
5 // input is already a user
6 var isUser = function(input) {
7 // We're returning a deferred promise
8 var d = $q.defer();
9
Angular using Rails as an API 66
10 if (input) {
11 $http({
12 url: '/api/check/is_user',
13 method: 'POST',
14 params: {
15 auth_token: tokenHandler.get()
16 },
17 data: { 'name': input }
18 }).then(function(data) {
19 if (data.status == 200){
20 d.resolve(data.data);
21 } else {
22 d.reject(data.data);
23 }
24 });
25 //...
1 def get_current_user
2 if user_signed_in?
3 render status: 200,
4 json: {
5 success: true,
6 info: "Current user",
7 data: {
8 token: current_user.authentication_token,
9 email: current_user.email
10 }}
11 else
12 render status: 401,
13 json: {
14 success: true,
15 info: "",
16 data: {}
17 }
Angular using Rails as an API 67
18 end
19 end
We’ll also need to create a route to point our API endpoint to this controller action. Update your
app/config/routes.rb file to include the following:
1 //...
2 devise_scope :user do
3 post '/check/is_user' => 'users/users#is_user',
4 as: 'is_user'
5 post '/current_user' => 'users/sessions#get_current_user'
6 end
7 //...
Finally, we’ll also update our token_handler.js file to fetch our current user data:
1 //...
2 return token
3 }
4 }
5 getCurrentUser: function() {
6 var d = $q.defer();
7
8 if (currentUser) {
9 d.resolve(currentUser);
10 } else {
11 $http({
12 url: '/api/current_user',
13 method: 'POST'
14 }).then(function(data) {
15 d.resolve(data.data);
16 });
17 }
18 return d.promise;
19 };
20 return tokenHandler;
21 });
Using $resource
The $resource service definitely saves us a LOT of time, but since we’re using the auth_token, we
need a way to send the token along with our requests to keep us authenticated.
Angular using Rails as an API 68
To do so, we’ll extend our tokenHandler to include a method that wraps our $resource requests
with the token.
Open the client/app/scripts/services/token_handler.js file and extend the tokenHandler like
so:
1 angular.module('shareupApp')
2 .factory('tokenHandler', function($rootScope, $http, $q, $location) {
3 var token = null,
4 currentUser;
5
6 // https://gist.github.com/nblumoe/3052052
7 var tokenWrapper = function(resource, action) {
8 // copy original action
9 resource['_' + action] = resource[action];
10 // create new action wrapping the original and sending token
11 resource[action] = function( data, success, error){
12 return resource['_' + action](
13 angular.extend({}, data || {}, {access_token: tokenHandler.get()}),
14 success,
15 error
16 );
17 };
18 };
19
20 var tokenHandler = {
21 set: function(v) { token = v; },
22 get: function() {
23 if (!token)
24 $rootScope.$broadcast('event:unauthorized');
25 else
26 return token
27 },
28 wrapActions: function(resource, actions) {
29 var wrappedResource = resource;
30 for (var i=0; i < actions.length; i++) {
31 tokenWrapper( wrappedResource, actions[i] );
32 };
33 return wrappedResource;
34 }
35 };
36
37 return tokenHandler;
Angular using Rails as an API 69
38 });
Let’s use this handler functionality to build our share service. Using the $resource service, which
will talk to our share API, we’ll wrap our calls in the token.
Open app/controllers/shares_controller.rb, and add this content:
Now, when we send an API call to /api/shares, we’re creating a new share for the authenticated
user. Since this share will use the $resource service, we’ll need to include the ngResource as a
dependency of our module.
In the client/scripts/app.js, inject ngResource as a dependency for our app:
Angular using Rails as an API 70
1 angular.module('shareupApp', ['ngResource'])
2 .config(function ($routeProvider) {
3 // ...
1 angular.module('shareupApp')
2 .factory('ShareService', function($resource, $q, tokenHandler) {
3 var Share = $resource('/api/shares/:id',
4 {id: '@id'},
5 {}
6 );
7
8 tokenHandler.wrapActions(Share, ["save"]);
9 return Share;
10 });
Once this service is set, we need to make sure we add it to our client/app/index.html, like so:
Let’s create a service that fetches articles. We do not need to go into this service in depth, as it’s pretty
standard across both methods of Rails integration. Create a client/app/scripts/services/article_-
service.js file and add the following content to it:
1 angular.module('shareupApp')
2 .factory('ArticleService', function($http, $q) {
3 var service = {
4 getLatestFeed: function() {
5 var d = $q.defer();
6 $http.jsonp('http://ajax.googleapis.com/ajax/services/'+
7 'feed/load?v=1.0&num=50&callback=JSON_CALLBACK&q='+
8 encodeURIComponent(
9 'http://feeds.huffingtonpost.com/huffingtonpost/raw_feed'
10 )
11 ).then(function(data, status) {
Angular using Rails as an API 71
Don’t forget to attach this script file to client/index.html, as we’ve done with the rest of our
JavaScript files:
Now, in our MainCtrl, we can implement the action share(), which will create a new share and
send it off to our Rails server; however, it’s better create a directive for this action.
Create the file client/app/directives/share.js, and add the share directive. This directive will
be responsible for creating a new share and showing confirmation to the user that the share was
sent.
1 angular.module('shareupApp')
2 // Our share directive
3 .directive('share', function($http, $timeout, ShareService, tokenHandler) {
4 return {
5 restrict: 'A',
6 require: 'ngModel',
7 templateUrl: 'views/share.html',
8 scope: {
9 ngModel: '=',
10 onShare: '&'
11 },
12 link: function(scope, attrs, ele) {
Angular using Rails as an API 72
1 <div data-ng-hide="showShareBox">
2 <a class="btn btn-success btn-small"
3 ng-click="showShareBox=!showShareBox">
4 Share with a friend
5 </a>
6 </div>
7 <div data-ng-show="showShareBox">
8 <form name="shareForm">
9 <input type="text"
10 placeholder="email or username"
11 ng-model="newShare.recipient"
12 is-user-or-email />
Angular using Rails as an API 73
We’ll use this directive to place our share view element on the page. For instance, instead of placing
the share HTML in the view, we can now simply call the directive to do it for us.
Modify client/app/views/main.html such that it looks like:
1 <div class="row">
2 <div ng-repeat="article in articles | orderBy:publishedDate:false">
3 <div class="article-listing-element row">
4 <div class="span9">
5 <h2><a href="{{ article.link }}" title="{{ article.title }}">
6 {{ article.title }}
7 </a></h2>
8 <p>
9 {{ article.publishedDate |date:'M/d/yy h:mm:ss a' }}
10 </p>
11 </div>
12 <div class="span2">
13 <a share ng-model="article">
14 Share</a>
15 </div>
16 </div>
17 </div>
18 </div>
With the inclusion of this HTML, we’re placing the share directive next to the listing item for every
single article. The share service will take the save method and wrap the auth_token on the back
end so the Rails server will be happy.
The MainCtrl should now look like:
1 angular.module('shareupApp')
2 .controller('MainCtrl', function ($scope, $http, ArticleService) {
3 $scope.currentUser = {};
4 ArticleService.getLatestFeed()
5 .then(function(data) {
6 $scope.articles = data;
7 });
8 });
Now, refresh the page (make sure your Rails and Grunt servers are running), and you should see our
fully functioning app.
Angular using Rails as an API 75
Full app
1 // ...
2 module.exports = function (grunt) {
3 require('load-grunt-tasks')(grunt);
4 require('time-grunt')(grunt);
5
6 // configurable paths
7 var yeomanConfig = {
8 app: 'app',
9 dist: '../public' // Instead of dist: 'dist'
10 };
11
12 // ...
Now, when you’re in the client/ folder and you’re ready to deploy the app, you can type grunt
build, and Yeoman will generate the app in the public/ directory for distribution alongside the
Rails app.
1 $ grunt build
It’s a good idea to include the grunt build command in your deployment tool so that when
you deploy a new version of the site, the client side is regenerated as well.
Conclusion
As we’ve shown you, it’s incredibly easy to marry two fantastic frameworks together with to enable
us to rapidly develop modern web applications.
As we mentioned, the purpose for this book is not to describe how to use AngularJS in-depth, but
how to use it with the Rails application framework. If you are interested in a comprehensive guide
on AngularJS, check out our book at ng-book.com²³.
Thanks and we hope you learned a lot from working with this book. If you have any questions, feel
free to email us at [email protected]²⁴.
²³http://ng-book.com
²⁴mailto:[email protected]