One of my current projects is building and maintaining a style guide that, in addition to making design recommendations, provides reusable components and template helpers that can be consumed by a variety of other applications. The project started out as a Rails gem to be consumed by other Rails apps but then requests started coming in to support Backbone, Angular and possibly even Node. The other frameworks could include the styleguide as a git submodule or using any given package management system but they wouldn't all be able to use the views with Ruby code in them. Our simple Rails template helpers just got a lot more complicated.
Modularize, isolate and extract every component
It seems that we ran into a coupling problem. The first thing I needed to do to support all of these different systems was to remove everything that was dependent on Rails. This meant removing all of the logic and keeping the templates on their own. The Rails helpers then consume these templates using render partial: 'my_partial'
. With the markup now extracted from the Ruby logic, other projects can consume the templates.
Logic goes in controllers, not views
We decided to go with the Mustache templating system for its ease of use and compatibility with almost every web system and framework out there. Keeping logic out of the templates makes them significantly easier to share across languages. Alas, going with logicless templates meant giving up things like the ability to show or hide certain features of a component depending on the application and this was problematic. Getting this ability back was pretty crucial to the success of the styleguide. One way I devised to get around this problem is to allow template helpers to insert classnames in addition to data. So with a template like:
<div class="modal {{classList}}">
Some text...
Maybe a button...
<div class="second-cta">
<button>We also have this other product!</button>
</div>
</div>
We can then show or hide the subcomponent with CSS(SCSS):
.modal {
.second-cta { display: none; }
&.show-button .second-cta { display: block; }
}
This is great! We now have templates that don't depend on one particular framework, are easily consumed by any larger framework and we can even show and hide subcomponents. But wouldn't it be better if we didn't have to play tricks and could just include arbitrary content like before?
Nesting views in logicless templates
The solution I devised to allow arbitrary content to be inserted into the templates is to use nested views. In a logicless templating system, the only way to do this is to render the nested content to a string and pass that string value as an argument to the template compiler. So now our template looks a bit like this:
<div class="modal">
Some text...
Maybe a button...
{{bottom_content}}
</div>
And in Rails the helper works as follows:
<%= render partial: 'my_modal', locals: {
bottom_content: '<div class="second-cta"><button>We also have this other product!</button></div>'
} %>
This allows us to insert content in pre-approved parts of a template in a manner similar to how javascript's callbacks allow you to insert arbitrary functionality into the middle of another function.
Consuming views on the front end
With our new system, the client-side applications can consume the styleguide view templates in the same way the Rails applications can. Extracting those templates in something like Angular is as easy as:
angular.module('app')
.run(function($templateCache) {
$http
.get('/styleguide/my_modal.html')
.success(function(template) {
var modal = Mustache.render(template);
$templateCache.put('myModal.html', template);
});
})
.directive('myModal', function() {
return {
restrict: 'E',
templateUrl: 'myModal.html'
};
});
Final thoughts
Making this template system work across frameworks was an interesting exercise but the styleguide is a very ambitious project and we're just getting started. There's all sorts of interesting problems with getting the templates to load quickly on the client side. I'm considering maintaining(autogenerating) a template manifest that could then be used by Angular to lazyload all of the templates in the background while a user is browsing the site.
In the future, it would also be fun doing something crazy like setting up a "Styleguide API" that would take requests for components and spit out all the HTML, CSS and JavaScript you might need. Polymer components as a service? The sky is the limit when you're trying to optimize applications!
A fun bonus of choosing mustache for the templating language(or any other similar templating system) is that its markup is the same as the markup Angular uses for its templates(as long as you don't engage the block/section helpers) so you don't need to include Mustache.js on the page or pre-parse template strings at all. The example above would really only be necessary if you wanted to use advanced mustache.js features like those blocks.