Single-Page Applications

How To Get HTML5 pushState Working With Angular And Rails

There are three steps to getting HTML5 pushState working in an Angular/Rails SPA.

The first step is to enable pushState within Angular. This is super simple.

If you just do the first step it will appear to work, but you’ll have a subtle issue I call “the reload problem” that will need to be addressed for both the development environment and the production environment. I’ll get into the details of that shortly. For now just know that the steps are:

  1. Enable pushState in Angular
  2. Fix the reload problem for the development environment
  3. Fix the reload problem for the production environment

January 2015 update: it seems that “the reload problem” in the development environment is no longer an issue. I believe you can safely ignore the steps to fix this problem.

And by the way, this post makes a few assumptions about the way your project is set up. You may want to read or at least skim How to Write Up Ruby on Rails and AngularJS as a Single-Page Application before you dive deep into this post. My advice doesn’t require your app to be set up exactly this way for my basic instructions to make sense, but reading that post might help you understand where I’m coming from.

Enabling pushState Within Angular

Enabling push state is a matter of adding one line to your Angular config: $locationProvider.html5Mode(true);

In case you’d like to know exactly where you should be adding this line, here’s my complete app.js file (which includes the relevant line and also a bunch of irrelevant crap). Notice that I had to specifically add $locationProvider as a parameter to the anonymous function I’m passing to the first app.config.

You will of course also have to remove the #/ portion from any of your links. So if you have a link to /#/sign_in, you’ll have to change it to /sign_in. Hopefully you’re making this change early enough in your application’s life that that change is not a very big deal.

Free Guide

Getting Started with Angular and Rails

Get an Angular/Rails app up and running in as little as 20 minutes

You’ll also need to change your asset references from relative to absolute. So <script src="scripts/app.js"></script> will need to become <script src="/scripts/app.js"></script>.

The Reload Problem (And How To Fix It)

Like I said, if you enable pushState in Angular and that’s all you do, it will appear to work as you click around to your different links. But if you reload any page other than the root URL, it won’t work. Let me explain that in detail.

If you navigate to http://localhost:9000/ (which is known as the root URL) and then click on a link that goes to /sign_in, that state change (not location change because this is a single-page application) will be going through Angular’s routing. We’re good so far.

But if you’re at http://localhost:9000/sign_in and you click reload, it doesn’t work. The reason is that your server is now trying to find some page that lives at http://localhost:9000/sign_in and, correctly, it’s determining that such a page does not exist. And again, correctly, it’s showing you an error.

What we need to do is kind of trick the server a little bit. When we get a request for http://localhost:9000/sign_in, we need to actually serve up http://localhost:9000/ and then tell Angular that we want to be at http://localhost:9000/sign_in.

And by the way, http://localhost:9000/ and http://localhost:9000/index.html are functionally equivalent, and in our redirections we’ll be talking about /index.html. I mention this so you don’t get confused by the /index.html references.

Development Page Reloads

The solution in both production and development is to add a little redirect that takes you from whatever URI is requested and puts you at index.html. In development we add the redirect to Grunt because Grunt is serving our client-side part of our application. In production we add it to Rack because Rack is serving the whole thing.

Before we add the rewrite to Grunt we need to install the connect-modrewrite npm module.

Here’s the rewrite rule we add. There are a couple cases where a redirect would not make sense: a) when we’re dealing with an API call (which is necessarily a Rails thing and doesn’t involve HTML5 pushState) and b) when we’re serving an actual file. So our rewrite rule says “redirect anything that’s not an API call or a file.”

We add these couple lines to Gruntfile.js. If it’s not entirely clear where these lines go, don’t worry. I included my full Gruntfile.js below so you can see exactly where they go.

Here’s my entire Gruntfile.js:

 

Production Page Reloads

We do basically the same thing in production. We just achieve it in a different way. First, add the rack-rewrite gem to your Gemfile.

Then bundle install, of course.

Then you’ll add a redirect rule to your config.ru that looks like this:

Now you’re in business. Enjoy!

About the author

Jason Swett

12 Comments

Click here to post a comment

  • I am not using grunt but I used rack-rewrite

    now localhost:3000 is giving error => No route matches [GET] “/index.html”

    Any thing I am missing ?

  • Optimal solution and congratulations for the tutorial. I’ll just leave some comments for newcomers assimilate better.

    Note:
    – No need to Rails.
    – After making the Gruntfile.js configuration already reload without error.
    – Copy the lines 112 to 136 of Gruntfile.js posted on the site.
    Delete all *return* in livereload your Gruntfile.js and glue in place .
    – When trying to run the server gives this error: Cannot find module ‘grunt-connect-proxy/lib/utils’ , install in your application directory: npm install grunt-connect-proxy –save-dev
    – If you have not achieved , review and persevere because this tutorial works.

  • I can’t even begin to explain how helpful this has been. I feel like mastering the bolts and nuts of Grunt is exhaustive, and you Sir saved my ass! Thank you for posting this.

  • Nice tutorial. But I’m at almost 24 hours trying to solving this problema, i tried everything I found at Google, and nothing worked, including this tutorial. Anyone continuing with this problema or found another solution?
    Thanks!

  • Thanks for the tips! Sometimes it’s great just to know I’m not the only one dealing with things. This was a great write-up and makes the problem straightforward to understand and deal with. I haven’t looked over your website to understand why you’re not using the Rails web server in development (you said you are using Grunt, I assume you mean Express?). I used your Rack::Rewrite idea for my WEBrick development server, and I’ll have to use Nginx URL rewriting in production.

  • I’ve got this just about all working. However, when I get an error when I try to introduce:

    app.factory(‘Group’, [‘railsResourceFactory’, function (railsResourceFactory) {
    return railsResourceFactory({ url: ‘/api/groups’, name: ‘group’ });
    }]);

    The error is “Error: [$injector:unpr] Unknown provider: railsResourceFactoryProvider <- railsResourceFactory <- Group"

    I believe a change is required on the controller definition in groups.js, however I am not familiar enough with angular to get it right.

    Thanks to anyone who can suggest a solution.

  • I think this tutorial is missing key information about the angularjs-rails-resource plugin, and how to configure it to allow for this behavior. If that’s what is even missing from this.

    For example, how do you include ‘railsResourceFactory’ in your module? Must it be defined as a custom provider? How? What is the dependency ‘rails’ in the Module definition?

    Also, the reload problem existed for me, despite being unable to complete this tutorial. That part of the tutorial helped me.

Free Guide: Getting Started with Angular and Rails

Learn More