Solving The Delayed AngularJS UI Rendering Server-Side With Rails

We’ve now been writing some awesome AngularJS code at BuildZoom for nearly a year. It’s helped us release great features faster and provided for a significant ease of maintenance over the previously used “classical” jQuery. One issue that became apparent was the delay in rendering the templates and data client-side. The extra round trips for various JS libraries and the initialization of JavaScript was notably slower than server-side rendering. Yet all the pros were too strong to simply revert the use of a client-side framework.

While using AngularJS, our core stack technology has remained Ruby on Rails. I’ve previously mentioned a variety of advantages such as the asset pipeline. Some have turned to Node.js to solve client-side rendering issue, by rendering the javascript server-side. I decided to take a more creative approach.

I started by rendering the data server-side in a Rails view:

<%=@contractor.name%>

Then, wrapped the data with a div (can be any HTML element) and created a simple custom directive, “var-setter”. The only input to the directive is the variable to which I want to set a server-side value:

<div var-setter variable="contractor.name"><%=@contractor.name%></div>

The directive utilizes a very interesting feature of AngularJS: transclusion (yes, that’s a real word).

angular.module("bz_app", [])
.directive('varSetter', function () {
return {
restrict: 'AE',
transclude: true,
scope: {
variable: '='
},
template: '{{ variable }}',
link: function (scope, element, attrs, ctrl, transclude) {
transclude(scope, function(clone) {
scope.variable = clone.html();
});
}
}
});

The template consists of only the variable, which is passed in to the directive’s isolated scope and via ‘=’ set up for two-way binding.

The link function is where the magic happens. In addition to the common first three input variables, there are two more: ctrl and transclude. Ctrl is not used in this case. It’s meant for giving directives access to controllers of other directives. Transclude is what we’re really after. The function provides access to a clone (copy) of the element that the directive has taken over. Thus, the “clone.html()” returns the value set server-side inside the div.

Setting “scope.variable = clone.html()” initializes data on the AngularJS side and populates the directive’s template. During the entire time that JS was being loaded and initialized, the user was presented with the data instead of a blank screen. AngularJS took over without a glitch or a flicker.

NOTE: I did not include the controller, which wraps the directive and has the “scope.contractor” object.