The Template component provides Y.Template
, a generic template engine API, and Y.Template.Micro
, a string-based micro-templating language similar to ERB and Underscore templates.
Getting Started
To include the source files for Template and its dependencies, first load the YUI seed file if you haven't already loaded it.
<script src="http://yui.yahooapis.com/3.8.0/build/yui/yui-min.js"></script>
Next, create a new YUI instance for your application and populate it with the
modules you need by specifying them as arguments to the YUI().use()
method.
YUI will automatically load any dependencies required by the modules you
specify.
<script> // Create a new YUI instance and populate it with the required modules. YUI().use('template', function (Y) { // Template is available and ready for use. Add implementation // code here. }); </script>
For more information on creating YUI instances and on the
use()
method, see the
documentation for the YUI Global Object.
Using Template
Quick Start
A template engine takes a template—usually in the form of a string—and some data, and renders the data into the template to produce an HTML or text string. Using templates to keep markup and structure separate from content encourages reuse and can make code easier to read and maintain, and in many cases faster.
Y.Template
provides a common API that can be used to compile and render templates with a variety of template engines. The two template engines included in YUI are Handlebars and Template.Micro.
The quickest way to get started is using the template
module which will load both the template-base
and template-micro
modules. The following example shows the most basic usage with the Y.Template.Micro
engine (the default template engine):
YUI().use('template', function (Y) { var micro = new Y.Template(), html = micro.render('<i><%= this.message %></i>', {message: 'hello!'}); Y.log(html); // => "<i>hello!</i>" });
In the above example, micro
is an instance of a template engine backed by Template.Micro. The Y.Template()
constructor provides an abstraction over the backing engine, giving the engine instances a uniform API.
Handlebars templates can be used instead of Micro templates by using the template-base
and handlebars
modules. The following example shows how to generate the same output as the above example with the Handlebars engine:
YUI().use('template-base', 'handlebars', function (Y) { var handlebars = new Y.Template(Y.Handlebars), html = handlebars.render('<i>{{message}}</i>', {message: 'hello!'}); Y.log(html); // => "<i>hello!</i>" });
Note: Both examples are using the engine's render()
method to compile and render the template dynamically on the client, doing this with Micro templates is fine, but it should be avoided with Handlebars templates. It is recommended that Handlebars templates be precompiled, enabling the client code to use the lighter and faster handlebars-base
module.
Generic Template API
Y.Template
exists to specifically to provide its API as a normalization layer on top of conceptually similar, but technically different template engines and syntaxes. This layer of abstraction allows components which work with templates to not be tied to a particular engine. Another huge benefit is allowing developers to override a component's default templates using an entirely different template engine.
The two templates engines provided in YUI, Handlebars and Template.Micro, are conceptually similar. They both compile string-based templates into functions, which are invoked with a data context and return the rendered output as a string. Handlebars is really well suited for organizing and managing the templates of an entire app or complex widget because of its partials and helpers features. Template.Micro is great for small templates, or when you need more powerful templates and its compilation engine is extremely small.
By making Template.Micro's public API very similar to Handlebars, we've made it possible to use the two template engines interchangeably via the Y.Template
normalization API. When you need to compile templates on the client, it is strongly recommend that you use Micro templates, because Template.Micro's compile is much smaller than Handlebars' compiler — 0.5KB vs 9KB (minified and gzipped) respectively.
Instantiating a Template Engine
While you can use a specific template engine directly, it is recommended that you create an instance of the generic Y.Template
engine wrapper. Doing so allows for greater flexibility and interoperability as described in the previous section.
To create a template engine instance, you must first determine which underlying engine you want to use. The two template engines included in YUI are Handlebars and Template.Micro. If you're looking to use a different engine, refer to Creating a Custom Template Engine section below.
Once you've determined the underlying template engine, you'll need to load the appropriate YUI module to fulfill how you plan to use templates. Refer to the following table of YUI modules to understand what each module provides:
Module | Compiler | Description |
---|---|---|
template-base |
No |
Provides a generic API for using template engines such as |
template-micro |
Yes |
Adds the |
template |
Yes |
Virtual rollup of the |
handlebars-base |
No |
Provides basic Handlebars template rendering functionality. Use this module when you only need to render pre-compiled templates. |
handlebars-compiler |
Yes |
Handlebars parser and compiler. Use this module when you need to compile Handlebars templates. |
handlebars |
Yes |
Virtual rollup of the |
Using Micro Templates
When working with Micro templates, it's easiest to use the template
virtual rollup module. The Y.Template.Micro
compiler is small enough that it is included with the runtime functionality.
The following example creates two template engine instances with are functionally equivalent and both backed by Template.Micro:
YUI().use('template', function (Y) { var microExplicit, microDefault; // Creates a template engine instance and explicitly specifies the // underlying engine. microExplicit = new Y.Template(Y.Template.Micro); // Creates another template engine instance with the same functionality, // but relies on `Y.Template.Micro` being defined as the underlying engine // by default. microDefault = new Y.Template(); });
Using Handlebars Templates
When working with Handlebars templates, you'll need to determine if the need the Handlebars compiler functionality provided by the handlebars-compiler
module. It is recommended that Handlebars templates be precompiled, enabling the client code to use the lighter and faster handlebars-base
module.
The following example loads only the Handlebars runtime and generic Y.Template()
wrapper API. It assumes that all templates have previously been precompiled on the server or during a build step:
YUI().use('template-base', 'handlebars-base', function (Y) { // Creates a limited template engine instance using Handlebars as the // underlaying engine, but with only the runtime functionality. var handlebars = new Y.Template(Y.Handlebars); });
Note: In the above example, the handlebars
engine does not have the ability to render()
, compile()
, or precompile()
template. It only has the ability to revive()
and execute precompiled templates.
The following example, uses the handlebars
virtual rollup module which includes the handlebars-compiler
. This enables the Handlebars-backed template engine instances to use the full API:
YUI().use('template-base', 'handlebars', function (Y) { // Creates a template engine instance using Handlebars as the underlaying // engine, with both the runtime and compiler functionality. var handlebars = new Y.Template(Y.Handlebars); });
Compiling and Rendering Templates
Both Handlebars and Micro templates must be compiled before they can be rendered. One benefit of this is that a template only needs to be compiled once, and it can then be rendered multiple times without being recompiled. Templates can even be precompiled on the server or at build time and then rendered on the client for optimal performance.
Before compiling a template string, a template engine needs to be created. Once the engine instance has been created, the template string can be passed to its compile()
method. What's returned is a reusable function.
var engine, template; // Create a Template.Micro engine instance. engine = new Y.Template(); // Compile a template into a reusable function. template = engine.compile('My favorite animal is a <%= this.animal %>.');
When you're ready to render the template, execute the function and pass in some data. You'll get back a rendered string.
// Render a previously compiled template. var output = template({animal: 'Rhino'}); Y.log(output); // => "My favorite animal is a Rhino."
You can re-render the template at any time just by calling the function again. You can even pass in completely different data.
// Re-render a previously compiled template. output = template({animal: 'Spotted Cuscus'}); Y.log(output); // => "My favorite animal is a Spotted Cuscus."
If you don't plan to use a template more than once, you can compile and render it in a single step with the template engine's render()
method.
// Compile and render a template in a single step. output = engine.render('My favorite animal is a <%= this.animal %>.', {animal: 'Rhino'}); Y.log(output); // => "My favorite animal is a Rhino."
Note: The above examples are using Micro templates. If these examples used Handlebars templates, the engine
instance would have been created using Y.Handlebars
, and template syntax would have used {{animal}}
instead of <%= this.animal %>
.
Precompiling and Reviving Templates
Since Micro and Handlebars templates can be compiled and rendered in separate steps, it's possible to precompile a template for use later. You can precompile a template into raw JavaScript on the server (or even on the command line in the case of Handlebars), serve this precompiled JavaScript template to the client, and then render it on the client using any data the client has at its disposal.
The main benefit of precompilation is performance. Not only does the client not need to go through the compile step, but if your using a template engine like Handlebars, you don't even have to load the compiler on the client! All the client needs in order to render a precompiled template is the engine's runtime. In the case of Handlebars this is a 9KB (minified and gzipped) savings.
Y.Template
engine instances have a precompile()
method which uses the underlaying engine
to convert the specified text
into a string of JavaScript source code. This string of code which represents the template, can later be revived using the engine instance's revive()
method which turns it into a JavaScript function.
The precompile()
method differs from the compile()
method in a couple of important ways:
-
The
precompile()
method returns a string of JavaScript code that's meant to be parsed and executed later, whereascompile()
returns a live JavaScript function. -
The code returned by the
precompile()
method contains no references to any outside objects. Once it's evaluated, the resulting precompiled function must be passed toY.Template
engine instance'srevive()
method, which will "rehydrate" it into an executable template function using the current template engine.
For more details, refer to the Precompiling and Reviving Templates sections of the Template.Micro and Handlebars user guides.
Creating a Custom Template Engine
The generic Y.Template
interface is designed to worked with for variety of string -> function template engines. To implement a custom underlaying template engine for Y.Template
, refer to the following list of methods and their descriptions which need to be implemented:
compile( text , [options] )
-
Compiles a string template into a reusable function and returns that function to the caller.
The core concept of a string -> function template engine is its compilation method. A custom template engine must implement this method.
render( text , data , [options] )
-
Compiles and renders a template in a single step, and returns the rendered result.
A custom template engine does not have to implement this method. It is merely provided as a convenience to the user. If the underlying engine does not implement this method, the
compile()
method will be called and the resulting function will be invoked. precompile( text , [options] )
-
Precompiles a string template into a new string containing JavaScript source code for the precompiled template and returns it to the caller. The
revive()
method this method's companion, it converts the precompiled template back into a renderable function.A custom template engine does not have to implement this method. If precompilation is a feature of the underlying template engine, then the
revive()
method must also be implemented. revive( precompiled , [options] )
-
Revives a precompiled template function into an executable template function and returns that function to the caller. The precompiled code must already have been evaluated; this method won't evaluate it for you.
This is a companion method to the
precompile()
method and it must be implemented if the underlying template engine supports precompilation.
Using Template.Micro
Y.Template.Micro
is a string-based micro-templating language similar to ERB and Underscore templates. Template.Micro is great for small, powerful templates, and its compilation engine is extremely fast with a small footprint.
Compared with the features of Handlebars, Template.Micro is much simpler. Using the generic engine API provided by Y.Template
, Micro and Handlebars templates can be used interchangeably. This gives you a powerful way to customize a component's Handlebars templates by overriding them with Micro templates, and not incur the cost of loading the handlebars-compiler
module.
Template Syntax
Basic Expressions
Within a Micro template, use <%= ... %>
to output the value of an expression (where ...
is the JavaScript expression or data variable to evaluate). The output will be HTML-escaped by default.
A simple Template.Micro expression looks like this:
<h1><%= this.title %></h1>
This tells Template.Micro:
-
if there exists a
title
property in the current context in which the template function was executed, and that property is not falsy or an empty array, insert its value here. -
Otherwise, insert an empty string.
The following example shows how the data context is defined when executing a Micro template function:
var micro = new Y.Template(), heading = micro.compile('<h1><%= this.title %></h1>'), output; // The object passed to the template function becomes the context in which the // template is executed. This object is also available through the `data` // variable within the template's expressions. output = heading({title: 'The Adventures of the Spotted Cuscus'}); Y.log(output); // => "<h1>The Adventures of the Spotted Cuscus</h1>"
Note: The template functions are call()
-ed with the context of the object which is passed to the template function. This object is also available through the data
variable. Therefore, data === this
, within the template expressions. The previous template could have been written as:
<h1><%= data.title %></h1>
HTML Escaping
By default, content rendered using a percent-equals expression like <%= foo %>
will automatically be HTML-escaped for safety. To render unescaped HTML output, use a percent-double-equals expression like <%== foo %>
. Only use a percent-double-equals expression for content you trust! Never use it to render unfiltered user input.
Inline Code & Code Blocks
To execute arbitrary JavaScript code within the template without rendering its output, use <% ... %>
(where ...
is the code to be executed). This allows the use of if/else blocks, loops, function calls, etc., although it's recommended that you avoid embedding anything beyond basic flow control logic in your templates.
Template Source | |
---|---|
<h1>Animals</h1> <ul class="<%= this.classNames.list %>"> <% Y.Array.each(this.animals, function (animal, i) { %> <li class="<% i % 2 ? 'odd' : 'even' %>"> <%= animal %> </li> <% }); %> </ul> |
|
Data | Output |
{ classNames: {list: 'animals'}, animals: [ 'Rhino', 'Plain Tiger butterfly', 'Spotted Cuscus' ] } |
<h1>Animals</h1> <ul class="animals"> <li class="even">Rhino</li> <li class="odd">Plain Tiger butterfly</li> <li class="even">Spotted Cuscus</li> </ul> |
Precompiling and Reviving Micro Templates
Precompiling Micro templates has advantages, especially when an app uses many templates. The rest of this section will demonstrate how to precompile templates on the server.
To precompile Micro templates on the server using Node.js, first install the YUI npm module by running the following in a terminal from the directory that contains your server application (this assumes you already have Node and npm installed):
$ npm install yui
This will install the yui
npm module in the current directory and make it available to your application.
Next, in your application code, call the precompile()
method to precompile a Micro template. It will return a string containing JavaScript code.
// Load the YUI Template.Micro module. var Micro = require('yui/template-micro').Template.Micro; // Precompile a template string (pass any string you like here). var precompiled = Micro.precompile('My favorite animal is a <%= this.animal %>.');
The precompiled
variable will contain a string of JavaScript code that looks something like this:
function (Y, $e, data) { var $b='',$t='My favorite animal is a '+ $e(( this.animal )||$b)+ '.'; return $t; }
You can now serve this precompiled JS to the client in whatever way makes the most sense for your application. On the client, load the template
YUI module, create a Y.Template
engine instance, and pass the precompiled template to its revive()
method to convert it into a renderable template function.
Here's a simple Express app that precompiles a template on the server and renders it on the client:
#!/usr/bin/env node var Micro = require('yui/template-micro').Template.Micro, express = require('express'), app = express(), precompiled = Micro.precompile('My favorite animal is a <%= this.animal %>.'); app.get('/', function (req, res) { res.send( '<html><body>' + '<script src="http://yui.yahooapis.com/3.8.0/build/yui/yui-min.js"></script>' + '<script>' + 'YUI().use("template", function (Y) {' + 'var micro = new Y.Template(),' + ' template = micro.revive(' + precompiled + ');' + 'Y.one("body").append(template({animal: "Plain Tiger butterfly"}));' + '});' + '</script>' + '</body></html>' ); }); app.listen(7000);
To see this simple server in action, save it to a file, install Express and YUI by running npm i express yui
, then execute the file with Node.js and browse to http://localhost:7000/.
Customizing Template Syntax
Micro templates have a simple syntax, there are only three forms:
<%= ... %>
-
Safely outputs the value of a JavaScript expression. The output will be HTML-escaped by default.
<%== ... %>
-
Outputs the raw value of a JavaScript expression. This does not HTML-escape the output. Never use it to render unfiltered user input.
<% ... %>
-
Executes arbitrary JavaScript code within the template without rendering its output.
These syntax identifiers are defined as RegExp on Y.Template.Micro.options
. If you wish to define a custom syntax for your Micro templates, you can do so by defining new RegExps for your custom identifiers.
Defining a Handlebars-like Syntax
The following example will define a Handlebars-like Micro template syntax:
{{ ... }}
-
Safely outputs the value of a JavaScript expression. The output will be HTML-escaped by default.
{{{ ... }}}
-
Outputs the raw value of a JavaScript expression. This does not HTML-escape the output. Never use it to render unfiltered user input.
{{% ... %}}
-
Executes arbitrary JavaScript code within the template without rendering its output.
// Create RegExps which define our new Micro template syntax. Y.mix(Y.Template.Micro.options, { code : /\{\{%([\s\S]+?)%\}\}/g, escapedOutput: /\{\{(?!%)([\s\S]+?)\}\}/g, rawOutput : /\{\{\{([\s\S]+?)\}\}/g }, true);
Template Source | |
---|---|
<h1>Animals</h1> <ul class="{{ this.classNames.list }}"> {{% Y.Array.each(this.animals, function (animal, i) { %}} <li class="{{ i % 2 ? 'odd' : 'even' }}"> {{ animal }} </li> {{% }); %}} </ul> |
|
Data | Output |
{ classNames: {list: 'animals'}, animals: [ 'Rhino', 'Plain Tiger butterfly', 'Spotted Cuscus' ] } |
<h1>Animals</h1> <ul class="animals"> <li class="even">Rhino</li> <li class="odd">Plain Tiger butterfly</li> <li class="even">Spotted Cuscus</li> </ul> |
Note: The syntax identifiers can be specified on a per-template basis by passing options
as the second argument to the compile()
, precompile()
, or render()
methods.
Using Templates in Custom Components
When created custom components for your app, it's natural to bundle the templates with the component that will use them. The following examples show how create a custom view component which use templates.
Custom View with Embeded Template
This example shows how to create a very basic view with an embedded template:
YUI().use('template-micro', 'view', function (Y) { Y.AnimalListView = Y.Base.create('animalListView', Y.View, [], { // The compiled Micro template sits on the view's prototype. template: Y.Template.Micro.compile( '<ul class="animals">' + '<% Y.Array.each(this.animals, function (animal, i) { %>' + '<li class="<% i % 2 ? "odd" : "even" %>">' + '<%= animal %>' + '</li>' + '<% }); %>' + '</ul>' ), render: function () { var html = this.template({ animals: this.get('animals') }); this.get('container').setHTML(html); return this; } }); // Create an instance of the view and render it to the `<body>`. var animalListView = new Y.AnimalListView({ animals: [ 'Rhino', 'Plain Tiger butterfly', 'Spotted Cuscus' ] }); animalListView.get('container').appendTo('body'); });
Custom View with External Template
Usually embedding the template in your component's JavaScript code is bad practice. The following examples show two way to externalize a component's templates.
Defining Templates in HTML
One option is to embed your template inside a special <script>
element, one whose type
attribute is set to something which the browser will not process as JavaScript:
<script id="t-animals" type="text/x-template"> <ul class="animals"> <% Y.Array.each(this.animals, function (animal, i) { %> <li class="<% i % 2 ? 'odd' : 'even' %>"> <%= animal %> </li> <% }); %> </ul> </script>
YUI().use('node-base', 'template-micro', 'view', function (Y) { Y.AnimalListView = Y.Base.create('animalListView', Y.View, [], { // The template source is pulled from the HTML, then compiled into a // template function which sits on the view's prototype. template: Y.Template.Micro.compile(Y.one('#t-animals').getHTML()), render: function () { var html = this.template({ animals: this.get('animals') }); this.get('container').setHTML(html); return this; } }); // Create an instance of the view and render it to the `<body>`. var animalListView = new Y.AnimalListView({ animals: [ 'Rhino', 'Plain Tiger butterfly', 'Spotted Cuscus' ] }); animalListView.get('container').appendTo('body'); });
Defining Templates in a Module
When your custom component is used within multiple apps, you might not have control over the HTML of the page. A great option is to create a separate module which holds your template source which your view module can require
:
// Defines a YUI module which will hold the template source for our view. YUI.add('animalListTemplate', function (Y) { Y.namespace('AnimalListView').template = Y.Template.Micro.compile( '<ul class="animals">' + '<% Y.Array.each(this.animals, function (animal, i) { %>' + '<li class="<% i % 2 ? "odd" : "even" %>">' + '<%= animal %>' + '</li>' + '<% }); %>' + '</ul>' ); }, '0.0.1', { requires: ['template-micro'] });
// Defines a YUI module defines our view and requires our template module. YUI.add('animalListView', function (Y) { var AnimalListView = Y.Base.create('animalListView', Y.View, [], { render: function () { var html = AnimalListView.template({ animals: this.get('animals') }); this.get('container').setHTML(html); return this; } }); // Properly exposes the view constructor while retaining the namespace. Y.AnimalListView = Y.mix(AnimalListView, Y.AnimalListView); }, '0.0.1', { requires: ['animalListTemplate', 'view'] });
// Create an instance of the view and render it to the `<body>`. YUI({ modules: { animalListTemplate: '/animal-list-template.js', animalListView : '/animal-list-view.js' } }).use('animalListView', function (Y) { var animalListView = new Y.AnimalListView({ animals: [ 'Rhino', 'Plain Tiger butterfly', 'Spotted Cuscus' ] }); animalListView.get('container').appendTo('body'); });
Refer to the Creating YUI Modules user guide for more details.
Best Practices
The following is a list of best practices to consider when using templates in your app and/or custom components:
Less Logic is Better
Make sure not to embed too much logic in your templates, things can get out of control if you do. You should avoid template logic which has side effects! Micro templates allow you to embed any arbitrary JavaScript in your templates, while Handlebars templates are logic-less by design.
Externalize Templates
Avoid embedding huge templates string in your JavaScript code. Strive to separate your templates from the code the uses them, having your templates specified in separate files is best. The template module example above would ideally use a build-time process to wrap the template source with the YUI module registration wrapper.
Compile Once, Render Often
Template compliation is expensive. You should avoid compiling a template more than once. Ideally, you are precompiling templates on the server or during a build-time process to avoid the compilation step on the client.