Wednesday, December 5, 2012

How to pre-cache templates from Rails asset pipeline and use them with Angular

Abstract

By default, Angular makes a request to the server in order to process this pair of directives: ng-view and ng-include. This post is about using templates from the asset pipeline of Ruby on Rails to avoid unnecessary server requests.


Introduction

In human history, Rails asset pipeline is almost as important as manipulating fire or inventing the wheel. With a few lines of code you can embed string templates into your JavaScript source file. Sprockets is responsible for writing the code that shall create a global object named JST in the client's browser, which will hold all your templates.

By default, Sprockets supports EJS and ECO templates, but there's other options out there. Although I prefer Jade, I'm using Haml for two reasons: the first is the excellent gem Haml Coffee Assets; and the second is the better syntax highlighting support to Haml from most text editors.

Uncomment your favorite template engine and let's get going.

RAILS_ROOT/Gemfile

group :assets do # Rails 4 doesn't make use of the assets group.
  # gem 'ejs'
  # gem 'eco'
  # gem 'haml_coffee_assets'

  # Most likely, you already have this gem in your gemfile.lock,
  # but it doesn't hurt to be too cautious. Haml Coffee Assets requires it.
  # gem 'execjs' 
end
* You don't need to declare dependency of Haml in any environment while using Haml Coffee Assets. I usually write the layout file for web apps in ERB.

Server side

Presuming you have chosen to put your templates at: "RAILS_ROOT/app/assets/javascripts/templates", don't forget to require them.

RAILS_ROOT/app/assets/javascripts/application.js

//= require hamlcoffee // If you're using Haml Coffee Assets.
//= require_tree templates

The extension of your template files depends on your choice of engine. For EJS you must use ".jst.ejs", similarly, for ECO, use ".jst.eco". For Haml, you may use either ".hamlc" or ".jst.hamlc", although I can't find any advantage in using ".jst.hamlc". You should read the official documentation about that, but if you want to save some time, I firmly recommend using the simpler extension ".hamlc".

* Avoiding headaches *

While using Haml Coffee Assets, it may come a time when you do some configuration, like suppressing "templates" from file's path, and you don't see any changes when you hit reload in your browser. Rebooting the server also won't work.

The thing is that your assets won't be recompiled unless you change something in them. Just add an extra line in some file and you're ready to go.

Client side

Angular is so good that it accomplishes something unimaginable until its appearance: making client side programming something enjoyable (I must have stolen that from someone talking about some other framework).

I fail to understand why frameworks like Ember and Knockout are designed in a way to expect you to do something like this.

/index.html

<html>
  <body>
    <script type="text/handlebars" id="some-template">
      <h1>Do not do this!</h1>
    </script>

    <script type="text/html" id="another-template">
      <h1>There is a better a way!</h1>
    </script>
  </body>
</html>

Ember, Knockout and friends, when it comes to insert a partial, expects you to provide a string that must match the id property of some script tag. There are ways to overcome this problem, but my point is: if you're willing to do that(!!!), would it kill you to write "$('#some-template').text()" in place of "some-template"?

If these frameworks were designed to expect a string containing the whole template, you wouldn't need to spend some hours figuring out how to do it. Rails developers could easily write "JST['some-template']()". I may sound angry about it, but I'm not (or at least a little). Knockout's core member Ryan was really cool helping me out. But, in the end, I switched to Angular and never have looked back.

Enough chattering!

To make good use of your super awesome compiled templates, you just need some few lines of code.

RAILS_ROOT/app/assets/javascripts/../app.js (for example)

angular.module('MyApp', [])
// As soon as possible.
.run(['$window', '$templateCache', function($window, $templateCache) {
  var templates = $window.JST,
      fileName,
      fileContent;

  for (fileName in templates) {
    fileContent = templates[fileName];
    $templateCache.put(fileName, fileContent);
    // Note that we're passing the function fileContent, and not the object
    // returned by its invocation. More on that on Digging Deeper.
  }
]);

Or if you prefer CoffeeScript, like me:

angular.module('MyApp', [])
# As soon as possible.
.run([
  "$window", "$templateCache",
  ($window,   $templateCache) ->
    templates = $window.JST

    for fileName, fileContent of templates
      $templateCache.put(fileName, fileContent)
    # Note that we're passing the function fileContent, and not the object
    # returned by its invocation. More on that on Digging Deeper.
])

Do notice that we are using the run method. With this method, $templateCache should be available for use. It won't be available, however, for the config method.

That's it! The users of your great app won't have to wait for a page when accessing it for the first time.

Digging deeper

You can embed code in your templates in order to produce dynamic content once, while bootstraping, or each time a page is rendered. You may prefer this over Angular directives for some reasons, like using an internationalization tool instead of hardcoding the text into your templates.

Suppose you have a page like this:

RAILS_ROOT/app/assets/javascripts/templates/institutionals/home.hamlc
%h1
  = new Date;

Here's the catch mentioned above. By passing a function - fileContent - as an argument to the method put of the object $templateCache, Angular will invoke that function every time its template is required, giving you the current time on each one. I believe this is the behavior expected by most of the people.

If you choose to pass the object returned by the function invocation - fileContent() -, Angular will cache just a string, and you'll only get the time at bootstraping.

Thank's for reading!

Haml Coffee Assets
EJS
ECO

11 comments:

  1. Thanks for writing this! It helped me a lot.

    In case it helps anyone - for Coffeescript, I had to do this to make it work: http://pastebin.com/AB6mP74d

    ReplyDelete
    Replies
    1. Thanks, I edited the original text in order to also present the CoffeeScript snippet.

      Delete
  2. Thanks for this post! Also thanks to zek for the coffeescript, worked like a charm.

    ReplyDelete
  3. Thought I'd mention since I am new to angular and it took me a bit to figure out... You can use the templates by injecting the $templateCache into your directive or whatever and then use: template: $templateCache.get('my_cool_template')

    ReplyDelete
  4. You are the man, thank you very much.

    ReplyDelete
  5. Hi, forced a problem. Getting Error: Unknown provider: $templateCache from angularDevise
    Can you explain why?
    throw Error("Unknown provider: " + path.join(' <- '));
    App.config(['$routeProvider', '$templateCache', function($routeProvider, $templateCache) {
    $routeProvider.when('/home', {template: $templateCache.get('home.hamlc'), controller: 'MainController'});
    }]);

    ReplyDelete
    Replies
    1. This is happening because config is executed before the run method. I haven't found a solution to this :(

      Delete
    2. Well, you already gave the solution. $templateCache is available when of the time of running, and not configuring.
      I edited the original text to emphasise that. Just switch from config to run and you will be good.

      Delete
  6. it is what I was looking for, thanks a lot (it worked, I just needed to adapt it to coffeescript as Zek posted).

    ReplyDelete