Rails and Ionic Make Love Flashcards
Setup Project
$mkdir blog_app_project
$cd blog_app_project
Setup Basic Rails App
$rails new BlogApp $cd BlogApp $mv BlogApp web $bundle install $rails g scaffold BlogEntries title:string content:text $rake db:migrate
Active Model Serializers: use the AMS gem when building RESTful API’s in Rails
blog_entry_seriailizer.rb
#/web/app/serializers/blog_entry_seriailizer.rb class BlogEntrySerializer
Active Model Serializers
active_model_serializer.rb
Ionic uses Angular,by default receive data w/o root node. Need to disable the root node in the JSON output.
#/web/config/initializers/active_model_serializer.rb ActiveSupport.on_load(:active_model_serializers) do # Disable for all serializers (except ArraySerializer) ActiveModel::Serializer.root = false # Disable for ArraySerializer ActiveModel::ArraySerializer.root = false end
Test the Rails JSON API
$rails s
localhost: 3000/blog_entries/new
localhost: 3000/blog_entries.json
Setup Rack Cors
Installin Gem #/web/Gemfile
gem ‘rack-cors’, :require => ‘rack/cors’
Setup Rack Cors
Configuring the middleware #/web/config/application.rb
config.middleware.insert_before 0, “Rack::Cors” do
allow do
origins ‘’
resource ‘’, :headers => :any, :methods => [:get, :put, :delete, :post, :options]
end
end
Create The Ionic App ionic start ionic_blog_app tabs
mv ionic_app mobile
- blog_app_project
- web
- … rails app
- mobile
- … ionic app
Angular $resource
Ionic and RESTful resources we tend to use the Angular $resource service.
Angular $resourceAnd inject ngResource into your app module:
/ /mobile/www/js/app.js /
angular.module(‘starter’, [‘ionic’, ‘starter.controllers’, ‘starter.services’, ‘ngResource’])
add the BlogEntry factory Note: for production apps we wouldn't hardcode these URL's, but rather have a module that determines whether to fire requests to the production, staging or the local environment.
/ /mobile/www/js/services/js /
.factory(‘BlogEntry’, function($resource) {
return $resource(“http://localhost:3000/blog_entries/:id.json”);
})
Update Ionic Controller and Route
pull our blog entries JSON from the Rails app and display it in the Ionic app
first ‘Status’ tab. This Status tab is named as the DashCtrl and tab-dash.html t
/ /mobile/www/js/controllers.js: /
.controller(‘DashCtrl’, function($scope, BlogEntry) {
BlogEntry.query().$promise.then(function(response){
$scope.blog_entries = response;
}); })
Injecting the BlogEntry factory into the DashCtrl.
Query the BlogEntry service, returns an Angular $promise as the asynchronous toRails app.
Rails app responds and this promise is resolvedreturned collection to $scope.blog_entries and available to our view.
blog_entries con the scope, can interate over the collection in tab-dash.html.
/ /mobile/www/templates/tab-dash.html /
<div class="list card"> <div class="item item-divider">{{blog_entry.title}}</div> <div class="item item-body"> <div> {{blog_entry.content}} </div> </div> </div>
Install Devise in your Rails App
$rake db:migrate
gem ‘devise’
$bundle install
$rails generate devise:install
$rails generate devise User
Changes to Devise
# web/config/environments/development.rb config.action_mailer.default_url_options = { host: 'localhost', port: 3000 }
Adding our custom Devise controllers to the routes.
# web/app/config/routes.rb: devise_for :users, :controllers => {sessions: 'user/sessions', registrations: 'user/registrations', passwords: 'user/passwords' }
Create the three new controllers to handle JSON in Devise
Sessions Controller
# web/app/controllers/user/sessions_controller.rb class User::SessionsController
Create the three new controllers to handle JSON in Devise
Registrations Controller
# web/app/controllers/user/registrations_controller.rb class User::RegistrationsController
Create the three new controllers to handle JSON in Devise
Password Controller
# web/app/controllers/user/passwords_controller.rb class User::PasswordsController
Authentication across Rails & Ionic.
Disable CSRF protection
# /web/app/controllers/application_controller.rb # protect_from_forgery with: :exception
Create a new user
$bundle exec rails s -p 3000
locahost:3000/users/sign_up
Implementing Devise login in Ionic
Hook up Ionic to login through the Devise JSON controller we just added.
Four things to make all of this work on the Ionic end: -A UserSession factory, to abstract away some of our API implementation
- A login form
- A login controller
- A login route
Define this UserSession factory in services.js
// /mobile/www/js/services.js: .factory('UserSession', function($resource) { return $resource("http://localhost:3000/users/sign_in.json"); })
Creating the login form template
- binding a data object, with the attributes password and email, onto our $scope
- assign an ng-click directive to our Login button, call the login() in controller.
<div>
<span>Email</span>
<span>Password</span>
</div>
<div class="padding"> Login </div>
The login() function composes almost all of this controller.
- This login() method is called when a user clicks on the Login button. It does the following:
- Creates a new user_session, with the data from our login form.
- Attempts to save this user_session, hitting the Devise JSON end-point
- If successful, we save the userId and userName to localStorage, and redirect the user to the Dash path
// /mobile/www/controllers.js .controller('LoginCtrl', function($scope, $location, UserSession, $ionicPopup, $rootScope) { $scope.data = {}; $scope.login = function() { var user_session = new UserSession({ user: $scope.data }); user_session.$save( function(data){ window.localStorage['userId'] = data.id; window.localStorage['userName'] = data.name; $location.path('/tab/dash'); }, function(err){ var error = err["data"]["error"] || err.data.join('. ') var confirmPopup = $ionicPopup.alert({ title: 'An error occured', template: error }); } ); } })
Wire the login template w/ LoginCtrl (adding a new state in our app.js)
// /mobile/www/js/app.js .state('login', { url: '/login', templateUrl: 'templates/login.html', controller: 'LoginCtrl' })
Change the default route in app.js
// /mobile/www/js/app.js $urlRouterProvider.otherwise('/login');
Persist current_user:
Rails forgets who you are in just a single request!
To persist each request needs a session cookie
// /mobile/www/js/app.js .config(function($stateProvider, $urlRouterProvider, $httpProvider) { $httpProvider.defaults.withCredentials = true; // ... routes etc below
Increase the time it takes for the Devise login to expire
# web/initializers/devise.rb config.remember_for = 20.years
Making the Leap to Production -Check for authenticated users on each state change. and re-direct t if they were not authenticated.
- No registrations or passwords through Ionic and into our Rails app
- we have temporarily disabled CSRF protection forgery. You’d want to re-enable this with something like this strategy. Note: Cloudspace have released a nice and simple library that you might like to check out to supplement this info: angular_devise
Implement Capybara
gem ‘capybara’
gem ‘selenium-webdriver’
gem ‘chromedriver-helper’
gem ‘capybara-angular’
Set capybara
-Capybara.raise_server_errors = false. (
Once you have it up and running you will want to switch this to true, but setting it to false makes it easier to get started.)
-We ensure that a version of the ionic app is running on PORT=5000 and if thats not the case then we start an instance.
-We have to hack ActiveRecord a little bit to ensure that it will share db connections across threads, otherwise rails records wont be visible in ionic.
Basic setup with rails running on port 4321 and the ionic app running on port 5000
Capybara runs on port 4321
web/test/test_helper.rb
Dir[Rails.root.join(“test/helpers/*/.rb”)].each { |f| require f }
require ‘capybara/rails’
Capybara.register_driver :selenium do |app|
Capybara::Selenium::Driver.new(app, :browser => :chrome)
end
Capybara.always_include_port = true Capybara.server_port = 4321 Capybara.raise_server_errors = false
class ActionController::TestCase # Helpful if using Devise for user auth # include Devise::TestHelpers end
class ActionDispatch::IntegrationTest
# Make the Capybara DSL available in all integration tests
include Capybara::DSL
include Capybara::Angular::DSL
def setup
super
end
def teardown super Capybara.reset_sessions! Capybara.use_default_driver end end
Ensure that node server is running ionic app
unless lsof -i :5000
.include?(‘node’)
puts ‘Starting ionic node server’
Dir.chdir(“../mobile”) do
system “PORT=5000 nohup node server.js > /dev/null 2>&1 &”
end
end
# Force capybara to share db connections between threads. class ActiveRecord::Base mattr_accessor :shared_connection @@shared_connection = nil
def self.connection
@@shared_connection || retrieve_connection
end
end
ActiveRecord::Base.shared_connection = ActiveRecord::Base.connection
Create a helper toto make tests more readable.
web/test/helpers/ionic_helper.rb
module IonicHelper def on_ionic_app Capybara.app_host = 'http://localhost:5000' visit('/') begin yield rescue => error puts error ensure Capybara.app_host = 'http://localhost:4321' end end end
Creating tests
web/test/integration/blogentriestest.rb
require “test_helper”
class BlogEntriesTest
Running ionic app in background
in /mobile/server.js
var express = require(‘express’),
app = express();
app.use(express.static(‘www’));
app.set(‘port’, process.env.PORT || 5000);
app.listen(app.get(‘port’), function () {
console.log(‘Express server listening on port ‘ + app.get(‘port’));
});
Installing ExpressJS for tests
cd rails_and_ionic_make_love_part_three/mobile
npm install express
Ionic app needs to know if its in test mode so that it can talk to our test server instead of the development or production server.
mobile/www/js/services.js
.factory(‘api’, function() {
return {
url: function(path) {
return this.base() + path;
},
base: function() {
if ( this.isTestMode() ) {
return “http://localhost:4321/”
} else if ( this.isLocalhost() ) {
return “http://localhost:4444/”
} else {
return “https://production-url.com/”
}
},
isLocalhost: function() {
return ionic.Platform.platform() === “macintel” && !this.isHttps();
},
isTestMode: function() {
return location.port && location.port == “5000”;
},
isHttps: function() {
return window.location.origin.split(‘:’)[2] == “https”;
}
}
})
BlogEntries resource
.factory(‘BlogEntry’, function($resource, api) {
return $resource(api.url(“blog_entries/:id.json”));
})
Poltergeist Gem (to make tests go faster)
gem ‘poltergeist’
web/test/test_helper.rb
require ‘capybara/poltergeist’
if ENV[‘VIEW_IN_BROWSER’] == “true”
Capybara.register_driver :selenium do |app|
Capybara::Selenium::Driver.new(app, :browser => :chrome)
end
else
Capybara.javascript_driver = :poltergeist
end
Run tests
run using:
$ rake test a (for headless poltergeist driver.)
to debug a test: switch over to
$bundle exec rake test VIEW_IN_BROWSER=true