J Cole Morrison
J Cole Morrison

J Cole Morrison

Startup Engineering, former Techstars Hackstar, AWS Solutions Architect and Culture guy. Based out of Sacramento, California.


AngularJS, Rails 4.1 and UI Router Tutorial

Posted by J Cole Morrison on .
Featured

AngularJS, Rails 4.1 and UI Router Tutorial

Posted by J Cole Morrison on .

I'll be covering the following in this tutorial:

  • Setting up Angular and Rails 4.1
  • Using Bower to manage front end dependencies
  • Getting UI Router setup and running
  • Setting up Bootstrap via Bower with Rails
  • Doing the two above in HTML5 Mode for SEO benefits
  • Discussing gotchas / solutions for working with UI Router
  • Using NgAnimate with UI Router for seamless state transitions

If you've done my previous tutorial on Rails 4.1 and Angular you'll notice that the beginning is quite similar. I won't be assuming readers have done the previous tutorial, and will therefore explain choices of gems, code structure, etc. fully.

This isn't a tutorial about the in depth mechanics of Rails, Angular, UI-Router, etc. I'll try and explain the trickier aspects of integrating the technologies, but not much beyond that.

The reason I wanted to go into UI Router is because it's so much more appropriate for REAL applications. For example, let's say you have a splash page with a navigation bar that appears on all non-logged in pages. At the same time you have a logged in set of pages that don't expose that navigation bar but instead show a side navigation. How do you go about showing/hiding that to navigation bar? By doing an ng-include on each and every view? By using ng-show and setting something to the $rootScope?? And after you do all of that, you'll wind up using setTimeout's galore just to get it to pop up at the right time...

Finally, I also have a github repo you can use for both reference and follow along. At step 13 I started creating a branch for each successive step so that you can see what you're code should look like incrementally. I didn't start prior to step 13 simply because most of the code is setup. "Part 2" is around Step's 11 and 12 though. Here's the link:

https://github.com/jcolemorrison/ui-router-tutorial

Without further delay:

1) Make a directory for your project and cd into it.

2) Create the new app:

$ rails new . database=postgresql

3) Setup the database:

$ rake db:create
$ rake db:migrate

4) Open up the Gemfile and add two gems to it

gem 'bower-rails'
gem 'angular-rails-templates'

If you haven't heard of Bower... I ... I .. just don't know what to say. It's an amazing front end package manager that will make your life exceptional. To use the rails version you MUST have both Bower AND Node.js installed.

As stated from my other post:

Angular Rails Templates is a helpful gem that makes working with angular templates far easier. It also kills the AJAX requests per template by leverage the template cache. This means less HTTP requests for your user and thus better performance. You'd honestly windup implementing what this package does even if you chose not to use it.

And that's why we're using it.

After doing so, stop your rails server (if you started it) and install the gems:

$ bundle install

5) Initialize Bower Rails

$ rails g bower_rails:initialize json

This will create a file in your root project directory called Bower.json. This is where you'll define front-end dependencies for your rails project.

6) Open up Bower.json and add the following to the "dependencies" property:

...
"name": "bower-rails generated lib assets",
"dependencies": {
    "angular": "latest",
    "angular-ui-router": "latest",
    "angular-animate": "latest",
    "bootstrap-sass-official": "latest"
}
...

7) Install the dependencies

$ rake bower:install

This puts the front end dependencies inside of lib/assets/bower_components.

Now we need to tweak our javascript manifest file to include and exclude some dependencies..

8) Open app/assets/javascripts/application.js and add the angular assets and remove turbolinks like so:

...
//= require turbolinks <-- get rid of this
//= require angular
//= require angular-animate
//= require angular-ui-router
//= require angular-rails-templates
//= require_tree .

9) Make the folder app/assets/templates/ and go back to app/assets/javascripts/application.js and include it in the manifest.

..
//= require_tree .
//= require_tree ../templates

This is a required step for the Angular Rails Templates package. It makes it so that we can use this folder to hold our html templates we'll be using with Angular.

10) Open up app/views/layouts/application.html.erb and remove the turbolink references in the stylesheet_link_tag and the javascript_include_tag within the <head> tag.

Turbolinks can interfere with Angular (well, client thick apps in general).

Now to start creating the angular code.

11) Create the file app/assets/javascripts/ng-app/app.js and input the following code:

angular
    .module('myApp', [
        'ngAnimate',
        'ui.router',
        'templates'
    ])
    .config(function ($stateProvider, $urlRouterProvider, $locationProvider) {
    });

As with any Angular project, this defines our module and modules it's dependent upon.

12) Go back to app/views/layouts/application.html.erb and add the ng-app attribute to the body tag like so:

<body ng-app="myApp">
...
</body>

13) Now we need to make sure that Rails allows angular to handle our client side routing. To do so open up config/routes.rb and input the following two lines:

root 'application#index'
get '*path' => 'application#index'

14) Open up app/controllers/application_controller.rb and add the following:

...
class ApplicationController < ActionController::Base
...
    # add these two lines below
    def index
    end
end
...

When someone hits our app, they'll be sent to this method, which will in turn provide a particular view file. Now we need to create it.

15) Create the file app/views/application/index.html.erb and put the following in it:

<div class="main-view-container" ui-view></div>

That's it. The class is going to be used to help animate in child views. The ui-view is an attribute for UI Router to know where to insert templates and views.

Now let's create some routes (states) for our angular app.

16) Open up app/assets/javascripts/ng-app/app.js and insert the following code:

... 
.config(function ($stateProvider, $urlRouterProvider, $locationProvider) {

    /**
     * Routes and States
     */
    $stateProvider
        .state('home', {
            url: '/',
            templateUrl: 'home.html',
            controller: 'HomeCtrl'
        });

    // default fall back route
    $urlRouterProvider.otherwise('/');

    // enable HTML5 Mode for SEO
    $locationProvider.html5Mode(true);
});

Defines a state called "home" which will be matched to the root route. Notice that our template URL is just home.html. The way that Angular Rails Templates works is that it makes your templates folder the "root" reference point for your angular code. So if your home.html.erb file was in a sub folder templates/example/home.html.erb you'd reference it like example/home.html in your angular code.

PS: Don't forget you can use the branches in the github repo to follow along and/or cross reference your code with mine.

Now let's create both the referenced template and controller.

17) Create the file app/assets/templates/home.html.erb and input the following:

<div class="container">
    <h1>The Home View!</h1>
    <ul>
        <li ng-repeat="thing in things">{{thing}}</li>
    </ul>
</div>

Also create the file app/assets/javascripts/ng-app/controllers/home.js and put the following code in:

angular.module('myApp')
    .controller('HomeCtrl', function ($scope) {
        $scope.things = ['Angular', 'Rails 4.1', 'UI Router', 'Together!!'];
    });

Now you should be able to open up your app in the browser and see it all working! If it doesn't pop up immediately, restart your rails server.

Surely you've noticed bootstrap isn't working. Let's change that. To do so we are going to do some configuration changes in config/application.rb that are more or less for convenience purposes.

18) Open up the config/application.rb file and add the following two lines in the class declaration:

...
class Application < Rails::Application
    ...
    config.assets.paths << Rails.root.join("lib","assets","bower_components","bootstrap-sass-official", "assets", "stylesheets")
    config.assets.paths << Rails.root.join("lib","assets","bower_components","bootstrap-sass-official", "assets","fonts")
end
...

This will make it so that we can reference bootstrap styles / fonts without having to type out a wall of text to reference it. Make sure you restart rails after making this change. Now let's actually bring in the bootstrap files.

19) open up app/assets/stylesheets/application.css and rename it to app/assets/stylesheets/application.css.scss. Delete the *= require_tree . and *= require_self lines. Also add:

@import "ng-app/main";

to application.css.scss. Next, make the above referenced file app/assets/stylesheets/ng-app/main.scss. And add the following line:

@import "bootstrap/bootstrap";

Now when you refresh, bootstrap should be up and running. If not, restart your rails server.

Next, we need to create all of the templates and sub states.

20) Create the different states we'll be using inside of the angular app. Open up app/assets/javascripts/ng-app/app.js and add the following lines:

    ...
    /**
     * Routes and States
     */
    $stateProvider
        .state('home', {
            url: '/',
            templateUrl: 'home.html',
            controller: 'HomeCtrl'
        }) // make sure you remove the semicolon

        // an abstract state that just serves as a
        // parent for the below child states
        .state('dashboard', {
            abstract: true,
            url: '/dashboard',
            templateUrl: 'dashboard/layout.html'
        })
            // the default route when someone hits dashboard
            .state('dashboard.one', {
                url: '',
                templateUrl: 'dashboard/one.html'
            })
            // this is /dashboard/two
            .state('dashboard.two', {
                url: '/two',
                templateUrl: 'dashboard/two.html'
            })
            // this is /dashboard/three
            .state('dashboard.three', {
                url: '/three',
                templateUrl: 'dashboard/three.html'
            });
    ...

The above references 4 templates we'll need to create in the UI-Router flavor. The most interesting one is the abstact state for 'dashboard'. Abstract states, for the most part, allow us to namespace states. Next, let's make a navigation bar so that we can actually get to the dashboard state.

Create the file app/assets/templates/navbar.html.erb and input the following code:

<nav class="navbar navbar-default" role="navigation">
  <div class="container">

    <div class="navbar-header">
      <a class="navbar-brand" href="/">My App!</a>
    </div>

    <ul class="nav navbar-nav">
      <li><a href="/">Home</a></li>
      <li><a href="/dashboard">Dashboard</a></li>
    </ul>

  </div>
</nav>

open up the the app/views/application/index.html.erb and add the following above the ui-view tag.

<div ng-include="'navbar.html'"></div>

Now let's create the following files and input their respective code.

First app/assets/templates/dashboard/one.html.erb and input the following:

<h2>First Dashboard View</h2>

Second app/assets/templates/dashboard/two.html.erb and input the following:

<h2>Second Dashboard View</h2>

Third app/assets/templates/dashboard/three.html.erb and input the following:

<h2>Third Dashboard View</h2>

Finally, we need to make the layout file for our Dashboard state. Create the file app/assets/templates/dashboard/layout.html.erb. Input the following:

<div class="container">
    <div class="row">
    <div class="col-sm-3 dashboard-navigation">
        <h4>Dashboard Navigation</h4>
        <ul class="nav nav-pills nav-stacked">
            <li><a ui-sref="dashboard.one">One</a></li>
            <li><a ui-sref="dashboard.two">Two</a></li>
            <li><a ui-sref="dashboard.three">Three</a></li>
        </ul>
    </div>
    <div class="col-sm-9">
        <div class="dashboard-view-container">
            <div class="dashboard-ui-view" ui-view></div>
        </div>
    </div>
    </div>
</div>

After doing so everything should be up and running!

Don't do this next snippet. It's just informational for if you are nesting views really deep with UI Router

Now.. let's have a talk about some errors that may occur when using UI Router. When you start nesting templates REALLY deep WITH Html 5 Mode, rails will sometimes get confused and try to send your layouts/application.html.erb file along with all of its baggage. It'll wind up in an infinite loop where angular keeps trying to nest everything (like the head tag) infinitely. This problem can be solved "easily" by just turning off HTML5 mode, but you lose all of the SEO benefits of doing so. So here's how to solve it properly.

Open up config/routes.rb and add the following line

root 'application#index'

get "*path.html" => "application#index", :layout => 0
get "*path" => "application#index"

This will watch for when angular requests templates instead of just a route. Now we need to modify the application index method. Open up app/controllers/application_controller.rb and change the index method to the following:

...
def index
    render layout: layout_name
end

private

def layout_name
    if params[:layout] == 0
        false
    else
        'application'
    end
end
...

This sends our layout conditionally.

Now that that's been said. Let's talk about animating these views.

21) Create two more stylesheets:

First create app/assets/stylesheets/ng-app/_globals.scss and input the following:

.main-view-container{
    // Animations for the separate UI Views
    &.ng-enter,
    &.ng-leave{
        position: absolute;
        left: 0;
        right: 0;
    }

    &.ng-enter{
        @include transition(.2s ease-out all .2s);
    }

    &.ng-leave{
        @include transition(.2s ease-out all);
    }

    &.ng-enter,
    &.ng-leave.ng-leave-active{
        @include opacity(0);
    }

    &.ng-leave,
    &.ng-enter.ng-enter-active{
        @include opacity(1);
    }
}

ng-leave, ng-enter, etc. are classes that angular throws on templates that are dynamically inserted in and out. In order to get them to layout right, you have to absolutely position them while their transitioning, otherwise you'll seem the separate views stack on top of each other.

Second create app/assets/stylesheets/ng-app/_dashboard.scss and input the following:

.dashboard-view-container{
    position: relative;
}
.dashboard-ui-view{
    @extend .main-view-container;
}

Here, we're just using the SASS @extend functionality to avoid rewriting that entire animation boilerplate.

Now open up app/assets/stylesheets/ng-app/main.scss and import the two files:

@import "bootstrap/bootstrap";
@import "globals";
@import "dashboard";

Now, refresh and watch your pretty app animate in and out!


Conclusion

I'm sure this is simple to someone, somewhere, but I wanted to put it out there. The most useful piece of this tutorial, in my opinion, is the gotcha on nested views. While building a bigger app, I ran into that problem and wanted to break my computer. And even though you can solve it by getting rid of html5Mode, it completely kills the benefits of SEO you can achieve as noted in this article.

Additionally, since there seems to be so much interest around this topic, I figured I'd go ahead and show how to setup Restangular with a Rails API as well. This tutorial will serve as the starting point for the next one.

Please tell me if you see any mistakes or typos!

J Cole Morrison

J Cole Morrison

http://start.jcolemorrison.com

Startup Engineering, former Techstars Hackstar, AWS Solutions Architect and Culture guy. Based out of Sacramento, California.

J Cole Morrison

J Cole Morrison

Startup Engineering, former Techstars Hackstar, AWS Solutions Architect and Culture guy. Based out of Sacramento, California.