Angular & Rails: Part 3
The example application mentioned in this post can be found here: https://justcheckin.herokuapp.com
As a follow up to my previous post talking about the benefits of using AngularJS and Ruby on Rails together on your next project. I’d like to go through a tutorial of how to build a simple application using both technologies.
In this tutorial, I’ll go over building the frontend (using AngularJS) and how to integrate with your Rails backend as created with my previous blog post. You can also start from the beginning if you’d like.
Prerequisites
You will need to have gone to my previous post to create the backend application, using Ruby on Rails and PostgreSQL for the database. You will also need to have Node and Node Package Manager (NPM) install on your local, which we will use to install other dependencies.
Setting Up Your Environment
There are various ways to add Angular to your application, but the easiest way I have found is to use a generator. Yeoman is a generator to help improve your application workflow. To use Yeoman with Angular, we will need to also install Grunt and Bower. Grunt is a task runner for JavaScript (helping you run your angular app locally and compile it for deploy), and Bower is a package manager to manage your internal dependencies for your Angular front-end application.
1. To install these dependencies, run the following command in your terminal
npm install -g yo grunt-cli bower
2. We will also need a specific generator for Angular. So install it by running the following command on your terminal
npm install -g generator-angular
3. Alright, so now we are able to generate the front-end application. Navigate to the root of your existing Rails application, and create a folder named “client”. In your terminal navigate to this folder, and run the following command
yo angular
4. After going through the basic terminal prompt with the generator, it will create all the necessary files in the “client” folder. Navigate to this folder with your terminal, and run the following command to run this Angular application with Grunt.
grunt serve
5. If everything was run successfully to create and run the application, the basic app will open in a new tab within your default browser.
Let’s Do Some Basic Configuration
1. In your development environment, getting the frontend and backend to talk to each other requires a little bit of setup. It will require a proxy. A proxy will allow requests to be made to the front-end application (running by default at port 9000) to your backend application. When we set up factories in a few more steps, it will be imperative that this is set up correctly for data to be posted and retrieved correctly. Follow the following tutorial to accomplish this:
http://www.hierax.org/2014/01/grunt-proxy-setup-for-yeoman.html
2. Now within your “client” folder, locate the “scripts” folder. You will see 2 things: a “controllers” folder & an app.js file.
- The “controllers” folder will store all of the controllers that you create to be the middle layer between the view and the rest of your application
- The app.js file is your configuration file for your application, which will help you inject your 3rd party dependencies and with Yeoman’s example let you define routes within your application.
3. Open the app.js file and towards the beginning, modify the name of your application to “clientApp”:
[sourcecode language=”javascript”]
.module(‘clientApp’, [
[/sourcecode]
This is the name that identifies your angular application. Without this, you will be unable to add the other services in different files or build correctly.
4. Locate the bower.json file in the root of your “client” folder. Open this file and modify the module name to “clientApp”:
[sourcecode language=”javascript”]
"appPath": "app",
"moduleName": "clientApp",
[/sourcecode]
5. Locate the gruntfile.js in the root of your “client” folder. Open this file and modify the ngTemplates task to update the module name to “clientApp”:
[sourcecode language=”javascript”]
ngtemplates: {
dist: {
options: {
module: ‘clientApp’,
htmlmin: ‘<%= htmlmin.dist.options %>’,
usemin: ‘scripts/scripts.js’
},
cwd: ‘<%= yeoman.app %>’,
src: ‘views/{,*/}*.html’,
dest: ‘.tmp/templateCache.js’
}
},
[/sourcecode]
Let’s Code the Frontend
1. Navigate back to your “scripts” folder and create a factories.js file. In this file copy/paste the following code.
[sourcecode language=”javascript”]
angular.module(‘clientApp’)
.factory(‘MainFactory’, [‘$http’, function ($http) {
var baseUrl = ‘/api’;
var mainFactory = {};
mainFactory.recentCheckins = function() {
return $http.get(baseUrl + ‘/recent_checkins’);
};
mainFactory.checkin = function(browserInfo) {
return $http.post(baseUrl + ‘/checkin’, {"checkin": browserInfo});
};
return mainFactory;
}]);
[/sourcecode]
This code depicts a factory that is being added the client clientApp. A factory is a type of Angular service that is used to add properties to an object and return the object. In conjunction with the $http service we injected in it, it will let us perform HTTP requests, add the results to an object, and return that object.
2. Now that we have our factories available, we can add them to our main controller. Navigate to the “scripts” folder and locate the “controllers” folder. Within the “controllers” folder, Yeoman would have created a main.js file, which is your Main Controller. Open it and replace the code with the following:
[sourcecode language=”javascript”]
angular.module(‘clientApp’)
.controller(‘MainCtrl’, [‘$scope’,’MainFactory’, function ($scope, MainFactory) {
$scope.recentCheckins = [];
getCheckins();
function getCheckins() {
MainFactory.recentCheckins()
.success(function(checkins) {
var newCheckins = []
for (var i in checkins) {
var newCheckin = {};
newCheckin.id = checkins[i][‘id’];
newCheckin.browser = checkins[i][‘browser’];
newCheckin.os = checkins[i][‘os’];
newCheckin.mobile = checkins[i][‘mobile’] ? ‘Yes’ : ‘No’;
newCheckin.date = new Date(checkins[i][‘created_at’]);
newCheckins.push(newCheckin);
}
$scope.recentCheckins = newCheckins;
})
.error(function(){
console.log(‘Something went wrong!’);
})
};
}]);
[/sourcecode]
As you can see in the snippet above, the controller is injected with $scope and MainFactory. We will use $scope when interacting with the view, but MainFactory will be used within the getCheckins function. The getCheckins function gets executed on instantiation of the Main Controller. The getCheckins function consists of calling the MainFactory’s recentCheckin’s function and loads a recentCheckins $scope array variable on success of the factory’s function. That $scope variable will be accessible on the view that is bound to this controller.
3. Now that we have the data available from the factory within our Main Controller, let’s add a view that can interact with this data. Navigate to the folder “views” (“client” > “app” > “views”). Yeoman should have put a main.html file in there which we will replace with the following code:
[sourcecode language=”html”]
<div ng-controller="MainCtrl as main" >
<div class="row">
<div class="col-lg-12">
<h2>Past Checkins (Last 25)</h2>
<table class="table">
<thead>
<tr>
<th>Number</th>
<th>Browser</th>
<th>OS</th>
<th>On Mobile?</th>
<th>Date</th>
</tr>
</thead>
<tbody ng-repeat="checkin in recentCheckins">
<tr>
<th scope="row">{{checkin.id}}</th>
<td>{{checkin.browser}}</td>
<td>{{checkin.os}}</td>
<td>{{checkin.mobile}}</td>
<td>{{checkin.date | date:’MM/dd/yyyy @ h:mma’}}</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
[/sourcecode]
As you can see we bind the block to the Main controller using the ng-controller directive. We define a table and repeat the recentCheckins array using the ng-repeat directive. This is where $scope comes in. Since we have $scope accessible in the controller, the recentCheckins array becomes available on the view without having to access the controller directly (There are alternatives to using Scope, but I think it looks the cleanest). Then to format the date column’s data, we are using an Angular filter.
4. Well without data, the “recent 25” table will be pretty barren, so let’s add a way to interact with adding data to the model. Two components will be necessary: 1) We will need to capture information about the user’s browser and make it easy for our app to consume (which I copied from a wonderful StackOverflow post). 2) We will need to interact with our other MainFactory’s checkin function, which will POST data to our backend.
[sourcecode language=”javascript”]
$scope.checkin = function() {
var browser = getInfoFromBrowser();
var object = {
"browser": browser[‘browser’],
"mobile": browser[‘mobile’],
"os": browser[‘os’]
};
MainFactory.checkin(object)
.success(function(checkin) {
getCheckins();
})
.error(function() {
console.log(‘Something improbable happened’);
})
};
function getInfoFromBrowser() {
var unknown = ‘-‘;
//browser
var nVer = navigator.appVersion;
var nAgt = navigator.userAgent;
var browser = navigator.appName;
var version = ” + parseFloat(navigator.appVersion);
var majorVersion = parseInt(navigator.appVersion, 10);
var nameOffset, verOffset, ix;
// Opera
if ((verOffset = nAgt.indexOf(‘Opera’)) != -1) {
browser = ‘Opera’;
version = nAgt.substring(verOffset + 6);
if ((verOffset = nAgt.indexOf(‘Version’)) != -1) {
version = nAgt.substring(verOffset + 8);
}
}
// MSIE
else if ((verOffset = nAgt.indexOf(‘MSIE’)) != -1) {
browser = ‘Microsoft Internet Explorer’;
version = nAgt.substring(verOffset + 5);
}
// Chrome
else if ((verOffset = nAgt.indexOf(‘Chrome’)) != -1) {
browser = ‘Chrome’;
version = nAgt.substring(verOffset + 7);
}
// Safari
else if ((verOffset = nAgt.indexOf(‘Safari’)) != -1) {
browser = ‘Safari’;
version = nAgt.substring(verOffset + 7);
if ((verOffset = nAgt.indexOf(‘Version’)) != -1) {
version = nAgt.substring(verOffset + 8);
}
}
// Firefox
else if ((verOffset = nAgt.indexOf(‘Firefox’)) != -1) {
browser = ‘Firefox’;
version = nAgt.substring(verOffset + 8);
}
// MSIE 11+
else if (nAgt.indexOf(‘Trident/’) != -1) {
browser = ‘Microsoft Internet Explorer’;
version = nAgt.substring(nAgt.indexOf(‘rv:’) + 3);
}
// Other browsers
else if ((nameOffset = nAgt.lastIndexOf(‘ ‘) + 1) < (verOffset = nAgt.lastIndexOf(‘/’))) {
browser = nAgt.substring(nameOffset, verOffset);
version = nAgt.substring(verOffset + 1);
if (browser.toLowerCase() == browser.toUpperCase()) {
browser = navigator.appName;
}
}
// trim the version string
if ((ix = version.indexOf(‘;’)) != -1) version = version.substring(0, ix);
if ((ix = version.indexOf(‘ ‘)) != -1) version = version.substring(0, ix);
if ((ix = version.indexOf(‘)’)) != -1) version = version.substring(0, ix);
majorVersion = parseInt(” + version, 10);
if (isNaN(majorVersion)) {
version = ” + parseFloat(navigator.appVersion);
majorVersion = parseInt(navigator.appVersion, 10);
}
// mobile version
var mobile = /Mobile|mini|Fennec|Android|iP(ad|od|hone)/.test(nVer);
// cookie
var cookieEnabled = (navigator.cookieEnabled) ? true : false;
if (typeof navigator.cookieEnabled == ‘undefined’ && !cookieEnabled) {
document.cookie = ‘testcookie’;
cookieEnabled = (document.cookie.indexOf(‘testcookie’) != -1) ? true : false;
}
// system
var os = unknown;
var clientStrings = [
{s:’Windows 10′, r:/(Windows 10.0|Windows NT 10.0)/},
{s:’Windows 8.1′, r:/(Windows 8.1|Windows NT 6.3)/},
{s:’Windows 8′, r:/(Windows 8|Windows NT 6.2)/},
{s:’Windows 7′, r:/(Windows 7|Windows NT 6.1)/},
{s:’Windows Vista’, r:/Windows NT 6.0/},
{s:’Windows Server 2003′, r:/Windows NT 5.2/},
{s:’Windows XP’, r:/(Windows NT 5.1|Windows XP)/},
{s:’Windows 2000′, r:/(Windows NT 5.0|Windows 2000)/},
{s:’Windows ME’, r:/(Win 9x 4.90|Windows ME)/},
{s:’Windows 98′, r:/(Windows 98|Win98)/},
{s:’Windows 95′, r:/(Windows 95|Win95|Windows_95)/},
{s:’Windows NT 4.0′, r:/(Windows NT 4.0|WinNT4.0|WinNT|Windows NT)/},
{s:’Windows CE’, r:/Windows CE/},
{s:’Windows 3.11′, r:/Win16/},
{s:’Android’, r:/Android/},
{s:’Open BSD’, r:/OpenBSD/},
{s:’Sun OS’, r:/SunOS/},
{s:’Linux’, r:/(Linux|X11)/},
{s:’iOS’, r:/(iPhone|iPad|iPod)/},
{s:’Mac OS X’, r:/Mac OS X/},
{s:’Mac OS’, r:/(MacPPC|MacIntel|Mac_PowerPC|Macintosh)/},
{s:’QNX’, r:/QNX/},
{s:’UNIX’, r:/UNIX/},
{s:’BeOS’, r:/BeOS/},
{s:’OS/2′, r:/OS/2/},
{s:’Search Bot’, r:/(nuhk|Googlebot|Yammybot|Openbot|Slurp|MSNBot|Ask Jeeves/Teoma|ia_archiver)/}
];
for (var id in clientStrings) {
var cs = clientStrings[id];
if (cs.r.test(nAgt)) {
os = cs.s;
break;
}
}
var osVersion = unknown;
if (/Windows/.test(os)) {
osVersion = /Windows (.*)/.exec(os)[1];
os = ‘Windows’;
}
switch (os) {
case ‘Mac OS X’:
osVersion = /Mac OS X (10[._d]+)/.exec(nAgt)[1];
break;
case ‘Android’:
osVersion = /Android ([._d]+)/.exec(nAgt)[1];
break;
case ‘iOS’:
osVersion = /OS (d+)_(d+)_?(d+)?/.exec(nVer);
osVersion = osVersion[1] + ‘.’ + osVersion[2] + ‘.’ + (osVersion[3] | 0);
break;
}
// flash (you’ll need to include swfobject)
/* script src="//ajax.googleapis.com/ajax/libs/swfobject/2.2/swfobject.js" */
var flashVersion = ‘no check’;
if (typeof swfobject != ‘undefined’) {
var fv = swfobject.getFlashPlayerVersion();
if (fv.major > 0) {
flashVersion = fv.major + ‘.’ + fv.minor + ‘ r’ + fv.release;
}
else {
flashVersion = unknown;
}
}
return {
browser: browser,
browserVersion: version,
mobile: mobile,
os: os,
osVersion: osVersion,
cookies: cookieEnabled,
flashVersion: flashVersion
}
};
[/sourcecode]
As you can see, in the success callback of the checkin function we will call getCheckins, which will reload the recentCheckins array. The cool thing about AngularJS is that we don’t have to do anything else besides reloading the array to refresh the table.
5. Let’s hookup this checkin function to our view, so that we can POST information to our backend. Add the following code above the table within our div that is bound to the Main controller:
[sourcecode language=”html”]
<div class="row">
<div class="col-lg-8 leadContainer">
<p class="lead">
Take a second, and say hello!
</p>
<a class="btn btn-lg btn-primary" ng-click="checkin()">Checkin <span class="glyphicon glyphicon-ok"></span></a>
</div>
<div class="col-lg-4">
</div>
</div><br />
[/sourcecode]
Using the ng-click directive, the checkin function will be executed on button click. This will then feed into our backend method and on success refresh the table to show the latest checkin.
6. Lets add some basic style adjustments to the container. Navigate to the “styles” folder (“client” > “app” > “styles”) and locate the main.scss file. Open the file and add the following class:
[sourcecode language=”css”]
.leadContainer {
margin-bottom: 25px;
}
[/sourcecode]
7. Now back on your terminal, create 2 separate tabs. You will need to start the rails server first (rails s if you are using the built-in server or foreman start if you are using foreman). To start up the angular server with Grunt, run the following command
grunt serve
8. Everything should now work to create checkins and view the last 25!