- Be able to explain what a Single Page App is
- Be able to write a full CRUD application using Angular and Rails
Initialize the app
rails new spa-app --database=postgresql --skip bundle
Add the following gems
gem 'angularjs-rails'
gem 'angular-rails-templates', '~> 0.2.0'
gem 'bootstrap-sass', '~> 3.3.4'
Remove the following gems (don’t forget to run bundle install)
gem 'turbolinks'
Remove references to turbolinks in application.html.erb. Add bootstrap class to body element
<%= stylesheet_link_tag 'application', media: 'all' %>
<%= javascript_include_tag 'application'%>
...
<body class="container-fluid">
Rename application.css to application.css.css and import bootstrap
@import "bootstrap-sprockets";
@import "bootstrap";
Update application.js (remove reference to turbolinks
//= require jquery
//= require jquery_ujs
//= require angular
//= require angular-route
//= require angular-rails-templates
//= require_tree .
***Create a controller named home with one controller named index. ***
rails g controller home index
Change your routes file to root to home#index
root 'home#index
Update app/views/home/index.html.erb
<div ng-app="dogApp">
<div class="view-container">
<div ng-view></div>
</div>
</div>
-
Inside of app/assets/javascripts create a folder named
templates
,angular-controllers
andangular-config
-
Inside of app/assets/javascripts/angular-config create two files
app.js
androuting.js
-
Inside of app/assets/javascripts/angular-controllers create
dogsController.js
-
Inside of app/assets/javascripts/templates create
index.html
app.js
angular.module('dogApp',['templates','ngRoute']);
routing.js
angular.module('dogApp')
.config(['$routeProvider', config]);
function config($routeProvider){
$routeProvider
.when('/',{
templateUrl: "index.html",
controller: "DogsController",
controllerAs: "dogsCtrl"
})
.otherwise({
redirectTo: '/'
});
}
index.html
<section>
{{dogsCtrl.dogs}}
</section>
dogsController.js
angular.module('dogApp')
.controller('DogsController', DogsController);
DogsController.$inject = ['$http','$routeParams', '$window'];
function DogsController($http, $routeParams, $window){
var self = this;
self.dogs = "It's working!!!";
}
Create the Dog model
rails g model Dog name:string breed:string age:integer
Seed the database
Dog.create(name:'Fido', breed:'Labrador', age:3)
Dog.create(name:'Max', breed:'Pit Bull', age:4)
Dog.create(name:'Benji', breed:'Bulldog', age:5)
Dog.create(name:'Dolly', breed:'Beagle', age:6)
Dog.create(name:'Princess', breed:'Greyhound', age:7)
config/initializers/inflections.rb
ActiveSupport::Inflector.inflections(:en) do |inflect|
inflect.acronym 'API'
end
app/controllers/application_controller.rb
class ApplicationController < ActionController::Base
protect_from_forgery with: :null_session
end
app/controllers/api/dogs_controller.rb
module API
class DogsController < ApplicationController
def index
render json: Dog.all
end
def show
render json: Dog.find(params[:id])
end
def update
@dog = Dog.find(params[:id])
if @dog.update(dog_params)
render json: @dog, status: 200
else
render json: {errors: @dog.errors}, status: 422
end
end
def create
@dog = Dog.new(dog_params)
if @dog.save
render json: @dog, status: 201
else
render json: {errors: @dog.errors}, status: 422
end
end
def destroy
@dog = Dog.find(params[:id])
@dog.destroy
render json: :no_content
end
private
def dog_params
params.require(:dog).permit(:name,:breed,:age)
end
end
end
routes.rb
namespace :api do
resources :dogs, only:[:index,:show,:update,:create, :destroy]
end
get '*path' => 'home#index'
dogsController.js
function DogsController($http, $routeParams, $window){
var self = this;
self.getDogIndex = function(){
return $http({
method:'GET',
url: "/api/dogs"
})
.success(function(data){
console.log('success');
self.jsonDogs = data;
})
.error(function(data){
console.log('error!');
});
};
...
index.html (inside of javascripts/templates)
<div class="jumbotron" ng-init="dogsCtrl.getDogIndex()">
<h1 class="text-center">A Dog App</h1>
</div>
<section>
<ul>
<li ng-repeat="dog in dogsCtrl.jsonDogs">{{dog.name}} - {{dog.age}}
<div>
<button class="btn">Show</button>
<button class="btn">Edit</button>
<button class="btn">Delete</button>
</div>
</li>
</ul>
</section>
- First create a
new.html
template
new.html
<section>
<form>
Name: <input type="text" ng-model="dogsCtrl.name"><br>
Breed: <input type="text" ng-model="dogsCtrl.breed"><br>
Age: <input type="text" ng-model="dogsCtrl.age"><br>
<button ng-click="dogsCtrl.createDog()">Create New Dog</button>
</form>
</section>
<section>
<a href="#/">Home</a>
</section>
routing.js
...
.when('/new',{
templateUrl: "new.html",
controller: "DogsController",
controllerAs: "dogsCtrl"
})
...
dogsController.js
self.createDog = function(){
var newDog = {
name: self.name,
breed: self.breed,
age: self.age
};
console.log(newDog);
$http.post("/api/dogs", newDog)
.success(function(data){
console.log('successfuly created dog');
console.log(data);
$window.location.href = ('#/dogs/' + data.id);
})
.error(function(data){
console.log(data);
console.log('something went wrong!');
self.error = data.error;
});
};
index.html
<button class="btn"><a href="#/dogs/{{dog.id}}">Show</a></button>
show.html
<div ng-init="dogsCtrl.showDog()">
<section ng-init="dogsCtrl.showDog()">
Name: {{dogsCtrl.currentDog.name}}<br>
Breed: {{dogsCtrl.currentDog.breed}}<br>
Age: {{dogsCtrl.currentDog.age}}<br>
</section>
<a href="#/">Home</a>
</div>
routing.js*
.when('/dogs/:id',{
templateUrl: "show.html",
controller: "DogsController",
controllerAs: "dogsCtrl"
})
dogController.js
...
self.params = $routeParams;
...
self.showDog = function(){
var url = "/api/dogs/" + self.params.id;
$http.get(url)
.success(function(data){
console.log('successful show request');
self.currentDog = data;
})
.error(function(data){
console.log('something went wrong!');
});
};
index.html
<button class="btn" ng-click="dogsCtrl.deleteDog(dog.id, $index)">Delete</button>
dogsController.js
self.deleteDog = function(id, index){
var url = "/api/dogs/" + id;
$http.delete(url)
.success(function(){
console.log('succesfully deleted');
self.jsonDogs.splice(index,1);
})
.error(function(data){
console.log("Something went wrong!");
});
};
routing.js
.when('/dogs/:id/edit',{
templateUrl: "edit.html",
controller: "DogsController",
controllerAs: "dogsCtrl"
})
index.html
<button class="btn"><a href="#/dogs/{{dog.id}}/edit">Edit</a></button>
edit.html
<div ng-init="dogsCtrl.showDog()">
<form>
Name: <input type="text" ng-model="dogsCtrl.currentDog.name"><br>
Breed: <input type="text" ng-model="dogsCtrl.currentDog.breed"><br>
Age: <input type="text" ng-model="dogsCtrl.currentDog.age"><br>
<button ng-click="dogsCtrl.editDog()">Edit Dog</button>
</form>
<a href="#/">Home</a>
</div>
dogsController.js
self.editDog = function(){
var url = "/api/dogs/" + self.params.id;
var editedDog = {
name: self.currentDog.name,
breed: self.currentDog.breed,
age: self.currentDog.age
};
$http.patch(url, editedDog)
.success(function(data){
console.log(data);
console.log("successfully edited");
$window.location.href = ('#/dogs/' + data.id);
})
.error(function(){
console.log("something went wrong");
});
};
-
Have our Rails application generate an access token that contains a random hexadecimal string.
-
If a user successfully authenticates with their email address and password, then we will send the client their access token.
-
Then the client must send this access token back to our Rails API with every request or they will receive a 401 unauthorized response from the server.
Before we get started, let's get on the same page about the things we will need to add:
Rails:
- a user model
- an authentication controller and a route to handle login requests
- some way of preventing access to our API unless the clients provides a valid access token
Angular:
- some way of retrieving our access token via a login form
- an authentication controller to handle the logic around signing in and signing out
- some way of storing our access token and sending it back to the server with every request
Uncomment the bcrypt gem in your Gemfile add the has_secure_token gem and responders gem
#Gemfile
gem 'bcrypt', '~> 3.1.7'
gem 'has_secure_token'
gem 'responders', '~> 2.0'
Generate the User model
rails g model User email name password_digest token
user.rb
#app/models/user.rb
class User < ActiveRecord::Base
has_secure_password
has_secure_token
end
Create a user in the console
- After creating a user the password_digest and token fields should be populated.
User.create(email:"[email protected]", name:"Glenn",password:"banana")
Create an AuthenticationController in Rails
- The authentication controller in Rails will handle authentication (duh).
#app/controllers/api/authentication_controller.rb
module API
class AuthenticationController < ApplicationController
respond_to :json
def sign_in
#finds a user by email
user = User.find_by(email: params[:email])
#if it finds a user and the user's password is correct it'll return the user
if user && user.authenticate(params[:password])
render json: user
else
render json: { message: "email or password incorrect" }, status: 422
end
end
end #end class
end #end module
Update routes.rb
- We’ll use this route to send our HTTP POST request in Angular
#app/config/routes.rb
namespace :api do
resources :dogs, only:[:index,:show,:update,:create, :destroy]
post '/authenticate' => 'authentication#sign_in'
end
Update dogs_controller.rb
- We need to write a method named
restrict_access
that will run before the actions we want to protect. - The
restrict_access
is a private method - The
before_action
executes therestrict_access
method before our protected actions
#app/controllers/api/dogs_controller.rb
module API
class DogsController < ApplicationController
before_action :restrict_access, only:[:update,:create,:destroy]
...
...
private
def restrict_access
token = User.find_by(token: params[:token])
render json: {error:"You need to be logged in to access this"}, status: 401 unless token
end
end
end
- Because we’re going to be sending an authorization token on actions we want to protect we need to whitelist the
token
property in ourdog_params
method
#app/controllers/api/dogs_controller.rb
def dog_params
params.require(:dog).permit(:name,:breed,:age, :token)
end
- Create an AuthenticationController in Angular
//app/assets/javascripts/templates/authenticationController.js
angular
.module("dogApp")
.controller("AuthenticationController", AuthenticationController);
AuthenticationController.$inject = ["$http"];
function AuthenticationController($http){
var self = this;
}
Add a login form to the index.html Angular template
<!--app/assets/javascripts/templates/index.html -->
<div>
<!-- begin login form -->
<form class="login-form" ng-submit="auth.login()">
<input class="login-input" type="text" placeholder="email" ng-model="auth.email">
<input class="login-input" type="password" placeholder="password" ng-model="auth.password">
<input type="submit" value="Log in" class="btn btn-submit">
</form>
<!-- end login form -->
</div>
Update authenticationController.js with the login function
//app/assets/javascripts/angular-controllers/authenticationController.js
self.email; //bound to form in view
self.password; //bound to form in view
self.login = login;
function login(){
var credentials = {
email: self.email,
password: self.password
};
$http.post("/api/authenticate", credentials)
.success(function(data){
console.log(data);
setAccessToken(data.token);
self.email = null;
self.password = null;
})
.error(function(data){
console.log(data);
});
function setAccessToken(token){
window.sessionStorage.setItem("access_token", token);
}
}
Update the dogsController.js
//app/assets/javascripts/angular-controllers/dogsController.js
...
var self = this;
var accessToken = window.sessionStorage.access_token;
...
- The access token should be set in Local Storage (check your browser under the Resources tab)
- Next we want the form to go away if the user is authenticated
<!--app/assets/javascripts/templates/index.html -->
<form class="login-form" ng-submit="auth.login()" ng-if="auth.isAuthenticated == false">
//app/assets/javascripts/angular-controllers/authenticationController.js
//add this inside the AuthenticationController constructor function
self.isAuthenticated = isAuthenticated();
function isAuthenticated(){
return window.sessionStorage.access_token ? true : false;
}
- Don’t forget to update your createDog method to send the access token
//app/assets/javascripts/angular-controllers/dogsController.js
self.createDog = function(){
// firstName,etc are being passed on the form
var newDog = {
name:self.name,
breed: self.breed,
age: self.age,
token: accessToken
};
FINISHED authenticationController.js
angular
.module("dogApp")
.controller("AuthenticationController", AuthenticationController);
AuthenticationController.$inject = ["$http"];
function AuthenticationController($http){
var self = this;
self.email; //bound to form in view
self.password; //bound to form in view
self.login = login;
self.logout = logout;
self.isAuthenticated = isAuthenticated();
function login(){
var credentials = {
email: self.email,
password: self.password
};
console.log(credentials);
$http.post("/api/authenticate", credentials)
.success(function(data){
console.log(data);
setAccessToken(data.token);
self.isAuthenticated = isAuthenticated();
self.email = null;
self.password = null;
})
.error(function(data){
console.log(data);
});
}//end login function
function logout(){
window.sessionStorage.clear();
self.isAuthenticated = isAuthenticated();
}//end logout function
function setAccessToken(token){
window.sessionStorage.setItem("access_token", token);
}
function isAuthenticated(){
return window.sessionStorage.access_token ? true : false;
}
}
FINISHED dogsController.js
angular.module('dogApp')
.controller('DogsController', DogsController);
DogsController.$inject = ['$http', '$routeParams', '$window'];
function DogsController($http, $routeParams, $window){
var self = this;
var accessToken = window.sessionStorage.access_token;
self.params = $routeParams;
//get error response from server
self.error;
self.getDogIndex = function(){
return $http({
method: 'GET',
url: '/api/dogs'
})
.success(function(data){
console.log("get index worked");
self.jsonDogs = data;
})
.error(function(data){
console.log('error!');
console.log(data);
});
};
///////////////////
self.createDog = function(){
var newDog = {
name: self.name,
breed: self.breed,
age: self.age,
token: accessToken
};
console.log(newDog);
$http.post("/api/dogs", newDog)
.success(function(data){
console.log('successfuly created dog');
console.log(data);
$window.location.href = ('#/dogs/' + data.id);
})
.error(function(data){
console.log(data);
console.log('something went wrong!');
self.error = data.error;
});
};
///////////////
self.showDog = function(){
var url = "/api/dogs/" + self.params.id;
console.log(self.params);
console.log(url);
$http.get(url)
.success(function(data){
console.log('successful show request');
self.currentDog = data;
})
.error(function(data){
console.log('something went wrong!');
});
}
}
dogsController.js
- Add this line to to the .error portion of the method you want to protect (e.g., createDog, editDog)
//app/assets/javascripts/angular-controllers/dogsController.js
.error(function(data){
console.log(data);
console.log('something went wrong!');
self.error = data.error;
});
Updating the Views
- Add this HTML to your desired template (e.g., new.html, edit.html)
<!--app/assets/javascripts/templates/index.html -->
<div ng-if="dogsCtrl.error">
{{dogsCtrl.error}}
</div>
authenticationController.js
//app/assets/javascripts/angular-controllers/authenticationController.js
function logout(){
window.sessionStorage.clear();
self.isAuthenticated = isAuthenticated();
}//end logout function
Updating the views
- Add this HTML to your desired template (e.g., new.html, edit.html)
<!--app/assets/javascripts/templates/index.html -->
<div ng-if="auth.isAuthenticated">
<button ng-click="auth.logout()">Logout</button>
</div>