Single Page Apps – Deep Dive into Loading Templates Using Sammy, Mustache, RequireJS

Sammy.jsIn this tutorial you will learn how Sammy renders a Mustache template and then load and interpolate the template. In addition, you will use Sammy and templates as Asynchronous Module Definition (AMD) modules.

The tutorial builds on the previous postings Getting Started with SammyJS – Routes, where you learned you can use Sammy to provide client side routing, and Loading JSON Using Sammy where you learned how to load JSON data using sammy.load().

This tutorial goes beyond the getting started with Sammy tutorial, JSON Store, provided in Sammy’s documentation. In this tutorial you will learn what happens behind the scenes with each of the important calls. The idea is to help you choose the right Sammy calls as your application gets more complex.

Getting Started

You will need jQuery, Mustache, Sammy, Sammy.Mustache from the Sammy Plugins. The links take you to where get the libraries. Also jQuery, Mustache, and Sammy.js are available on NuGet.

Place the libraries in a folder named Scripts.

Sample Data

Here is some sample data that goes in a file named Products.txt in a Data folder.

Data/Products.txt


{
"products": [
{
"id": 1,
"name": "Toy1",
"category": "Toy",
"price": 45.05,
"description": "<p>This is <strong>strong</strong> description Toy1</p>"
},
{
"id": 2,
"name": "Toy2",
"category": "Toy",
"price": 35.20,
"description": "<p>This is an <em>emphasis</em> description of Toy2</p>"
}
]
}

Sample Templates

Let’s start with the template. Create the file named productsList.html in Templates folder.

Templates/productList.html

https://gist.github.com/devdays/418696e90decdb0ad701

and another for a single product productDetail.html in Templates folder.

Templates/productDetail.html


<!– product {{id}} –>
<h3>{{name}}</h3>
<div>Category: {{category}}</div>
<div>Price: ${{price}}</div>
<div>Description: {{&description}}</div>
<div>Description: {{{description}}}</div>

Note that for the demo we are using Mustache triple brackets to render unescaped HTML, use the triple mustache: {{{name}}}. You can also use & to unescape a variable.

The template will display as:

image
image

Getting Started HTML

Begin with the following starting point for our single page app. It uses needs Mustache and Sammy.

In this tutorial, you are using sammy.app.get(), which is an alias for route(‘get’, … For more information on routes, see Sammy Routes.

Display Products

Replace this.get(‘#/products’, route with the following code:


this.get('#/products', function (context) {
var products = context.items;
context.app.swap(''); // clear out the $element in this case '#content'
context.render('templates/productsList.html', products)
.appendTo(context.$element());
});

The products data was loaded with sammy’s around() method that you learned about in a previous post. The data has been cached, so the data is retrieved once. In our implementation of loading the data, our JSON data has been put into the context.items property for use in the routes.

sammy.app.use

sammy.use() tells the application the entry point for Sammy plugins. The first argument to use should be a function() that is evaluated in the context of the current application.

For example:

this.use('Mustache', 'html');

If plugin name is passed as a string Sammy assumes you are trying to load the function names Sammy.Mustache.

In sammy.mustache.js, the plugin code makes the Sammy.Mustache method public and available to the application when it is time to use a template.

The second parameter tells Sammy to accept the .html extension as the one to use with the Mustache template.

$element, sammy.app.swap

Note the sammy.app.swap() method takes the text of the first parameter and makes it the new html in the $element. In this example, you clear the content of $element.

context.app.swap(''); 

$element represents the HTML element that we assigned when we created the sammy app. In this case you used the jQuery selector #content when  you called $.Sammy('#content'.

You can also set a new $element by assigning a new selector to sammy.app.element_selector.

Overriding Swap

Optionally you could provide your own swap.

For example, you could provide a function to fade out and fade in new content.


// implements a 'fade out'/'fade in'
this.swap = function(content, callback) {
var context = this;
context.$element().fadeOut('slow', function() {
context.$element().html(content);
context.$element().fadeIn('slow', function() {
if (callback) {
callback.apply();
}
});
});
};

context.render

Sammy assumes you will want to render data using a template in just a few lines of code. It will automatically loads the template from a location that you specify, apply the data, call the template engine (in our case, the Mustache plugin) and returns you the HTML result.

The solution provides for great flexibility, if you understand the default behavior.

sammy.render() is a shortcut that you can use to can be called with multiple different signatures:

  • this.render(callback);
  • this.render(‘/location’);
  • this.render(‘/location’, {some: data});
  • this.render(‘/location’, callback);
  • this.render(‘/location’, {some: data}, callback);
  • this.render(‘/location’, {some: data}, {my: partials});
  • this.render(‘/location’, callback, {my: partials});
  • this.render(‘/location’, {some: data}, callback, {my: partials});

It takes the parameters and then, in order:

  1. .loadPartials(partials)
  2. .load(location)
  3. .interpolate(data, location)
  4. .then(callback);

If you need finer control, you can use the sammy functions yourself to replace render. Here’s a summary of what each function does.

Note: as an alternative to calling swap and then calling render to put the content into $element, you could also call partial() which combines render and swap into the $element as a single method.

context.loadPartials()

This takes the path to template that can be called from within the template. An example is shown in the Sammy documentation here.

context.load()

sammy.load() is most often used to load templates into the context. Load has three parameters:

  • The name of the remote file, jQuery object, or DOM element
  • The options. You can  specify that you are retrieving JSON or you can also specify to not cache the data loaded.
  • The callback that is executed after the template load.

If a jQuery or DOM object is passed the innerHTML of the node is used. This is useful for nesting templates as part of the initial page load wrapped in invisible elements or <script> templates.

You can specify options.

 { json: true, cache: false }

JSON data is automatically cached in the templateCache unless you specify cache as false.  The default is to cache the incoming data. The data is stored in the sammy.app.templateCache().

The callback is the function you execute after the load. Use .then to get the data returned from the load.

context.interpolate()

context.interpolate() takes the template, looks up the templating engine that you want, applies the data to the template using that engine, and returns the HTML.

context.then()

context.then() puts functions into a stack that are called once a long-running operation is completed. So when you use Sammy’s load function, you use then to respond once the load has completed.

Note that sammy is not really executing a promise. In version 0.7.4 there is not a fail method.

If then() is passed a string instead of a function, Sammy will looked for a helper method on the event context.

context.appendTo(), prependTo(), replace()

context.appendTo() inserts the set of matched elements to the end of the target.in a way that insures the right order. It uses jQuery.appendTo.

In our sample, it takes the result from render and then appends it to the context.$element.

prependTo() inserts every element in the set of matched elements to the beginning of the target. It uses jQuery.prependTo.

replace() to replace the content of the selector using jQuery.html().

Display Product

Replace this.get(‘#/product/:id’, route with the following code:


this.get('#/products/:id', function (context) {
var param = context.params['id'];
var products = context.items.products;
if (!products[param]) {
return context.notFound();
}
context.partial('Templates/productDetail.html', products[param]);
});

Note for the use in this tutorial, you are using the array offset, and not the product id value to find the product. (See the next tutorial on how to find a product using your own matching function using LoDash.)

You can retrieve the products from the context.items property that you defined in your implementation of this.around().

context.params[]

Returns the value of the URL parameters.

context.partial()

context.partial() first calls sammy.context.render(), which is explained in a previous section, and then calls swap(), also explained in a previous section.

It assumes you want to replace the content of $element.

HTML Tutorial Example


<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<title></title>
</head>
<body>
<nav>
<ul>
<li><a href="#/">Home</a></li>
<li><a href="#/products">Products</a></li>
<li><a href="#/products/1">Product 1</a></li>
<li><a href="#/data">Data</a></li>
</ul>
</nav>
<div id='content'></div>
<script src="Scripts/jquery-1.9.1.js"></script>
<script src="Scripts/mustache.js"></script>
<script src="Scripts/sammy-0.7.4.js"></script>
<script src="Scripts/sammy.mustache.js"></script>
<script>
// ====== set up require.js here in a later step ======
(function () {
"use strict";
// this line changes to
// var app = sammy('#content', function () {
// without the reference to jQuery when we use requireJS
var app = $.sammy('#content', function () {
// first param is a function name
this.use('Mustache', 'html');
// the callback is the entire route wrapped in a closure
this.around(function (callback) {
var context = this;
this.load('data/products.txt', { json: true })
.then(function (items) {
context.items = items;
})
.then(callback);
});
this.get('#/', function (context) {
context.app.swap(''); // clear the content area before loading the partials
context.$element().append('<h1>Main page</h1>');
});
this.get('#/data', function (context) {
context.app.swap(''); // clear the content area before loading the partials
context.$element().append(JSON.stringify(context.items));
});
this.get('#/products/:id', function (context) {
// render product here
});
this.get('#/products', function (context) {
// render products here
});
});
$(function () {
app.run('#/');
});
})();
</script>
</body>
</html>

AMD Using RequireJS With Sammy

You may want to use Sammy use as a module Asynchronous Module Definition (AMD) for JavaScript modules.

You will need to make just a couple changes.

First, you will need require.js which you can download RequireJS on its Website or get RequireJS on NuGet. And you will need to load require. You will use RequireJS to load jQuery, Sammy, Mustache, and the Sammy.Mustache plugin. The Sammy.Mustache plugin is already a AMD module that requires Mustache.

Also, when you start Sammy, you no longer need $.sammy. Use sammy() instead.


<!DOCTYPE html>
<html>
<head>
<title>Sammy+Mustache+RequireJS</title>
</head>
<body>
<nav>
<ul>
<li><a href="#/">Home</a></li>
<li><a href="#/products">Products</a></li>
<li><a href="#/products/1">Product 1</a></li>
<li><a href="#/data">Data</a></li>
</ul>
</nav>
<div id='content'></div>
<script src="Scripts/require.js"></script>
<script>
// ====== set up require.js ================
(function () {
"use strict";
require.config({
baseUrl: 'Scripts',
paths: {
//"underscore": "lodash",
"jquery": "jquery-1.9.1",
//"q": "q",
"sammy": "sammy-0.7.4",
"mustache": "mustache",
"mustachePlugin" : "sammy.mustache"
},
shim: {
// we get an error that "jQuery is not defined" error without this
// shim for sammy
"sammy": {
deps: ["jquery"],
exports: "sammy"
}
}
});
})();
require(['sammy', 'mustachePlugin'], function (sammy) {
"use strict";
var app = sammy('#content', function () {
// first param is a function name
this.use('Mustache', 'html');
// the callback is the entire route wrapped in a closure
this.around(function (callback) {
var context = this;
this.load('data/3-products.txt', { json: true })
.then(function (items) {
context.items = items;
})
.then(callback);
});
this.get('#/', function (context) {
context.app.swap('');
context.$element().append('<h1>Main page</h1>');
});
this.get('#/data', function (context) {
context.app.swap('');
context.$element().append(JSON.stringify(context.items));
});
this.get('#/products/:id', function (context) {
var param = context.params['id'];
var products = context.items.products;
if (!products[param]) {
return context.notFound();
}
// partial() internally calls render and swap
// creates the html and puts it into $element
context.partial('Templates/productDetail.html', products[param]);
});
this.get('#/products', function (context) {
var products = context.items;
context.app.swap('');
context.render('templates/productsList.html', products)
.appendTo(context.$element());
});
});
$(function () {
app.run('#/');
});
})();
</script>
</body>
</html>

Sample Code

Sample code for this post is available in the DevDays GitHub repository at https://github.com/devdays/single-page-app/tree/master/Sammy

You can find the example in 6a-RequireSammyTemplates.html.

References

Sammy API Reference

Sammy Source Code