0% found this document useful (0 votes)
9 views

Riding Rails with AngularJS

The document is an introduction to 'Riding Rails with AngularJS' by Ari Lerner, aimed at helping readers quickly learn to integrate AngularJS with Ruby on Rails. It outlines the book's organization, which includes setting up a Rails app, integrating AngularJS, and using Rails as a back-end API. The book is intended for those with basic knowledge of both technologies and provides resources for further learning.

Uploaded by

splinter2007
Copyright
© © All Rights Reserved
We take content rights seriously. If you suspect this is your content, claim it here.
Available Formats
Download as PDF, TXT or read online on Scribd
0% found this document useful (0 votes)
9 views

Riding Rails with AngularJS

The document is an introduction to 'Riding Rails with AngularJS' by Ari Lerner, aimed at helping readers quickly learn to integrate AngularJS with Ruby on Rails. It outlines the book's organization, which includes setting up a Rails app, integrating AngularJS, and using Rails as a back-end API. The book is intended for those with basic knowledge of both technologies and provides resources for further learning.

Uploaded by

splinter2007
Copyright
© © All Rights Reserved
We take content rights seriously. If you suspect this is your content, claim it here.
Available Formats
Download as PDF, TXT or read online on Scribd
You are on page 1/ 81

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

Setting up our Rails app . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 5


Setting up our user model and authentication . . . . . . . . . . . . . . . . . . . . . . . . . 6
Setting up our sharing model . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 15
Setting up a test harness for our Rails app . . . . . . . . . . . . . . . . . . . . . . . . . . . 17
Testing WelcomeController . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 18
Building the session API . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 20

Angular in the Rails asset pipeline . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 24


Preparing to integrate the app . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 27
Fetching articles . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 28
Building the ShareService . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 31
Interacting with the session API . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 32
Building the share API . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 33
Client-side validation . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 37
Creating a directive . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 37

Angular using Rails as an API . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 43


Using the almighty Yeoman . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 43
Configuring Yeoman proxy . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 45
Developing the app . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 48
Developing Shareup . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 49
Securing the app (as much as possible) . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 49
CORS-based auth in the Rails app . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 49
Authentication . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 51
Implementing front-end signup . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 54
Implementing back-end login . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 57
CONTENTS

Implementing front-end login . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 58


Making an authenticated API call . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 62
Getting current user data . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 66
Using $resource . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 67
Deploying the app . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 75
Conclusion . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 76
Introduction
About the author
Ari Lerner is a Founder of FullStack, a full-stack engineering and development shop that works with
all parts of the stack, from hardware up through the browser. He has also authored ng-book: The
Complete Book on AngularJS and co-authored The Rails 4 Way.
He enjoys all parts of the development ecosystem, from infrastructure deployment through front-end
application development and optimization. He co-manages defnewsletter.com and ng-newsletter.com
and lives in the sunny part of San Francisco.

About this book


The AngularJS Rails Handbook is written with the explicit goal of getting you up and running with
with AngularJS¹ on Rails as fast as possible.
AngularJS is an advanced front-end framework released by the team at Google². It enables you to
build a rich front-end experience with cutting-edge tools, quickly and easily.
The AngularJS Rails Handbook gets you on your feet with AngularJS so that you can quickly begin
creating impressive web experiences on Ruby on Rails. It addresses challenges and provides real-
world techniques that you can use immediately in your web applications.
We’ll give you the tools and tests you need to make your own dynamic web applications with
AngularJS and be confident that you are building a scalable applications, from this point forward.

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

Organization of this book


In an effort to get you comfortable with building in AngularJS and Rails, we’ll first set up a Rails
application with authentication and authorization.
Then we’ll walk through the two approaches we will take in building a Rails app fronted by
AngularJS, spending a chapter on each: After we build our Rails app in a traditional way, we’ll
dive into separating our Rails app as a back-end RESTful API and focus on building the AngularJS
front end independently.
You will therefore want to build your first practice app using the chapters “Setting up our Rails app”
and “Angular in the Rails asset pipeline.” Then you will use “Setting up our Rails app” and “Angular
using Rails as an API” to build the second practice app.

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.

Conventions used in this book


Throughout this book, you will see the following typographical conventions that indicate different
types of information:
In-line code references will look like: <h1>Hello</h1>.
A block of code appears like so:

1 var App = angular.module('App', []);


2
3 function FirstController($scope) {
4 $scope.data = "Hello";
5 }

At the command line, a $ character always precedes the command:

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):

1 > var obj = {message: "hello"};

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()

Important words will be shown in bold.


Finally, tips and tricks will be shown as:

Tip: This is a tip


¹¹http://bloc.io
¹²https://onemonthrails.com/
Introduction 4

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:

1 $ npm install -g karma

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:

1 $ gem install rails

¹³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:

• Creating a new Rails app


• Setting up our user model and authentication
• Setting up our sharing model
• Setting up a test harness for our Rails app
• Integrating Angular into the app and setting up sprockets
• Wiring up the interface
• Creating our API on the back end
• Setting up login with AngularJS

Creating a new Rails app


First, use the Rails generator command to create a new Rails app, our news feed reader app called
shareup:

1 $ rails new shareup && cd shareup

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

Setting up our user model and authentication


In this Rails application, we’re going to use the default sqlite3 database, so no need to set up an
external database. We’ll build our app such that we can later switch to a production-ready relational
database, like PostgreSQL.

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¹⁶.

Adding Devise and Omniauth


Devise is the canonical flexible authentication system for Rails. It’s based on the Warden gem and it
provides many helpers through the open community. It allows us to provide a lot of features that are
desirable in an authentication system, such as encrypting user passwords in the database, handling
password resetting, sign-in tracking, and more.

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¹⁸.

Let’s open our Gemfile and add the following lines:

¹⁵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.

1 $ rails generate devise:install

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:

1 $ rails generate devise user

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:

1 $ rails generate model authorization \


2 provider:string \
3 uid:string \
4 user_id:integer \
5 token:string \
6 secret:string \
7 name:string \
8 url:string

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

1 class DeviseCreateUsers < ActiveRecord::Migration


2 def change
3 create_table(:users) do |t|
4 ## Database authenticatable
5 t.string :name
6 t.string :email, :null => false, :default => ""
7 t.string :encrypted_password, :null => false, :default => ""
8
9 ## Recoverable
10 t.string :reset_password_token
11 t.datetime :reset_password_sent_at
12
13 ## Rememberable
14 t.datetime :remember_created_at
15
16 ## Trackable
17 t.integer :sign_in_count, :default => 0
18 t.datetime :current_sign_in_at
19 t.datetime :last_sign_in_at
20 t.string :current_sign_in_ip
21 t.string :last_sign_in_ip
22
23 t.timestamps
24 end
25
26 add_index :users, :email, :unique => true
27 add_index :users, :reset_password_token, :unique => true
28 end
29 end

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.

Once we’ve made these changes, we can migrate our database:

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):

1 class Authorization < ActiveRecord::Base


2 belongs_to :user
3 end

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.

1 config.omniauth :twitter, "CONSUMERKEY", "CONSUMERSECRET"

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:

1 config.omniauth :twitter, AppConfig.twitter['clientId'],


2 AppConfig.twitter['clientSecret']

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 new file, add the following lines:

1 class Users::OmniauthCallbacksController < Devise::OmniauthCallbacksController


2 def twitter
3 end
4 end

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:

1 class User < ActiveRecord::Base


2 devise :database_authenticatable, :registerable,
3 :recoverable, :rememberable, :trackable,
4 :validatable, :omniauthable
5 has_many :authorizations, :dependent => :destroy
6
7 end

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

1 class Users::OmniauthCallbacksController < Devise::OmniauthCallbacksController


2 def twitter
3 auth_hash = request.env["omniauth.auth"]
4 uid = auth_hash['uid']
5 name = auth_hash['info']['name']
6 auth = Authorization.find_by_provider_and_uid("twitter", uid)
7 if auth
8 ## We already know about this user who is signing in with
9 # the provider: just return the user associated with the Authorization
10 user = auth.user
11 else
12 ## Is a user signed in already? If not, then find or create
13 # the user
14 unless current_user
15 unless user = User.find_by_name(name)
16 user = User.create({
17 name: name,
18 password: Devise.friendly_token[0,8],
19 email: "#{UUIDTools::UUID.random_create}@shareup.com"
20 })
21 end
22 else
23 user = current_user
24 end
25
26 ## Finally, create an authorization for the current user
27 unless auth = user.authorizations.find_by_provider("twitter")
28 auth = user.authorizations.build(provider: "twitter", uid: uid)
29 user.authorizations << auth
30 end
31 auth.update_attributes({
32 uid: uid,
33 token: auth_hash['credentials']['token'],
34 secret: auth_hash['credentials']['secret'],
35 name: name,
36 url: "http://twitter.com/#{name}"
37 })
38 end
39 if user
40 sign_in_and_redirect user, :event => :authentication
41 else
42 redirect_to :new_user_registration
Setting up our Rails app 15

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.

Setting up our sharing model


Now that we have our user and authorization/authentication set up, we’ll start building our news
sharing models.
In our app, we’re going to find the latest news on the front end. A user sends a particularly interesting
article to a friend of his through our back end. The back end will then be responsible for creating
and storing only interesting articles that our users share with each other.
The logic of our app is as follows:

• Only logged-in and registered users can share articles


• The user can share articles either with registered users or via email

Our sharing model will need to know about the following information:

• from_user - the user who is sending the article to a friend’s user id


• to_user - the registered user with whom we’re sharing
• to_email - the email of the user we’re going to send to, if and only if the to_user is nil
• created_at - the time at which we shared the file
• url - the URL of the link that our user is sharing

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

1 $ rails generate resource share from_user_id:integer \


2 to_user_id:integer \
3 to_email:string \
4 created_at:datetime \
5 url:string

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 # ...

Once again, we now want to migrate our database:

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:

1 class User < ActiveRecord::Base


2 # ...
3 has_many :shares, foreign_key: 'from_user_id'
4 end

We’ll now add the inverse relationship on the Share model. Open the model at app/models/share.rb
and add the belongs_to:

1 class Share < ActiveRecord::Base


2 belongs_to :from, foreign_key: 'from_user_id', class_name: 'User'
3 belongs_to :to, foreign_key: 'to_user_id', class_name: 'User'
4 end

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

1 $ rails generate controller welcome

In our app/controllers/welcome_controller.rb, add the following lines:

1 class WelcomeController < ApplicationController


2 def index
3 end
4
5 def dashboard
6 end
7 end

We’ll need to add a route in our config/routes.rb file to support the dashboard route.

1 get '/dashboard' => 'welcome#dashboard'


2 root to: 'welcome#index'

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.

Setting up a test harness for our Rails app


When building a large-scale app, it’s always a great idea to set up testing.

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

20 test "should redirect from dashboard view to index without a user" do


21 get :dashboard
22 assert_response :redirect
23 end
24
25 end

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:

1 class WelcomeController < ApplicationController


2 before_filter :authenticate_user!, except: [:index]
3 layout :choose_layout
4
5 def index
6 end
7
8 def dashboard
9 end
Setting up our Rails app 20

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:

1 class ApplicationController < ActionController::Base


2 # Prevent CSRF attacks by raising an exception.
3 # For APIs, you may want to use :null_session instead.
4 protect_from_forgery with: :exception
5
6 after_filter :set_csrf_cookie_for_ng
7
8 private
9
10 def set_csrf_cookie_for_ng
11 cookies['XSRF-TOKEN'] = form_authenticity_token if protect_against_forgery?
12 end
13
14 def verified_request?
15 super || form_authenticity_token ==
16 request.headers['HTTP_X_XSRF_TOKEN']
17 end
18 end

Building the session API


First, let’s fill out our SessionService. This service returns information about the current user,
which will enable our API to talk back to our Rails app and query information about our currently
logged-in user.
We’ll need to write a new sessions controller that will simply return the currently logged-in user.
We’ll map this process as a route in our router and use our SessionService to interact with it.
Setting up our Rails app 21

Create the controller at app/controllers/users/sessions_controller.rb and add the controller


and the show_current_user method.
In this method, we need to check to ensure the user is currently logged in and authenticated for
the request. If the user is not logged in, then we’ll want to kick them out and throw an appropriate
response.
Inside this method, we’ll use the raw warden helper to authenticate users. Let’s update our controller
with the following content:

1 class Users::SessionsController < Devise::SessionsController


2 respond_to :json
3 def show_current_user
4 reject_if_not_authorized_request!
5 render status: 200,
6 json: {
7 success: true,
8 info: "Current user",
9 user: current_user
10 }
11 end
12
13 def failure
14 render status: 401,
15 json: {
16 success: false,
17 info: "Unauthorized"
18 }
19 end
20
21 private
22
23 def reject_if_not_authorized_request!
24 warden.authenticate!(
25 scope: resource_name,
26 recall: "#{controller_path}#failure")
27 end
28 end

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

Start up your Rails server to run your app in the browser:

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.

Creating the user_check API


To make it possible for us to check a user, we’ll need to add a new controller. Create the file
app/controllers/users/users_controller.rb with the following content:

1 class Users::UsersController < Devise::SessionsController


2 respond_to :json
3
4 def is_user
5 reject_if_not_authorized_request!
6 render status: 200,
7 json: {
8 success: !User.find_by_name(params[:name]).blank?
9 }
Setting up our Rails app 23

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.

This integration of Rails and AngularJS is the easier of the two.

The advantages to this approach include:

• Zero configuration in Rails


• Rendering of the Angular app only to Rails-based authenticated users
• Rendering of custom data in the JavaScript files
• Choice of using either CoffeeScript or JavaScript without changing workflow
• Simultaneous Rails and Angular app development

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

1 gem 'angularjs-rails', '>= 1.2.0.rc1'


2 gem 'ngmin-rails'

Feel free to include other style gems in your Gemfile as well, such as Twitter’s Bootstrap or the Zurb
Foundation gem:

1 # For Twitter's Bootstrap


2 gem 'bootstrap-sass-rails'
3 # or for Zurb Foundation
4 gem 'zurb-foundation'

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.

And now run the install command:

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:

1 //= require jquery


2 //= require jquery_ujs
3 //= require angular
4 //= require angular-route
5 //= require angular-resource
6 //= require turbolinks
7 //= require_tree .

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

Preparing to integrate the app


Now that we have a basic stub of our Angular app that will bootstrap with the rest of the page, let’s
start creating our functional Angular app.
First, let’s extend our app/assets/javascripts/app/app.js to load a home template and boot into
a default HomeController:

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 });

To support our routing, we need to create a dashboard.html template, as well as a HomeController.


We can skip our Rails controllers entirely by using the public/ directory to serve out our HTML
templates.
Create the public/templates directory, and place a file named dashboard.html inside containing
the following content:

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:

1 angular.module('myApp', ['ngRoute', 'myApp.controllers'])


2 // ...

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:

1 <%= link_to "Sign In", new_user_session_path %>

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

1 angular.module('myApp', ['ngRoute', 'myApp.controllers', 'myApp.services'])


2 // ...

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

Building the ShareService


The second service we need is the one that talks with our Rails back end. This is where the Rails
integration makes things fun.
We’ll want to put a few stubs in place before we dive into having the two talk to each other. We’re
going to create an empty 'Share resource.
First, we’ll need to inject ngResource as a dependency for the app, as we did before, in app/assets/javascripts/app/

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 });

Interacting with the session API


In the first chapter, we added a session controller that provides the route /api/current_user. We’ll
use this controller to build our SessionService, which will contain three properties:

• getCurrentUser - fetches the current user and caches the currentUser


• isAuthenticated - asks whether we have fetched an authenticated user
• currentUser - calls the cached current_user

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:

1 angular.module('myApp', ['ngRoute', 'myApp.controllers', 'myApp.services'])


2 .config(function($routeProvider) {
3 $routeProvider.when('/', {
4 templateUrl: '/templates/dashboard.html',
5 controller: 'HomeController',
6 resolve: {
7 session: function(SessionService) {
8 return SessionService.getCurrentUser();
9 }
10 }
11 })
12 .otherwise({
13 redirectTo: '/'
14 });
15 });

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:

1 <h2>Welcome back {{ user.name }}</h2>

Building the share API


Now that we are sending our Angular app only to authenticated users, let’s build our sharing API.
In our shareup app, we’re going to share articles that we fetch on the front end (using AngularJS)
and send to our Rails API as an interesting share.
Angular in the Rails asset pipeline 34

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:

1 <div class="small-1 large-1 columns">


2 <div data-ng-hide="showShareBox">
3 <a ng-click="showShareBox=!showShareBox">Share with a friend</a>
4 </div>
5 <div data-ng-show="showShareBox">
6 <form name="shareForm">
7 <label>Share with a friend</label>
8 <input type="text"
9 placeholder="email or username"
10 data-ng-model="newShare.recipient" >
11 <input type="button"
12 class="btn"
13 value="Cancel"
14 data-ng-click="showShareBox = !showShareBox">
15 <input type="submit" class="btn"
16 ng-click="showShareBox=!showShareBox;share()"
17 ng-disabled="shareForm.$invalid" value="Share">
18 </form>
19 </div> <!-- showShareBox -->
20 </div> <!-- commentbox -->

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.

To the Rails side


In order to support our Angular app, we’ll need to create our API. Rails is particularly good at giving
us the ability to create a declarative, descriptive RESTful API.
We’ll add a create method to our SharesController so that when we execute a share() function
on the client side, we’ll create a share in the database.
Edit the app/controllers/shares_controller.rb to respond to json requests and create the Share:

1 class SharesController < ApplicationController


2 before_filter :authenticate_user!, only: [:new, :create]
3 respond_to :json
4
5 def index
6 render json: []
7 end
8
9 def create
10 link = params[:url]
11
12 share_params = {from_user_id: current_user.id}
13 if to_user = User.find_by_name_or_email(params[:user])
14 share_params[:to_user_id] = to_user.id
15 else
16 share_params[:to_email] = params[:user]
17 end
18
19 share = Share.create(share_params)
20 render status: 200,
21 json: {
22 success: share.persisted?,
23 share_id: share.id
24 }
25 end
26 end
Angular in the Rails asset pipeline 37

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:

1 class Share < ActiveRecord::Base


2 belongs_to :from_user
3 belongs_to :to_user
4
5 validates_format_of :to_email,
6 :with => /\A([^@\s]+)@((?:[-a-z0-9]+\.)+[a-z]{2,})\z/i
7 end

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.

Interacting with the user_check API


Our live check API is available at /api/check/is_user so that our Angular app can call to check
whether a username belongs to a known user. We’ll use this check in our isUser directive, like so:

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.

1 <div class="small-1 large-1 columns">


2 <div data-ng-hide="showShareBox">
3 <a ng-click="showShareBox=!showShareBox">Share with a friend</a>
4 </div>
5 <div data-ng-show="showShareBox">
6 <form name="shareForm">
7 <label>Share with a friend</label>
8 <input type="text"
9 placeholder="email or username"
10 data-ng-model="newShare.recipient"
11 is-user-or-email="">
12 <input type="button" class="btn"
13 value="Cancel"
14 data-ng-click="showShareBox = !showShareBox">
15 <input type="submit" class="btn"
16 ng-click="showShareBox=!showShareBox;share()"
17 ng-disabled="shareForm.$invalid"
18 value="Share">
19 </form>
20 </div> <!-- showShareBox -->
21 </div> <!-- commentbox -->

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:

• Zero configuration in Rails


• Distinct separation of concerns of the back-end API from the front end
• Ability to use other development environments better suited for front-end development
• No need to worry about Rails layouts
• Maintenance of the Rails project clear of front-end assets
• Use of AngularJS community-driven generators
• Promotion of testing of the JS code-base

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}

Note about Rails API


In this book, we’re focusing on building an AngularJS app on vanilla Rails 4. There is a gem called
Rails::API, which is specifically designed to allow you to build an API server with Rails, stripping
away a lot of the functionality that Rails gives us for free.
Everything in this chapter can be applied to setting up Rails with the Rails::API gem.

Using the almighty Yeoman


If you’re not familiar, Yeoman²¹ is a powerful web development collection of tools promoted by
Google. It glues together three very powerful tools that handle a lot of the grunt work involved in
building front-end apps.
²¹http://yeoman.io/
Angular using Rails as an API 44

• Yeoman (yo) generates our scaffolding


• Grunt is our build tool
• Bower is our package management tool

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:

1 $ npm install -g generator-angular

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.

You can update your PATH by running this command: export


PATH=$PATH:/usr/local/share/npm/bin.

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

Generating the client app

Configuring Yeoman proxy


Now, to work with Yeoman in our Rails app as though we are developing the app alongside Rails
(and how it will work in production), we’ll need to add a proxy in our client-side app to proxy our
API requests to the Rails back end.
This process will require us to install a Grunt plugin, grunt-connect-proxy, and configure it to
proxy back to our Rails backend. Use the npm tool to install the dependency, like so:

1 $ npm install --save-dev grunt-connect-proxy

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

Developing the app


Now that we have our app ready to go, we’ll need to launch both our Rails app and the Grunt app.
Open two shell sessions and run both of the following commands:
Running the Rails server

1 $ rails server

Running the Grunt server

1 $ grunt server

If you haven’t already installed Grunt’s CLI, you’ll want to do that now:

1 $ npm install -g grunt -cli

Running the servers (with tmux)

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.

Securing the app (as much as possible)


Our app requires that users log in to interact with the site, so we’ll need to implement client-side
login back to Rails.

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.

CORS-based auth in the Rails app


All we need to do to implement CORS is set a few headers to allow resources. Modify our
config/app_config.yml to look like:
Angular using Rails as an API 50

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

In our app/controllers/application_controller.rb we’ll need to modify a few of the flows that


we set up previously. First, we’ll skip our CSRF token when options are fetched. Then we’ll add our
access control headers and authenticate our user. Our updated code for this controller should now
look like so:

1 class ApplicationController < ActionController::Base


2 protect_from_forgery with: :null_session
3
4 before_filter :set_cors_headers
5 before_filter :cors_preflight
6
7 def set_cors_headers
8 headers['Access-Control-Allow-Origin'] = AppConfig.client['origin']
9 headers['Access-Control-Allow-Methods'] = 'GET,POST,PUT,DELETE,OPTIONS'
10 headers['Access-Control-Allow-Headers'] = '*'
11 headers['Access-Control-Max-Age'] = "3628800"
12 end
13
14 def cors_preflight
15 head(:ok) if request.method == :options
16 end
17 end

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

User authentication check


We’re including a check inside the ApplicationController that checks if the user is logged in or
not. Unfortunately, we have to include this check as a component, otherwise we’ll never get a 401
response code back that checks whether our user is logged in.

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:

1 $ rails generate migration AddAuthTokenToUsers authentication_token:string

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:

1 class AddAuthTokenToUsers < ActiveRecord::Migration


2 def change
3 add_column :users, :authentication_token, :string
4 add_index :users, :authentication_token, :unique => true
5 end
6 end

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:

1 class User < ActiveRecord::Base


2 devise :database_authenticatable, :registerable,
3 :recoverable, :rememberable, :trackable,
4 :validatable, :omniauthable, :token_authenticatable
5
6 before_save :ensure_authentication_token
7 end

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.

Implementing back-end signup


Create the registrations controller in app/controllers/users/registrations_controller.rb. This
controller will extend from Devise::RegistrationsController as a base controller. Since we’re
going to make this accessible through an API, we’ll want to only enable json responses.
When we create a new user, we’ll want to build the user (or resource, in the eyes of Devise) and
attempt to save it. If there are no errors upon saving, we’ll return a success response. If there are,
we’ll return the errors so we can display them on the front end.
The entire app/controllers/users/registrations_controller.rb looks like:
Angular using Rails as an API 53

1 class Users::RegistrationsController < Devise::RegistrationsController


2
3 respond_to :json
4 def create
5 # Create the user
6 build_resource(sign_up_params)
7 # Try to save them
8 if resource.save
9 sign_in resource
10 render status: 200,
11 json: {
12 success: true,
13 info: "Registered",
14 data: {
15 user: resource,
16 auth_token: current_user.authentication_token
17 }
18 }
19 else
20 # Otherwise fail
21 render status: :unprocessable_entity,
22 json: {
23 success: false,
24 info: resource.errors,
25 data: {}
26 }
27 end
28 end
29 end

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

Implementing front-end signup


In this app, we’re going to have a login screen and the app screen. Currently, we only have one of
these screens built out; we’ll break out into two. To do that, we’ll use AngularJS routing.
In client/app/scripts/app.js, we’ll need to add a second route:

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 // ...

Then create a new LoginCtrl in client/scripts/controllers/login.js add a new controller:

1 angular.module('shareupApp')
2 .controller('LoginCtrl', function() {});

and add a client/app/views/login.html file that contains the following:

1 <form ng-submit="login()" class="well form-inline">


2 <h3>Login</h3>
3 <input ng-model="user.email"
4 type="text"
5 val="Your email"
6 placeholder="Email">
7 <input ng-model="user.password" type="password"
8 val="password"
9 placeholder="Password">
10 <button type="submit" class="btn">Log in</button>
Angular using Rails as an API 55

11 <button ng-click="signup()" class="btn">Sign up</button>


12 </form>
13 <div ng-show="user.errors">
14 <h2>{{ user.errors }}</h2>
15 </div>

Add this file to be required in the client/app/index.html:

1 <!-- ... -->


2 <!-- build:js({.tmp,app}) scripts/scripts.js -->
3 <script src="scripts/app.js"></script>
4 <script src="scripts/controllers/main.js"></script>
5 <script src="scripts/controllers/login.js"></script>
6 <!-- ... -->

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:

1 <!-- ... -->


2 <script src="scripts/controllers/login.js"></script>
3 <script src="scripts/services/token_handler.js"></script>
4 <!-- ... -->

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

Implementing back-end login


To implement login, we’ll need to create a new Rails controller. Create this controller and add the
content:

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.

Implementing front-end login


Adding login() is easy, as the view has already been laid out for us. Modify the client/app/scripts/controllers/l
file. In order to add the login() action, we’ll need to define its behavior so we know what
dependencies we’ll need to inject into our controller.
The login function will need to:

• Make a POST to our Rails API with user data


• Store the auth_token that is returned and redirect the user to the ‘/’ route if the HTTP response
indicates that the request was successful and the user credentials are correct
• Store the errors that the Rails API returns (if the HTTP response indicates that the request
was successful, but the login was unsuccessful) so we can show them to the user
• Show the user an error if the HTTP response was unsuccessful (this way, we’ll know something
was wrong with our Rails API request)

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 .run(function($rootScope, $http, $location, tokenHandler ) {


2 $rootScope.$on('event:unauthorized', function(evt) {
3 $location.path('/login');
4 });
5 });

Run (or restart) your Rails app:


Angular using Rails as an API 61

1 $ rails server

and boot up the Grunt 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

Making an authenticated API call


Now that we have an auth_token, we can make authenticated API calls. We’ll do so with both the
$http and the $resource services (built into Angular).

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

1 class Users::UsersController < Devise::SessionsController


2 protect_from_forgery with: :exception, except: [:is_user]
3 respond_to :json
4
5 def is_user
6 authenticate_user!
7 render status: 200,
8 json: {
9 success: !User.find_by_name(params[:name]).blank?
10 }
11 end
12 end

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 //...

Getting current user data


We’ll use our tokenHandler to fetch the user that is currently logged in to the Rails site, as identified
by the auth_token. In order to do that, we’ll have to create an API endpoint that returns user details.
We’ll define this endpoint in our SessionsController in Rails, ‘app/controllers/users/sessions_con-
troller.rb’:

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:

1 class SharesController < ApplicationController


2 skip_before_filter :verify_authenticity_token
3 before_filter :authenticate_user!
4
5 def index
6 render status: 200,
7 json: {
8 success: true,
9 shares: current_user.shares
10 }
11 end
12
13 def create
14 share = Share.new(params[:share])
15 if share.save
16 render status: 200,
17 json: {
18 success: true,
19 share: share.id
20 }
21 else
22 render status: 200,
23 json: {
24 success: false,
25 error: share.errors
26 }
27 end
28 end
29 end

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 // ...

Create the Share service in the file client/app/scripts/services/share_service.js. This service


will return a $resource object, so add the following content:

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:

1 <!-- ... -->


2 <script src="scripts/controllers/login.js"></script>
3 <script src="scripts/services/token_handler.js"></script>
4 <script src="scripts/services/share_service.js"></script>
5 <!-- ... -->

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

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 return service;
23 });

Don’t forget to attach this script file to client/index.html, as we’ve done with the rest of our
JavaScript files:

1 <!-- ... -->


2 <script src="scripts/controllers/login.js"></script>
3 <script src="scripts/services/token_handler.js"></script>
4 <script src="scripts/services/article_service.js"></script>
5 <script src="scripts/services/share_service.js"></script>
6 <!-- ... -->

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

13 scope.newShare = {recipient: ""};


14 scope.showPending = false;
15 scope.showConfirmation = false;
16 // Our share function will take an article
17 // and use our share service, which is a $resource
18 // and call save on the current User
19 scope.share = function() {
20 scope.newShare = {recipient: ""};
21 var share = new ShareService({
22 url: scope.ngModel.url,
23 from_user: tokenHandler.getCurrentUser().id,
24 to_user: scope.newShare.recipient
25 });
26 // Show pending...
27 scope.showPending = true;
28 share.$save(function(s) {
29 // Show confirmation for 2 seconds
30 scope.showPending = !(scope.showConfirmation = true);
31 $timeout(scope.hideConfirmation, 2000);
32 });
33 }
34 scope.hideConfirmation = function() {
35 scope.showConfirmation = false;
36 }
37 }
38 }
39 });

Create the client/app/views/share.html file, which looks like:

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

13 <input type="button" class="btn"


14 value="Cancel"
15 data-ng-click="showShareBox=!showShareBox" />
16 <input type="submit"
17 ng-click="showShareBox=!showShareBox;share()"
18 class="btn"
19 ng-disabled="shareForm.$invalid"
20 value="Share" />
21 </form>
22 </div>
23 <div ng-show="showPending">Pending</div>
24 <div ng-show="showConfirmation">Shared!</div>

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>

Once again, we need to include this new directive in our index.html:


Angular using Rails as an API 74

1 <!-- ... -->


2 <script src="scripts/controllers/login.js"></script>
3 <script src="scripts/services/token_handler.js"></script>
4 <script src="scripts/services/article_service.js"></script>
5 <script src="scripts/services/share_service.js"></script>
6 <script src="scripts/directives/share.js"></script>
7 <!-- ... -->

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

Deploying the app


When we are ready to distribute the app, we won’t have the convenience of having a Grunt server
to accompany us (and we don’t want it).
Instead, we’ll use Yeoman to generate a static version of our app and place it in the public/ directory
of our Rails app. By default, if Rails finds an index.html file in the public directory, then it will serve
that file instead of hitting the controller route.
To get Yeoman to generate our static site in the public directory, we’ll need to modify the Yeoman
config again.
Update the file client/Gruntfile.js and change the yeomanConfig ‘dist’ key to match the public/
directory.
Angular using Rails as an API 76

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]

You might also like