#295 Sharing Mustache Templates pro
- Download:
- source codeProject Files in Zip (93.7 KB)
- mp4Full Size H.264 Video (48.4 MB)
- m4vSmaller H.264 Video (21.2 MB)
- webmFull Size VP8 Video (23.3 MB)
- ogvFull Size Theora Video (55.6 MB)
Mustache is a wonderfully simple templating language that is supported by a number of programming languages, including Ruby and JavaScript. If you need to share a template across multiple languages then Mustache is a great solution. The demo site has an example template that we can play around with. Here’s what it looks like:
<h1>{{header}}</h1> {{#bug}} {{/bug}} {{#items}} {{#first}} <li><strong>{{name}}</strong></li> {{/first}} {{#link}} <li><a href="{{url}}">{{name}}</a></li> {{/link}} {{/items}} {{#empty}} <p>The list is empty.</p> {{/empty}}
Mustache templates use double curly braces to define attributes. We can use curly braces and a hash symbol to define blocks which we can use to iterate through multiple items or use as if conditions. If we pass the template above this JSON data
{ "header": "Colors", "items": [ {"name": "red", "first": true, "url": "#Red"}, {"name": "green", "link": true, "url": "#Green"}, {"name": "blue", "link": true, "url": "#Blue"} ], "empty": false }
it will generate the following output.
<h1>Colors</h1> <li><strong>red</strong></li> <li><a href="#Green">green</a></li> <li><a href="#Blue">blue</a></li>
Let’s see what’s involved in adding Mustache to a Rails application.
Using Mustache to Add Items to a list With JavaScript
The Rails application we’ll use has a page that shows a list of products but as it stands it only displays the first ten products even though there are more in the database. We’ll it so that as a user scrolls down towards the bottom of the page more products are loaded giving us an endless scrolling effect.
The template for this page is simple. It has a div
with a id
of products and inside this we loop through the products and render each one.
<h1>Products</h1> <div id="products"> <% @products.each do |product| %> <div class="product"> <h2><%= link_to product.name, product %></h2> <div class="details"> <%= number_to_currency(product.price) %> <% if product.released_at %> | Released <%= product.released_at.strftime("%B %e, %Y") %> <% end %> </div> </div> <% end %> </div>
We need to detect when a user has scrolled down near to the bottom of the page and then add more products inside the div
. We’ll write the code to do this inside the products
CoffeeScript file. Here’s our first attempt at this code.
jQuery -> if $('#products').length new ProductsPager() class ProductsPager constructor: -> $(window).scroll(@check) check: => if @nearBottom() $(window).unbind('scroll', @check) alert 'near bottom' nearBottom: => $(window).scrollTop() > $(document).height() - $(window).height() - 50
In this code we first check that the DOM has loaded and that the products
div exists on the page. The logic to handle scrolling and loading the products will get fairly complex by the time we’ve finished it so we define it in a separate ProductsPager
class. In this class’s constructor we add an event handler that fires a check
function each time the page is scrolled.
When we define check we use the ‘fat’ arrow (=>
) instead of the skinny arrow (->
). This ensures that the context always remains the same, i.e. that this always refers to the ProductsPager
instance and not whatever the scroll
event has bound it to.
Inside the check
function we check to see if the user has scrolled near the bottom of the page. If they have then we unbind the function from the scroll
event to stop it firing repeatedly. It’s here that we need to get the next page of products and add them to the page but for now we’ll just show an alert
. We check that the user has scrolled near the bottom of the page in a nearBottom
function that checks to see if the window’s scrollbar is within 50 pixels of the bottom of the page.
jQuery -> if $('#products').length new ProductsPager() class ProductsPager constructor: -> $(window).scroll(@check) check: => if @nearBottom() $(window).unbind('scroll', @check) alert 'near bottom' nearBottom: => $(window).scrollTop() > $(document).height() - $(window).height() - 50
When we reload the page now and scroll down towards the bottom we get the alert, but after we dismiss it we won’t see it again as the event has been unbound.
Fetching More Products From The Server
Now that we know that our code works we can replace the alert
with some code that will fetch more products from our Rails application and display them on the page. Normally we’d use jQuery’s getScript
function, make a JavaScript template in the Rails app and handle everything there. Sometimes, though, we want to work with JSON data from the server and handle everything on the client. JSON works well with Mustache templates so that’s what we’ll be using here.
We’ll need a URL to fetch the JSON data from and rather than hard-code this in the CoffeeScript file we’ll add it to a data attribute in the products div
.
<div id="products" data-json-url="<%= products_url(:format => :json) %>">
Now in our CoffeeScript code we’ll replace the alert with a call to getJSON
to get the products JSON from that URL. The getJSON
function takes a callback function that fires when the data is returned and we’ll pass in a new function called render
. For now this function will just alert the data that’s returned.
jQuery -> if $('#products').length new ProductsPager() class ProductsPager constructor: -> $(window).scroll(@check) check: => if @nearBottom() $(window).unbind('scroll', @check) $.getJSON($('#products').data('json-url'), @render) nearBottom: => $(window).scrollTop() > $(document).height() - $(window).height() - 50 render: (products) => alert products
This JSON request will trigger the ProductController
’s index
action. We’ll have to modify this action’s code so that it handles JSON requests and returns the array of products as JSON.
def index @products = Product.order("name").limit(10) respond_to do |format| format.html format.json { render json: @products } end end
When we reload the page now and scroll down to the bottom we’ll see an alert
showing the list of products.
Rendering The Products With Mustache.js
We’re getting a list of products back from the server now but we still need to render them on the page and this where Mustache comes in. We’ll need a template and we’ll create one in the index
view based on the code that’s already in there to render each product.
Mustache is a simple language and doesn’t give us helper methods like link_to
so we’ll have to replace these with standard links. We don’t want the template to be shown on the page so we’ll wrap it in a script
tag and give that an id
so that we can reference it from JavaScript.
<script type="text/html" id="product_template"> <div class="product"> <h2><a href="/products/{{id}}">{{name}}</a></h2> <div class="details"> {{price}} {{#released_at}} | Released {{released_at}} {{/released_at}} </div> </div> </script>
We can use the mustache.js project to render a Mustache template in JavaScript. This project contains a JavaScript file that we can download and use in our Rails app. As this is an external JavaScript file we’ll download it with curl
and place it in a new vendor/assets/javascripts
directory.
$ mkdir -p vendor/assets/javascripts noonoo:store eifion$ curl https://raw.github.com/janl/mustache.js/master/mustache.js > vendor/assets/javascripts/mustache.js
Finally we’ll need to modify the application.js
file to include this file.
//= require jquery //= require jquery_ujs //= require mustache //= require_tree .
We now have almost everything in place to render out the products. We just need to replace the alert
in the render function with the code to do this which will loop through the products, render each one and append it to the products
div. To render a product we call Mustache.to_html
, passing it the our template and the product. Once we’ve rendered each product we’ll re-enable the scroll event so that the next time we scroll down to the bottom more products are added to the list.
render: (products) => for product in products $('#products').append Mustache.to_html($('#product_template').html(), product) $(window).scroll(@check)
When we reload the page now and scroll down new products are added each time we scroll to the bottom of the page. There is a problem, though. The same products are loaded each time we scroll down. We’ll fix this next.
Fetching The Next Page of Products
So that we know which page of products to fetch next we’ll keep track of the current page in our products CoffeeScript file and send it in the Rails request that fetches the products JSON. We’ll create a page
instance variable in the ProductsPager
’s constructor, increment it each time we get near to the bottom of the page and send its current value in the JSON request.
class ProductsPager constructor: (@page = 1) -> $(window).scroll(@check) check: => if @nearBottom() @page++ $(window).unbind('scroll', @check) $.getJSON($('#products').data('json-url'), page: @page, @render)
Once we’ve rendered the last page of products we should stop checking for the scroll
event so we’ll add a check to see if any products have been returned in the callback function and only re-enable the event if so.
render: (products) => for product in products $('#products').append Mustache.to_html($('#product_template').html(), product) $(window).scroll(@check) if products.length > 0
We can now use this page parameter in the index
action to change to define an offset so that the correct page of products is returned.
def index @products = Product.order("name").limit(10) @products = @products.offset((params[:page].to_i - 1) * 10) if params[:page].present? respond_to do |format| format.html format.json { render json: @products } end end
Now when we reload the page and keep scrolling to the bottom we see each product just once.
Removing Duplication
The code that’s rendered through the Mustache template needs some work on the formatting, but before we do that we’ll remove some of the duplication between the way the products are rendered in Rails and in JavaScript. The index
action currently has two very similar templates in it: the Mustache template and the Erb one. We can make our code cleaner by just using one so we’ll use the Mustache template for both. To do this we’ll need a way to render a Mustache template in Ruby and we can use the mustache gem for this. There are other gems available that build on this one to help integrate Mustache into Rails but here we’ll do it all manually to get a better idea as to how a template handler works.
The mustache gem is installed in the usual way by adding it to the Gemfile
and running bundle
.
source 'http://rubygems.org' gem 'rails', '3.1.1' gem 'sqlite3' # Gems used only for assets and not required # in production environments by default. group :assets do gem 'sass-rails', '~> 3.1.4' gem 'coffee-rails', '~> 3.1.1' gem 'uglifier', '>= 1.0.3' end gem 'jquery-rails' gem 'mustache'
Next we’ll move the mustache template out from our index
view into a partial. As our partial is a Mustache template it will need to have a .mustache
extension rather than the usual .erb
.
<div class="product"> <h2><a href="/products/{{id}}">{{name}}</a></h2> <div class="details"> {{price}} {{#released_at}} | Released {{released_at}} {{/released_at}} </div> </div>
We can call this partial in index
.
<script type="text/html" id="product_template"> <%= render 'product' %> </script>
If we reload our page now we’ll get a MissingTemplate
error as our application doesn’t have a handler for mustache templates. We’ll create one now in the /config/initializers directory
(although you could put it in the /lib
directory if you prefer). Template handlers have changed quite a bit in Rails 3.1. Previously we’d have to override a render
method and a compile
method, but in Rails 3.1 we only have to define call
.
module MustacheTemplateHandler def self.call(template) "#{template.source.inspect}.html_safe" end end ActionView::Template.register_template_handler(:mustache, MustacheTemplateHandler)
We used a module here, but we could have used a class, a proc or even a lambda as any of them respond to call
. The call
method is a little odd as it needs to return a string containing some Ruby code so if it returned "1 + 1"
then the template would render “2”
. Instead of that we call template.source
which is the content of the template that’s being rendered, in this case the Mustache template partial we created earlier. We call inspect
on this to return an object which will be the string-escaped version of it’s Ruby representation and then call html_safe
on the output from this to make sure that it’s escaped properly. Finally we register the new handler by calling register_template_handler
, passing in the name of the handler and our module.
Whenever we create or alter an initializer we need to restart the server, but once we do the page will load and work as it did before.
We can use our Mustache template now to render out the initial list of products in the index
view. We need to pass in each product
to the template so that it knows to render it out using the mustache
template. We’ll pass the product in as a local variable called mustache
and convert it to JSON to make sure that the data that’s passed in is consistent with the data that’s passed to the template from the JavaScript.
<h1>Products</h1> <div id="products" data-json-url="<%= products_url(:format => :json) %>"> <% @products.each do |product| %> <%= render 'product', :mustache => product.as_json %> <% end %> </div> <script type="text/html" id="product_template"> <%= render 'product' %> </script>
In our Mustache template handler we’ll check for that mustache
option so that we know that the request has come from the index
action directly rather than through the AJAX request. We do this by checking template.locals
which contains a list of the keys that we’re passed in. If it does then we need to render the template out using the Ruby Mustache
passing in the mustache
variable that contains the current product. Otherwise we render out the raw template for JavaScript to deal with on the client.
module MustacheTemplateHandler def self.call(template) if template.locals.include? :mustache "Mustache.render(#{template.source.inspect}, mustache).html_safe" else "#{template.source.inspect}.html_safe" end end end ActionView::Template.register_template_handler(:mustache, MustacheTemplateHandler)
When we restart the server and reload the page now all of the products are rendered using the Mustache template, not just those that are rendered by JavaScript.
Formatting The Output
Now that we’ve removed the duplication we can concentrate on getting the formatting right as the prices and release dates of the items don’t look the way we want them to. We’ll create a helper method called product_for_mustache
that will return a correctly formatted product that we can pass to the template.
module ProductsHelper def products_for_mustache(product) { url: product_url(product), name: product.name, price: number_to_currency(product.price), released_at: product.released_at.try(:strftime, "%B %e, %Y") } end end
This method is pretty straightforward but if you need to put some complex logic here then it might be worth moving this in to a presenter class, like we showed in episode 287. A simple helper method will work fine for us here, though.
In our index view we can now call this method so that the template is passed a correctly-formatted product.
<div id="products" data-json-url="<%= products_url(:format => :json) %>"> <% @products.each do |product| %> <%= render 'product', :mustache => products_for_mustache(product) %> <% end %> </div>
We’ll need to make a small change to the Mustache template. As our helper method returns the URL for each product we can replace the hard-coded URL in the template with that value.
<div class="product"> <h2><a href="{{url}}">{{name}}</a></h2> <div class="details"> {{price}} {{#released_at}} | Released {{released_at}} {{/released_at}} </div> </div>
There’s one final change we need to make. When we make a JSON request to the ProductsController
’s index
action it returns a list of products as JSON. We need to pass each one through our helper method so that it’s formatted correctly and we can do that by using map
to iterate through each product and pass it through our helper method. Note that as we’re calling a helper method from a controller we need to call it through view_context
.
def index @products = Product.order("name").limit(10) @products = @products.offset((params[:page].to_i - 1) * 10) if params[:page].present? respond_to do |format| format.html format.json do render json: @products.map { |p| view_context.products_for_mustache(p) } end end end
When we reload our products page now the products are formatted as we want them to be.
That’s it for this episode on sharing a Mustache template between Rails and JavaScript. We now have a page that renders items with same way whether they’re rendered through Rails for the initial ten products or through JavaScript when we scroll down to the bottom.
If you like Mustache it’s worth taking a look at Handlebars too. This extends Mustache and adds some more features to it so that you can do more complex things in its templates. It also adds a separate compile step for better performance.