How to Try Out Angular in your Existing Rails App
So you have a Rails application (we're going to assume Rails 4.X here) that uses the traditional setup. You create a resource /things
that of course has an index
action and its associated show
actions (/things/:id
). Like all rails apps, the templates, styles, and javascripts are served in a very opinionated fashion, that overall, makes it easier. Woohoo!
Turns out though, this makes doing things like adding in SPA (Angular 1.x) frameworks rather tricky. Sure it's not difficult
but its full of gotchas. Furthermore, every damn tutorial out there just teaches you how to integrate Rails and Angular as if you're starting from scratch! We're going to share how we did it at PeopleSpark in our application.
What we'll cover
0) Picking out a way to manage easily adding in front end assets with Bower Rails
1) Segmenting off a portion of your application to just use angular and creating the associated route.
2) Creating new, separate JS and CSS manifests to be served with the newly created angular side of your app.
3) Walking through the code required to get angular application up and running.
4) More tips, gotchas, if going further.
Note: This assumes basic knowledge of AngularJS and Rails
0 - Managing Your Front End Assets
We'll keep this short, since it seems most have started using something a front end manager or have their own way.
Bower Rails is an amazing way to manage your front end assets. Follow the instructions on the github to get it setup inside of your Rails application. We use the Ruby DSL configuration. Doing so would make your process look like the following:
a) add gem "bower-rails"
to your gemfile
b) run rails g bower_rails:initialize
c) open up (and/or make) a file in your root directory called Bowerfile
and include Angular
like so:
asset "angular"
Now in any manifest file, you'll be able to simply add:
//= require angular
and have it available for usage. Similarly if the front end library has stylesheets that come with it you'll be able to do the same thing in your stylesheet manifest.
1 - Create the new SPA (Angular) route and scaffolding out files
Admittedly, we made the mistake of trying to use the same default resources route (i.e. /things
) for the Angular route. And it was a mistake. Why? A couple of reasons:
Angular and Rails will fight over who controls the route. I.e. if you go from
/things
to/things/:id
rails will still control it without having to do a bunch of front end magic.Due to the previous point, client side routing doesn't work with HTMl5 history API. In english that means instead of
/things/cool
you have to do/things/#/cool
.Webkit browsers get confused with caching both
html
andjson
types at the same route. This means that sometimes your User will see a nastyjson
dump instead of the correct HTML.
The above means one of two things. You either need to overhaul your resource route to do something like namespacing (/api/things
), OR, since we're just trying this out anyway, make a new route. We're going to do the latter for now. So we're going to create a bunch of files and folders now...
a) Open up config/routes.rb
and add a new route for your angular app. You can really call this what ever you want. We'll call this /spa-things
.
# config/routes.rb
...
get '/spa-things/(*path)' => 'spa_things#index'
...
This allows us to do HTML5 routing in our angular application. Basically, every route nested in spa-things
just gets pointed back to the index which allows Angular to take care of it on the clientside.
b) Create a new folder at app/views/spa_things
.
c) Create a new Rails controller at app/controllers/spa_things_controller.rb
and inside put the following
class SpaThingsController < ApplicationController
# any auth code, callbacks, helpers, etc
def index
end
end
d) Create a new file at app/views/spa_things/index.html.erb
.
e) Create a new folder at app/assets/javascripts/spa-things
.
This will house your new angular app and all of its associated files.
f) Create a new file at app/assets/javascripts/spa-things/application.js
In Rails 4.x at least, we get a fun convention that helps out SPA'ifying. In JS folders, if you specify an application.js
, it will be treated like a manifest file, just like any other manifest!
g) Open up your app/assets/javascripts/spa-things/application.js
file and put the following:
//= require angular
//= require_tree .
//... we'll be putting more stuff here later
h) Create another file at app/assets/javascripts/spa-things/spa-things.controller.js
and leave it empty for now.
After doing so, all needed files should be setup and we should be ready to start filling in angular.
2 - Get the Actual Angular App up and Running
a) Let's open up app/views/spa_things/index.html.erb
and input the following:
<section
ng-controller="spaThingsController"
ng-app="spaThings">
<ul>
<li ng-repeat="thing in things">
{{thing.name}}
</li>
</ul>
</section>
<%= javascript_include_tag "spa-things/application" %>
A few things are happening here.
First, we're looking to bootstrap an Angular application that's the camelCase
version of your respective controller. In this case that means we'll be bootstrapping an app named spaThings
. Obviously, this is to keep to proper JS naming conventions.
Second, we're telling rails to look for an application manifest file in the app/assets/javascripts/
directory that has the "dasherized" version of you controller. In this case it's looking for app/assets/javascrips/spa-things/application.js
. We do this to keep to the html/css common folder naming convention.
Now obviously if you boot this up right now you'll just get an error in the browser telling you that no such app spaThings
exists. Let's make that next.
b) Open up your app/assets/javascripts/spa-things/application.js
and make the following modifications:
//= require angular
//= require_tree .
//... we'll be putting more stuff here later
//... and now we're putting stuff here
angular.module('spaThings', ['spaThings.controller'])
.config([
'$httpProvider',
'$locationProvider',
function ($httpProvider, $locationProvider) {
// Send CSRF token with every http request
$httpProvider.defaults.headers.common["X-CSRF-Token"] = $("meta[name=csrf-token]").attr("content");
$locationProvider.html5Mode({
enabled: true,
requireBase: false
});
}]);
This should be pretty straight forward angular code. First, we're going to make sure that our CSRF token is sent with any ajax requests. Second, we're going to allow for HTML5 routing. This means no nasty hashes in the URLs.
If you wanted to expand our spa-things
app into more routes, you'd do so here (hopefully with something like UI-Router).
An added nice thing here is it lets you build out this javascript section in a feature / pod oriented manner instead of a miserable dirty sock drawer.
c) Open up your app/assets/javascripts/spa-things/spa-things.controller.js
and input the following:
angular.module('spaThings.controller', [])
.controller('spaThingsController', ['$scope', function ($scope) {
$scope.things = [{ name: 'angular' }, { name: 'rails' }, { name: 'doing things together' }];
}]);
Nothing new or tricky here.
Now you should be able to hit your new route /spa-things
and indeed confirm that Angular and Rails are doing things together.
3 Growing Angular more into your Application
So now your ready to take the next step and begin moving Angular into many parts of your application! If you want to do so, there's some helpful things you can do to avoid hard coding each and every Angular application (if you're introducing it piecemeal).
Open up your application layout
file, whatever that is. Modify it in the following ways:
<!DOCTYPE html>
<html lang="en">
<% controller_app = controller.controller_name.camelize(:lower) %>
<head>
<!-- head tag items -->
</head>
<body ng-app="<%= controller_app %>">
<!-- other rendered partials -->
<%= yield %>
<%= javascript_include_tag 'your_old_application_js_manifest' %>
<%= javascript_include_tag "#{controller_app.underscore.dasherize}/application" if is_spa_page %>
</body>
</html>
note: this will load the application only if is_spa_page
which should just be a custom helper to check against the controller. Something like if controller.controller_name == 'spa'
If you were using this with your example we just made you'd modify app/views/spa_things/index.html.erb
to look like the following:
<section
ng-controller="spaThingsController">
<ul>
<li ng-repeat="thing in things">
{{thing.name}}
</li>
</ul>
</section>
So NOW. Whenever your application layout gets rendered, it will:
a) Look for an application manifest at app/assets/controller-name/applcation.js
b) Load said application manifest and its directory.
c) Attempt to bootstrap an angular application called controllerName
.
4 Spa'ifying Multiple Sections of your Application
Above we covered how to just create one segmented area of your rails app to be Angularized. But what if you want to do multiple areas?
We probably don't want a controller for each and every spa route, since they'll be pretty empty. So you can take that SPA controller we used above and do something like:
class SpaThingsController < ApplicationController
# any auth code, callbacks, helpers, etc
def routeOne
render `routeOne/index`
end
def routeTwo
render `routeTwo/index`
end
def routeThree
render `routeThree/index`
end
end
And then in your routes do something like:
# routes.rb
get 'routeOne/(*path)' => 'spa_things#routeOne'
get 'routeTwo/(*path)' => 'spa_things#routeTwo'
get 'routeThree/(*path)' => 'spa_things#routeThree'
And then in your application layout
file you need to modify your conditional
<%= javascript_include_tag "#{controller_app.underscore.dasherize}/application" if is_spa_page %>`
is_spa_page
helper to account for the controller.action_name
instead of just looking at the name of the controller.
This will let you completely segment these applications out.
Conclusion
The Rails environment is incredibly opinionated in the way it handles nearly everything. Therefore taking big leaps with adding in new concepts, like Angular, can be taxing and not seem worth the time sink. Hopefully this will help save some other developer and / or team some headache of arduous trial and error.
Note: If you have any thoughts, comments, or catch any bugs, please do tell!
J Cole Morrison
http://start.jcolemorrison.comDeveloper Advocate @HashiCorp, DevOps Enthusiast, Startup Lover, Teaching at awsdevops.io