Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP

Loading…

Y.Template and Y.Template.Micro - Generic template API and simple ERB/Underscore-style JavaScript templates #230

Closed
wants to merge 10 commits into from
@rgrove

While working on a new widget recently, I found myself wanting a string-based templating solution that was more advanced than Y.Lang.sub() or Y.substitute(), but without the overhead of Y.Handlebars. I needed support for interpolation, if/else branching, and looping, but only for a few very small templates. Underscore-style templates (more familiar as ERB-style templates to Rubyists) seemed like exactly the right fit.

These commits add a new Y.Template class which provides a generic template engine API, and a Y.Template.Micro static class which provides a simple micro-templating engine. Y.Template can be used to compile, precompile, render, and revive precompiled templates using Handlebars or Y.Template.Micro.

Y.Template

Using with Y.Template.Micro (the default template engine):

YUI().use('template', function (Y) {
    var micro = new Y.Template(),
        html  = micro.render('<%= data.message %>', {message: 'hello!'});

    // ...
});

Using with Handlebars:

YUI().use('template-base', 'handlebars', function (Y) {
    var handlebars = new Y.Template(Y.Handlebars),
        html       = handlebars.render('{{message}}', {message: 'hello!'});

    // ...
});

See the API docs for further details.

Y.Template.Micro

Provides simple ERB/Underscore-style micro-templating. Can be used standalone or via Y.Template.

Within a template, <% ... %> is used to insert a block of JavaScript code, <%= ... %> evaluates and prints an expression as HTML-escaped output, and <%== ... %> evaluates and prints an expression as raw, unescaped output. Properties on the data object passed to a template function are made available within the template on the data variable.

Here's a simple template that renders a list:

<ul class="<%= data.classNames.list %>">
    <% Y.Array.each(data.items, function (item) { %>
        <li><%= item %></li>
    <% }); %>
</ul>

And here's the code to compile the template and render it (assume listTemplate is a string containing the template above):

YUI().use('template-micro', function (Y) {
    var compiled = Y.Template.Micro.compile(listTemplate),
        html;

    // Render the compiled template to HTML.
    html = compiled({
        classNames: {list: 'yui3-list'},
        items     : ['one', 'two', 'three', 'four']
    });

    // And again, with a different set of data.
    html = compiled({
        classNames: {list: 'yui3-list'},
        items     : ['a', 'b', 'c', 'd']
    });
});

Precompilation is supported too, so Micro can be used to precompile templates at build time or on the server.

// `precompile()` returns a string containing JavaScript code that will evaluate
// to a template function. It can be written to a file, served to a remote
// client, etc.
var source = Y.Template.Micro.precompile(listTemplate);

Performance

Y.Template.Micro is significantly faster than both Underscore and Handlebars at both compiling and rendering templates. See http://jsperf.com/y-template-vs-others/6

Running Locally

The built template, handlebars, and yui modules are intentionally not included in this pull request in order to keep the diffs clean. Before testing this change, you'll need to build them.

$ cd src/handlebars && shifter
$ cd src/template && shifter
$ cd src/yui && shifter
rgrove added some commits
@rgrove rgrove Initial commit of Y.template().
ERB and Underscore-style templating for YUI.
5ee431d
@rgrove rgrove Trailing comma. 884289f
@rgrove

Oh travisbot. When will you learn to read?

@davglass
Owner

Ha! yogi picked up your tests automatically since they are in there ;)

I'll have to check and see if the new Travis Env var is there for pull requests. Then I can have shifter auto build the modules before running tests. I'll look into this tomorrow ;)

@ericf
Owner

I worry that this muddies the waters with respect to YUI and templates. Also, putting JavaScript in the templates might encourage people to do bad things; someone like yourself will do so wisely, but I worry about the average developer uses this as a hammer.

I think we should be encouraging people to move templates outside of their JavaScript code and this seems like it might encourage people to keep them internal (as does Y.Lang.sub().)

I'm curious to hear your arguments of why this should be in core vs. Gallery though — it might just be that this is bringing back horrible memories of ASP for me :)

@rgrove

@ericf: Long story short? Because today was the second time in the past few weeks that I've found myself writing a widget that needed templates more complex than Y.Lang.sub() could handle, but not as complex as Y.Handlebars would have been. After spending some time going down the path of precompiling Handlebars templates at build time and baking them into my modules (and then seeing how much my code size ballooned as a result), I decided we needed an option in between.

For me, Y.template() is the Goldilocks option. It's simple, small, fast, and powerful. That also means that, yes, you can use it to shoot your foot off. But YUI is already a pretty big gun, and this is just one of many bullets. If we wanted to protect everyone from themselves, we'd be politicians, not JavaScript developers. ;)

Y.Lang.sub() is great for strings. Y.Handlebars() is great for web pages and complicated views. I see Y.template() being great for widgets that don't need complex views, but do need to render HTML (like the list above) with simple conditional or looping logic.

Given a choice between doing something unholy with Y.Lang.sub(), doing something way overkill with Y.Handlebars, or doing something quick, simple, and readable with Y.template(), I'll choose Y.template() every time.

Why should it be in core? Well, beyond what I said above, if it's not in core, then the widgets I'm working on can't go into core either. And I think you might want them. ;)

@ericf
Owner

If precompiled Handlebars templates were easy to work with and the plumbing was baked into core (and Shifter), would you still feel the need for Y.template()?

If you assume the handlebars-base module was always going to be loaded (so don't count it), would the precompiled Handlebars template functions still add too much bloat to your Widgets?

@rgrove

@ericf: Even if handlebars-base (~3KB) were always loaded (which I find unlikely), a precompiled Handlebars template for the list example above would still come to 1,380 bytes, whereas the precompiled Y.template() template is only 260 bytes.

https://gist.github.com/3459956 vs. https://gist.github.com/3459993

Even for a tiny template, Handlebars has a lot of boilerplate overhead. Y.template() doesn't.

@ericf
Owner

@rgrove Gzipped the templates are 183B and 462B respectively. So in this case it's 60% smaller than Handlebars, as the template grows in size, the Handlebars boilerplate will hopefully become less of the overall size of a template.

The tentative plan I've began talking about with people is moving all Widget templates to Handlebars, precompiling the template(s) for a module into a separate YUI module as part of the build process, and making the template-module a dependency of the Widget.

I would like to see us be able take a single approach for HTML templates and I think the fact that you've created Y.template() means there's more to consider for smaller templates and how we should approach templates for all Widgets. It would be great to have a single solution we can use for everything, including for people to use for their app templates :-/

I think this means we should have this discussion now instead of in Q4 when this work on library-wide templates is scheduled.

@rgrove

@ericf: Actually, I'd expect the opposite to be true. Handlebars has significant boilerplate code for each {{token}}, whereas Y.template() doesn't. So as the size of the template grows, the Handlebars boilerplate will continue to balloon. Luckily, it's very repetitive so it should gzip well, but even so, that's significant.

I don't think there's anything wrong with having multiple templating solutions to meet different needs, as long as the reasons for using each are clear. Handlebars has its place, as does Y.template(). I see Handlebars being a more attractive solution for view management (partials and helpers are big wins there) and for server-side templates. But I think Y.template() is more attractive for the kinds of client-side widgets YUI has right now.

@lsmith

I expect Handlebars would be overkill for widgets in general, and including that much overhead for the (albeit unrealistic) "putting a single X on the page requires YK of YUI code?" story is bad for adoption. It reenforces the perennial "YUI is heavy/over-engineered" complaint.

Y.template feels like the right balance for use by widgets for markup creation. Adding vastly more logic in the templates (via Handlebars) is a direction we don't want to go because that easily balloons into PHP, and will likely hinder configurability of individual widgets. For simple string substitution, Y.Lang.sub is fine, but the lack of a compile step is a missed opportunity for widget performance. The security consciousness of <%= ... %> is a safe default for markup generation that saves boilerplate code and/or prevents developer forgetfulness. The conditionals I'm on the fence about, because they are the first step on the slippery slope toward Handlebars-esque feature richness and complexity. But I do think it's a reasonable place to draw the line in the sand and will prove practical and useful for widgets.

DataTable might benefit from Y.template.

@hojberg

Side discussion.
Should this be called something other than Y.template() ? I feel like Y.template should be used for something that is template-engine agnostic, and could be configured to use specific engines. Could make it a target for gallery modules that added new template engines.

@rgrove

@hojberg: I doubt we're likely to see (or need) something like that in YUI, but I'm open to other names if there's something that's clearly better.

@jshirley

I find myself agreeing with nearly all of the comments here. Y.template would discourage usage of Handlebars, when in many cases Handlebars would be the right solution. I can see in cases of widgets, the currently named Y.template.

However, having the name as global as Y.template without substituting the engine seems overly empowering to the (very slightly) more dangerous option.

I do think the name is key. Y.Handlebars exists specifically for this, but perhaps Y.Template as a top level namespace is better, and this can be something like:

var myTemplate = Y.Template.simple("Hello <%= name =%>!");
myTemplate({ name : 'World' });

(Then a wrapper for Y.Template.handlebars that calls Y.Handlebars.compile, for a consistent interface).

@davglass
Owner
@juandopazo
Collaborator

I understand the motivations, but I agree with Eric and Dav. It reminds me of my old PHP days as well. I don't like that it doesn't look at all like the existing options in YUI. So I ask myself, what do we need that's between Y.Lang.sub and Handlebars? Iteration? Mapping objects?

Why not Mustache instead? It's a subset of Handlebars and weighs much less:

  • Handlebars minified with Uglify: 29.5 KB
  • Mustache minified with Uglify: 4.39 KB
@hojberg

@jshirley exactly what I was thinking!

@rgrove

@jshirley: I'd be okay with moving this to the Y.Template namespace, and I think your comments tie in nicely with what Dav said...

@davglass: Y.Lang.sub() is still useful. Y.substitute() is useless, crappy, and needs to die. I'm not interested in adding a compat shim so that Y.template() can work like Y.substitute(), because I think the way Y.substitute() works is stupid.

I definitely see your point about Y.template() and Y.Handlebars needing to share common ground in how templates are compiled and loaded. I'll give this some thought. Whatever happens though, I don't want Y.template() to become something more complex than it is now -- there's huge value in a tiny (<1KB) module that can meet all simple templating needs with no confusing abstractions.

@juandopazo: What does Mustache provide that Y.template() doesn't to make it worth its 4.39KB? That's over four times the size of Y.template(), and even larger than the handlebars-base module needed to render precompiled Handlebars templates. If it's the syntax you like, then Y.template() can easily support that. Mustache's many shortcomings are why we ended up choosing Handlebars as YUI's heavy-weight templating solution. One of its biggest shortcomings is that templates are always interpreted at render time and can't be compiled, which would make Mustache both slower and bigger than using precompiled Handlebars templates.

I have to accept some of the blame for the current situation. Back when we were trying to decide on a templating solution for YUI, I had the urge to investigate Underscore-style templating, but ended up being swayed by the power and strict separation of concerns that Handlebars provided. In hindsight, I think Underscore-style templating would have been a better fit for YUI's client-side templating needs, and on the server where Handlebars templates make more sense it's easy enough to use the non-YUI Handlebars module.

I won't say I regret bringing Handlebars into YUI (or writing all that documentation -- gawd), but I do think it's not the best template language for client-side widgets, and I think Y.template() is.

@rgrove rgrove Generic Y.Template API and Y.template() -> Y.Template.Micro
Moves Y.template() to a static Y.Template.Micro namespace and
adds Y.Template, a generic template engine API that supports
Y.Handlebars, Y.Template.Micro, and any other template engine
that adheres to a simple API interface.
d3ce3d7
@rgrove

Okay dudes. My latest commit incorporates your feedback and adds a generic template API for both micro-templates and Handlebars. It also moves Y.template() to a static Y.Template.Micro namespace. I've updated the pull request description above with details of the new changes. Let me know what you think.

@okuryu
Collaborator

I feel that need to add docs/* if pull in this module. And, I would like to expect to get benefit from this one, but I think to need to some notes about "JavaScript in the templates" on docs as Eric remarks.

@rgrove

I'll definitely add a user guide if this gets pulled in. Wasn't going to write one on spec though -- too much work if things change.

@hojberg

@rgrove great stuff!

@rgrove

Updated benchmarks: http://jsperf.com/y-template-vs-others/4/

Turns out Y.Template.Micro using the variable option to disable the use of with inside compiled templates is faster than both Underscore and Handlebars.

@davglass
Owner

@rgrove I like the changes, great work as always!

@okuryu
Collaborator

I'd like to share talk about this Pull Request.
Part 1: http://www.youtube.com/watch?v=dLZ3AGD5d1w
Part 2: http://www.youtube.com/watch?v=qO2xCmnY53g

@rgrove

I filed an enhancement ticket for shifter describing how I think it should support automatic template precompilation for YUI modules using Y.Template. Feel free to weigh in on that discussion as well: yui/shifter#11

src/template/js/template-micro.js
((138 lines not shown))
+ // Replace the token placeholders with code.
+ .replace(/\ufffe(\d+)\uffff/g, function (match, index) {
+ return blocks[parseInt(index, 10)];
+ }) +
+
+ "';";
+
+ if (!options.variable) {
+ source = "with(data){\n" + source + "}";
+ }
+
+ source = "var $t='';\n" + source + "\nreturn $t;";
+
+ // Compile the template source into an executable function.
+ template = this.revive(new Function('Y', options.variable || 'data',
+ 'htmlEscaper', source));

Continuing thought from davglass/shifter#11:

This is where Y.Template.Micro#precompile() falls down, in my view, when used as a precompilation engine for shifter. It would be preferable to pass { "source": true } or something as the options for compile(), and only revive() the generated function when source is false. (Conversely, when the source option is true, return only the generated source, as with Handlebars#precompile())

if (options.source) {
    template = "function(Y," + (options.variable || 'data') +
        ",htmlEscaper){\n" + source + "\n}";
} else {
    template = this.revive(new Function ('Y', options.variable || 'data',
            'htmlEscaper', source));
}
@rgrove
rgrove added a note

This seems reasonable, although I don't think it has any direct bearing on what we were discussing re. shifter (except that this will make precompilation slightly more efficient). The question of whether precompilation should be possible without requiring YUI at all is still a separate issue, and I still think the answer to that is no.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
rgrove added some commits
@rgrove rgrove Don't rely on `with`, don't do extra work during precompilation.
Removing the `with` block in compiled templates makes rendering way
faster, bumping Micro ahead of both Underscore and Handlebars in
rendering speed.
173bf22
@rgrove rgrove Make Y.Template.Handlebars an alias for Y.Handlebars 7d9b214
@rgrove

No movement on this pull request despite the two hour meeting we had last week in which the team consensus was that this should be merged.

What's blocking this?

@hojberg

I'd love so much to see this merged!

@nhusher

Read through the code this morning. I would like to see this in core, especially with the ability to plug multiple render engines into Y.Template.

@yzapuchlak

Personally, I don't see myself using the Micro templating language in the near future (handlebars is working well for my current needs), but I think the Y.Template engine framework is a very valuable addition as it would standardize how people go about creating modules (gallery or private) to add YUI support for any of the other popular (or niche) templating languages out there.

So I agree with many on this thread. This would be great to see pulled in.

@ericf
Owner

I will merge this in after our 3.7.0 code freeze (which we're in right now). @rgrove sorry that the window passed us by for getting this into 3.7.0, and I'd like to stick firmly to our code freeze process.

This will also give me a chance to play around with the new APIs and really push on them. Having Y.Template will work out nicely with this ticket #2532428, which is something I'll be tackling for the next development sprint.

Once the code freeze is lifted, I'll merge this in and we'll start planning the first 3.8.0 preview release. Getting a preview release out quickly will get Y.Template and Y.Template.Micro on the CDN, and more importantly, be easy for other developers to try out and give feedback.

@rgrove

Thanks @ericf!

@dmitris

is the freeze over now as 3.7.0 has been released?

@ericf
Owner

@dmitris We are still in a code freeze post release to make sure we don't have to issue any emergency patches.

@hojberg

Possible to get this merged?

@ericf
Owner
@hojberg

thanks @ericf !

@ericf ericf was assigned
@ericf
Owner

I think that "template" is the wrong noun for the object returned from calling the Y.Template() constructor function. I found this confusing as to what to name my var which holds the instance object returned from Y.Template() while filling out the rest of the unit tests.

Really what we're doing is creating a template engine instance. The following seems more correct to me:

var engine = Y.Template.createEngine(Y.Handlebars),
    html   = engine.render('{{message}}', {message: 'hello!'});
@rgrove

@ericf: I think of it like this: Y.Template.Micro is an engine. An instance of Y.Template.Micro is a template.

Incidentally, what unit tests are you filling out?

@ericf
Owner

@rgrove Agreed, so what is the return value from Y.Template()? To me it returns a generic engine wrapper, therefore I see it as more of an engine factory than returning a template instance.

My updates to the unit tests are very simple, edge case tests which get template to 100% line, function, statement, and branch coverage! \o/

@rgrove

@ericf: Awesome, thanks for filling out the test coverage!

The return value from new Y.Template() is still a template, it's just a template that has some internal logic that tells it, at instantiation, what engine it should use. Y.Template is a generic engine wrapper, but an instance of Y.Template is a wrapped template.

@ericf
Owner

I'd argue that "{{message}}" is the template from the code snippet in my above comment.

@rgrove

@ericf: Agreed. "{{message}}" is a template, and Y.Handlebars.compile("{{message}}") is a compiled template. Y.Handlebars and Y.Template.Micro are engines, and the return value of new Y.Template() is an engine. I've made some clarifications in the doc comments.

@ericf
Owner

@rgrove I'll make you a deal… if you stub out a TOC for a Template/Template.Micro user guide, I'll fill in the details. I think we can get away with one user guide to service both Y.Template() and Y.Template.Micro. After I fill in the details you can copy-edit and add polish, if you wish (and have time to do so.)

@rgrove

@ericf You're too kind! I'll have something for you later today.

@rgrove

@ericf I pushed a first pass at a user guide TOC. Let me know what you think.

@ericf
Owner

This has been merged into 3.x.

@ericf ericf closed this
@hojberg

Awesome! :dancers:

@rgrove rgrove referenced this pull request in yui/shifter
Closed

Support template precompilation using Y.Template #11

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Commits on Aug 25, 2012
  1. @rgrove

    Initial commit of Y.template().

    rgrove authored
    ERB and Underscore-style templating for YUI.
  2. @rgrove

    Trailing comma.

    rgrove authored
  3. @rgrove
Commits on Aug 26, 2012
  1. @rgrove

    Generic Y.Template API and Y.template() -> Y.Template.Micro

    rgrove authored
    Moves Y.template() to a static Y.Template.Micro namespace and
    adds Y.Template, a generic template engine API that supports
    Y.Handlebars, Y.Template.Micro, and any other template engine
    that adheres to a simple API interface.
Commits on Aug 29, 2012
  1. @rgrove

    Don't rely on `with`, don't do extra work during precompilation.

    rgrove authored
    Removing the `with` block in compiled templates makes rendering way
    faster, bumping Micro ahead of both Underscore and Handlebars in
    rendering speed.
  2. @rgrove
Commits on Oct 2, 2012
  1. @rgrove
Commits on Oct 15, 2012
  1. @rgrove
Commits on Oct 17, 2012
  1. @rgrove
Commits on Nov 1, 2012
  1. @rgrove

    Template: Don't print `undefined` for falsy values.

    rgrove authored
    Can't believe I forgot this.
This page is out of date. Refresh to see the latest.
View
9 src/handlebars/js/yui-handlebars-base-after.js
@@ -33,9 +33,6 @@ Y.Handlebars = Handlebars;
Handlebars.VERSION += '-yui';
-// The rest of this file is just API docs for methods defined in Handlebars
-// itself.
-
/**
Registers a helper function that will be made available to all templates.
@@ -113,3 +110,9 @@ Converts a precompiled template into a renderable template function.
@param {Function} template Precompiled Handlebars template function.
@return {Function} Compiled template function.
*/
+
+// Alias for Y.Handlebars.template(), used by Y.Template.
+Handlebars.revive = Handlebars.template;
+
+// Make Y.Template.Handlebars an alias for Y.Handlebars.
+Y.namespace('Template').Handlebars = Handlebars;
View
7 src/template/HISTORY.md
@@ -0,0 +1,7 @@
+Template Change History
+=======================
+
+3.8.0
+-----
+
+* Initial release.
View
5 src/template/README.md
@@ -0,0 +1,5 @@
+Template
+========
+
+Provides a generic API for using template engines such as Handlebars and
+`Y.Template.Micro`.
View
17 src/template/build.json
@@ -0,0 +1,17 @@
+{
+ "name": "template",
+
+ "builds": {
+ "template-base": {
+ "jsfiles": [
+ "template-base.js"
+ ]
+ },
+
+ "template-micro": {
+ "jsfiles": [
+ "template-micro.js"
+ ]
+ }
+ }
+}
View
13 src/template/docs/component.json
@@ -0,0 +1,13 @@
+{
+ "name" : "template",
+ "displayName": "Template",
+ "description": "Provides a generic template engine API and a micro-templating language similar to ERB and Underscore templates.",
+ "author" : ["rgrove", "ericf"],
+
+ "tags": [
+ "utility", "beta", "template", "templating", "micro", "handlebars",
+ "erb", "underscore", "view"
+ ],
+
+ "use": ["template"]
+}
View
69 src/template/docs/index.mustache
@@ -0,0 +1,69 @@
+<div class="intro">
+<p>
+The Template component provides `Y.Template`, a generic template engine API, and `Y.Template.Micro`, a string-based micro-templating language similar to <a href="http://ruby-doc.org/stdlib-1.9.3/libdoc/erb/rdoc/ERB.html">ERB</a> and <a href="http://underscorejs.org/#template">Underscore</a> templates.
+</p>
+</div>
+
+{{>getting-started}}
+
+<h2>Using `Template`</h2>
+
+<h3>Quick Start</h3>
+
+<p>
+A template engine takes a template&mdash;usually in the form of a string&mdash;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.
+</p>
+
+<p>
+`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 <a href="../handlebars/">Handlebars</a> and [[#Using Template.Micro|Template.Micro]].
+</p>
+
+<p>[example of rendering a Template.Micro template]</p>
+
+<p>[example of rendering a Handlebars template]</p>
+
+<h3>Generic Template API</h3>
+
+<p>[overview of the purpose of Y.Template's generic API -- a widget's default templates can be Handlebars templates, but you can override with Micro templates, etc.]</p>
+
+<h3>Instantiating a Template Engine</h3>
+
+<h3>Compiling and Rendering Templates</h3>
+
+<h3>Precompiling and Reviving Templates</h3>
+
+<h3>Creating a Custom Template Engine</h3>
+
+<p>[how to create a template engine that conforms to the `Y.Template` API interface]</p>
+
+<h2>Using `Template.Micro`</h2>
+
+<p>[Overview of what Template.Micro is and what kinds of use cases it's suited for -- when you'd use it vs. Handlebars, etc.]</p>
+
+<h3>Template Syntax</h3>
+
+<h4>Basic Expressions</h4>
+
+<h4>HTML Escaping</h4>
+
+<h4>Inline Code &amp; Code Blocks</h4>
+
+<h3>Compiling and Rendering Templates</h3>
+
+<p>[may be redundant; possibly just refer to the Y.Template section above?]</p>
+
+<h3>Precompiling and Reviving Templates</h3>
+
+<p>[may be redundant; possibly just refer to the Y.Template section above?]</p>
+
+<h3>Customizing Template Syntax</h3>
+
+<p>[describe how to override Y.Template.Micro's regexes to customize the template syntax if desired]</p>
+
+<h2>Using Templates in Custom Components</h2>
+
+<p>[guidelines for bundling templates with custom views, widgets, etc. -- how to build, compile, and make them available on a namespace]</p>
+
+<h2>Best Practices</h2>
+
+<p>[recommended best practices for templates -- don't embed too much logic, don't embed huge template strings in JS, compile once; render often, etc.]</p>
View
132 src/template/js/template-base.js
@@ -0,0 +1,132 @@
+/**
+Virtual rollup of the `template-base` and `template-micro` modules.
+
+@module template
+@main template
+@since 3.8.0
+**/
+
+/**
+Provides a generic API for using template engines such as Handlebars and
+`Y.Template.Micro`.
+
+@module template
+@submodule template-base
+@since 3.8.0
+**/
+
+/**
+Provides a generic API for using template engines such as Handlebars and
+`Y.Template.Micro`.
+
+### Examples
+
+Using with `Y.Template.Micro` (the default template engine):
+
+ YUI().use('template', function (Y) {
+ var micro = new Y.Template(),
+ html = micro.render('<%= data.message %>', {message: 'hello!'});
+
+ // ...
+ });
+
+Using with Handlebars:
+
+ YUI().use('template-base', 'handlebars', function (Y) {
+ var handlebars = new Y.Template(Y.Handlebars),
+ html = handlebars.render('{{message}}', {message: 'hello!'});
+
+ // ...
+ });
+
+@class Template
+@param {Mixed} [engine=Y.Template.Micro] Template engine to use, such as
+ `Y.Template.Micro` or `Y.Handlebars`. Defaults to `Y.Template.Micro` if not
+ specified.
+@constructor
+@since 3.8.0
+**/
+
+function Template(engine) {
+ /**
+ Template engine class.
+
+ @property {Mixed} engine
+ **/
+ this.engine = engine || Y.Template.Micro;
+
+ if (!this.engine) {
+ Y.error('No template engine loaded.');
+ }
+}
+
+Template.prototype = {
+ /**
+ Compiles a template with the current template engine and returns a compiled
+ template function.
+
+ @method compile
+ @param {String} text Template text to compile.
+ @param {Object} [options] Options to pass along to the template engine. See
+ template engine docs for options supported by each engine.
+ @return {Function} Compiled template function.
+ **/
+ compile: function (text, options) {
+ return this.engine.compile(text, options);
+ },
+
+ /**
+ Precompiles a template with the current template engine and returns a string
+ containing JavaScript source code for the precompiled template.
+
+ @method precompile
+ @param {String} text Template text to compile.
+ @param {Object} [options] Options to pass along to the template engine. See
+ template engine docs for options supported by each engine.
+ @return {String} Source code for the precompiled template.
+ **/
+ precompile: function (text, options) {
+ return this.engine.precompile(text, options);
+ },
+
+ /**
+ Compiles and renders a template with the current template engine in a single
+ step, and returns the rendered result.
+
+ @method render
+ @param {String} text Template text to render.
+ @param {Object} data Data object to provide when rendering the template.
+ @param {Object} [options] Options to pass along to the template engine. See
+ template engine docs for options supported by each engine.
+ @return {String} Rendered result.
+ **/
+ render: function (text, data, options) {
+ if (this.engine.render) {
+ return this.engine.render(text, data, options);
+ }
+
+ return this.engine.compile(text, options)(data, options);
+ },
+
+ /**
+ Revives a precompiled template function into an executable template function
+ using the current template engine. The precompiled code must already have
+ been evaluated; this method won't evaluate it for you.
+
+ @method revive
+ @param {Function} precompiled Precompiled template function.
+ @param {Object} [options] Options to pass along to the template engine. See
+ template engine docs for options supported by each engine.
+ @return {Function} Compiled template function.
+ **/
+ revive: function (precompiled, options) {
+ return this.engine.revive ? this.engine.revive(precompiled, options) :
+ precompiled;
+ }
+};
+
+// Copy existing namespaced properties from Y.Template to the Template function
+// if Y.Template already exists, then make the function the new Y.Template.
+// This ensures that other modules can safely add stuff to the Y.Template
+// namespace even if they're loaded before this one.
+Y.Template = Y.Template ? Y.mix(Template, Y.Template) : Template;
View
212 src/template/js/template-micro.js
@@ -0,0 +1,212 @@
+/**
+Adds the `Y.Template.Micro` template engine, which provides fast, simple
+string-based micro-templating similar to ERB or Underscore templates.
+
+@module template
+@submodule template-micro
+@since 3.8.0
+**/
+
+/**
+Fast, simple string-based micro-templating engine similar to ERB or Underscore
+templates.
+
+@class Template.Micro
+@since 3.8.0
+@static
+**/
+
+// This code was heavily inspired by Underscore.js's _.template() method
+// (written by Jeremy Ashkenas), which was in turn inspired by John Resig's
+// micro-templating implementation.
+
+var Micro = Y.namespace('Template.Micro');
+
+/**
+Default options for `Y.Template.Micro`.
+
+@property {Object} options
+
+ @param {RegExp} [options.code] Regex that matches code blocks like
+ `<% ... %>`.
+ @param {RegExp} [options.escapedOutput] Regex that matches escaped output
+ tags like `<%= ... %>`.
+ @param {RegExp} [options.rawOutput] Regex that matches raw output tags like
+ `<%== ... %>`.
+ @param {RegExp} [options.stringEscape] Regex that matches characters that
+ need to be escaped inside single-quoted JavaScript string literals.
+
+@static
+@since 3.8.0
+**/
+Micro.options = {
+ code : /<%([\s\S]+?)%>/g,
+ escapedOutput: /<%=([\s\S]+?)%>/g,
+ rawOutput : /<%==([\s\S]+?)%>/g,
+ stringEscape : /\\|'|\r|\n|\t|\u2028|\u2029/g
+};
+
+/**
+Compiles a template string into a JavaScript function. Pass a data object to the
+function to render the template using the given data and get back a rendered
+string.
+
+Within a 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. To output a raw value without escaping, use
+`<%== ... %>`, but be careful not to do this with untrusted user input.
+
+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.
+
+Properties of the data object passed to a template function are made available
+on a `data` variable within the scope of the template. So, if you pass in
+the object `{message: 'hello!'}`, you can print the value of the `message`
+property using `<%= data.message %>`.
+
+@example
+
+ YUI().use('template-micro', function (Y) {
+ var template = '<ul class="<%= data.classNames.list %>">' +
+ '<% Y.Array.each(data.items, function (item) { %>' +
+ '<li><%= item %></li>' +
+ '<% }); %>' +
+ '</ul>';
+
+ // Compile the template into a function.
+ var compiled = Y.Template.Micro.compile(template);
+
+ // Render the template to HTML, passing in the data to use.
+ var html = compiled({
+ classNames: {list: 'demo'},
+ items : ['one', 'two', 'three', 'four']
+ });
+ });
+
+@method compile
+@param {String} text Template text to compile.
+@param {Object} [options] Options. If specified, these options will override the
+ default options defined in `Y.Template.Micro.options`. See the documentation
+ for that property for details on which options are available.
+@return {Function} Compiled template function. Execute this function and pass in
+ a data object to render the template with the given data.
+@static
+**/
+Micro.compile = function (text, options) {
+ var blocks = [],
+ tokenClose = "\uffff",
+ tokenOpen = "\ufffe",
+ source;
+
+ options = Y.merge(Micro.options, options);
+
+ // Parse the input text into a string of JavaScript code, with placeholders
+ // for code blocks. Text outside of code blocks will be escaped for safe
+ // usage within a double-quoted string literal.
+ source = "var $b='',$t='" +
+
+ // U+FFFE and U+FFFF are guaranteed to represent non-characters, so no
+ // valid UTF-8 string should ever contain them. That means we can freely
+ // strip them out of the input text (just to be safe) and then use them
+ // for our own nefarious purposes as token placeholders!
+ //
+ // See http://en.wikipedia.org/wiki/Mapping_of_Unicode_characters#Noncharacters
+ text.replace(/\ufffe|\uffff/g, '')
+
+ .replace(options.rawOutput, function (match, code) {
+ return tokenOpen + (blocks.push("'+\n((" + code + ")||$b)+\n'") - 1) + tokenClose;
+ })
+
+ .replace(options.escapedOutput, function (match, code) {
+ return tokenOpen + (blocks.push("'+\n$e((" + code + ")||$b)+\n'") - 1) + tokenClose;
+ })
+
+ .replace(options.code, function (match, code) {
+ return tokenOpen + (blocks.push("';\n" + code + "\n$t+='") - 1) + tokenClose;
+ })
+
+ .replace(options.stringEscape, "\\$&")
+
+ // Replace the token placeholders with code.
+ .replace(/\ufffe(\d+)\uffff/g, function (match, index) {
+ return blocks[parseInt(index, 10)];
+ })
+
+ // Remove noop string concatenations that have been left behind.
+ .replace(/\n\$t\+='';\n/g, '\n') +
+
+ "';\nreturn $t;";
+
+ // If compile() was called from precompile(), return precompiled source.
+ if (options.precompile) {
+ return "function (Y, $e, data) {\n" + source + "\n}";
+ }
+
+ // Otherwise, return an executable function.
+ return this.revive(new Function('Y', '$e', 'data', source));
+};
+
+/**
+Precompiles the given template text into a string of JavaScript source code that
+can be evaluated later in another context (or on another machine) to render the
+template.
+
+A common use case is to precompile templates at build time or on the server,
+then evaluate the code on the client to render a template. The client only needs
+to revive and render the template, avoiding the work of the compilation step.
+
+@method precompile
+@param {String} text Template text to precompile.
+@param {Object} [options] Options. If specified, these options will override the
+ default options defined in `Y.Template.Micro.options`. See the documentation
+ for that property for details on which options are available.
+@return {String} Source code for the precompiled template.
+@static
+**/
+Micro.precompile = function (text, options) {
+ options || (options = {});
+ options.precompile = true;
+
+ return this.compile(text, options);
+};
+
+/**
+Compiles and renders the given template text in a single step.
+
+This can be useful for single-use templates, but if you plan to render the same
+template multiple times, it's much better to use `compile()` to compile it once,
+then simply call the compiled function multiple times to avoid recompiling.
+
+@method render
+@param {String} text Template text to render.
+@param {Object} data Data to pass to the template.
+@param {Object} [options] Options. If specified, these options will override the
+ default options defined in `Y.Template.Micro.options`. See the documentation
+ for that property for details on which options are available.
+@return {String} Rendered result.
+@static
+**/
+Micro.render = function (text, data, options) {
+ return this.compile(text, options)(data);
+};
+
+/**
+Revives a precompiled template function into a normal compiled template function
+that can be called to render the template. The precompiled function must already
+have been evaluated to a function -- you can't pass raw JavaScript code to
+`revive()`.
+
+@method revive
+@param {Function} precompiled Precompiled template function.
+@return {Function} Revived template function, ready to be rendered.
+@static
+**/
+Micro.revive = function (precompiled) {
+ return function (data) {
+ data || (data = {});
+ return precompiled.call(data, Y, Y.Escape.html, data);
+ };
+};
View
20 src/template/meta/template.json
@@ -0,0 +1,20 @@
+{
+ "template": {
+ "use": [
+ "template-base",
+ "template-micro"
+ ]
+ },
+
+ "template-base": {
+ "requires": [
+ "yui-base"
+ ]
+ },
+
+ "template-micro": {
+ "requires": [
+ "escape"
+ ]
+ }
+}
View
218 src/template/tests/unit/assets/template-test.js
@@ -0,0 +1,218 @@
+YUI.add('template-test', function (Y) {
+
+var Assert = Y.Assert,
+ Micro = Y.Template.Micro,
+
+ templateSuite = Y.TemplateTestSuite = new Y.Test.Suite('Template'),
+ microSuite = new Y.Test.Suite('Micro');
+
+// -- Y.Template.Micro ---------------------------------------------------------
+
+// -- Methods ------------------------------------------------------------------
+microSuite.add(new Y.Test.Case({
+ name: 'Methods',
+
+ 'compile() should return a compiled template function': function () {
+ var compiled = Micro.compile('test');
+
+ Assert.isFunction(compiled, 'return value should be a function');
+ Assert.areSame('test', compiled(), 'executing the function should render the template');
+ },
+
+ 'compile() should return precompiled source if the `precompile` option is `true`': function () {
+ Assert.isString(Micro.compile('test', {precompile: true}));
+ },
+
+ 'compile() options should be overridable': function () {
+ var compiled = Micro.compile('{{data.foo}}', {escapedOutput: /\{\{([\s\S]+?)\}\}/g});
+ Assert.areSame('bar', compiled({foo: 'bar'}));
+ },
+
+ 'precompile() should return precompiled template code': function () {
+ Assert.areSame(
+ "function (Y, $e, data) {\nvar $b='',$t='test';\nreturn $t;\n}",
+ Micro.precompile('test')
+ );
+ },
+
+ 'precompile() should respect compile options': function () {
+ Assert.areSame(
+ "function (Y, $e, data) {\nvar $b='',$t=''+\n$e((data.test)||$b)+\n'';\nreturn $t;\n}",
+
+ Micro.precompile('{{data.test}}', {
+ escapedOutput: /\{\{([\s\S]+?)\}\}/g
+ })
+ )
+ },
+
+ 'render() should compile and render in a single step': function () {
+ Assert.areSame('bar baz', Micro.render('<%=data.a%> <%=data.b%>', {a: 'bar', b: 'baz'}));
+ },
+
+ 'render() should respect compile options': function () {
+ Assert.areSame('foo', Micro.render('{{data.a}}', {a: 'foo'}, {escapedOutput: /\{\{([\s\S]+?)\}\}/g}));
+ },
+
+ 'revive() should revive a precompiled template into an executable template function': function () {
+ eval('var precompiled=' + Micro.precompile('<%== data.a %> <%= data.b %>') + ';');
+
+ var revived = Micro.revive(precompiled);
+
+ Assert.areSame('bar baz', revived({a: 'bar', b: 'baz'}));
+ },
+
+ '`this` object in the compiled template should refer to the data object': function () {
+ Assert.areSame('bar', Micro.render('<%= this.foo %>', {foo: 'bar'}));
+ }
+}));
+
+// -- Syntax -------------------------------------------------------------------
+microSuite.add(new Y.Test.Case({
+ name: 'Syntax',
+
+ '<% ... %> should be rendered as a block of JavaScript code': function () {
+ var compiled = Micro.compile('<% if (data.display) { %>hello<% } %>');
+
+ Assert.areSame('hello', compiled({display: true}));
+ Assert.areSame('', compiled({display: false}));
+ },
+
+ '<% ... %> should allow line breaks': function () {
+ var compiled = Micro.compile("<%\nif (data.display) {\n%>hello<%\n}\n%>");
+
+ Assert.areSame('hello', compiled({display: true}));
+ Assert.areSame('', compiled({display: false}));
+ },
+
+ '<% ... %> should support switch statements': function () {
+ var compiled = Micro.compile(
+ 'test' +
+ '<% switch (data.foo) { %>' +
+ '<% case "a": %>' +
+ 'a' +
+ '<% break; %>' +
+
+ '<% case "b": %>' +
+ 'b' +
+ '<% break; %>' +
+
+ '<% case "c": %>' +
+ 'c' +
+ '<% break; %>' +
+ '<% } %>' +
+ 'test'
+ );
+
+ Assert.areSame('testatest', compiled({foo: 'a'}));
+ Assert.areSame('testbtest', compiled({foo: 'b'}));
+ Assert.areSame('testctest', compiled({foo: 'c'}));
+ },
+
+ '<%= ... %> should be rendered as HTML-escaped output': function () {
+ Assert.areSame('at&amp;t', Micro.render('<%= data.name %>', {name: 'at&t'}));
+ },
+
+ '<%= ... %> should print an empty string if given a falsy value': function () {
+ Assert.areSame('foobar', Micro.render('foo<%= data.bogus %>bar'));
+ },
+
+ '<%== ... %> should be rendered as raw output': function () {
+ Assert.areSame('at&t', Micro.render('<%== data.name %>', {name: 'at&t'}));
+ },
+
+ '<%== ... %> should print an empty string if given a falsy value': function () {
+ Assert.areSame('foobar', Micro.render('foo<%== data.bogus %>bar'));
+ }
+}));
+
+templateSuite.add(microSuite);
+
+// -- Y.Template ---------------------------------------------------------------
+
+templateSuite.add(new Y.Test.Case({
+ name: 'Lifecycle',
+
+ 'constructor should accept an engine class': function () {
+ Assert.areSame(Y.Handlebars, (new Y.Template(Y.Handlebars)).engine);
+ },
+
+ 'engine should default to Y.Template.Micro if available': function () {
+ Assert.areSame(Y.Template.Micro, (new Y.Template()).engine);
+ }
+}));
+
+templateSuite.add(new Y.Test.Case({
+ name: 'Methods',
+
+ 'compile() should compile a template using the selected engine': function () {
+ Assert.areSame('foo', (new Y.Template()).compile('<%=data.a%>')({a: 'foo'}), 'should compile Micro templates');
+ Assert.areSame('foo', (new Y.Template(Y.Handlebars)).compile('{{a}}')({a: 'foo'}), 'should compile Handlebars templates');
+ },
+
+ 'compile() should pass options to the selected engine': function () {
+ Assert.areSame(
+ 'foo',
+ (new Y.Template()).compile('{{data.a}}', {
+ escapedOutput: /\{\{([\s\S]+?)\}\}/g
+ })({a: 'foo'})
+ );
+ },
+
+ 'precompile() should precompile a template using the selected engine': function () {
+ Assert.areSame(
+ "function (Y, $e, data) {\nvar $b='',$t='test';\nreturn $t;\n}",
+ (new Y.Template()).precompile('test')
+ );
+ },
+
+ 'precompile() should pass options to the selected engine': function () {
+ Assert.areSame(
+ "function (Y, $e, data) {\nvar $b='',$t=''+\n$e((data.test)||$b)+\n'';\nreturn $t;\n}",
+
+ (new Y.Template()).precompile('{{data.test}}', {
+ escapedOutput: /\{\{([\s\S]+?)\}\}/g
+ })
+ )
+ },
+
+ 'render() should compile and render a template using the selected engine': function () {
+ Assert.areSame(
+ 'foo',
+ (new Y.Template()).render('<%=data.a%>', {a: 'foo'})
+ );
+ },
+
+ "render() should compile and render internally if the selected engine doesn't have a render() method": function () {
+ var FakeEngine = Y.merge(Y.Template.Micro);
+ delete FakeEngine.render;
+
+ Assert.areSame(
+ 'foo',
+ (new Y.Template(FakeEngine)).render('<%=data.a%>', {a: 'foo'})
+ );
+ },
+
+ 'render() should pass options to the selected engine': function () {
+ Assert.areSame(
+ 'foo',
+ (new Y.Template()).render('{{data.a}}', {a: 'foo'}, {
+ escapedOutput: /\{\{([\s\S]+?)\}\}/g
+ })
+ );
+ },
+
+ 'revive() should revive a precompiled template': function () {
+ eval('var precompiled = ' + Micro.precompile('<%=data.a%>') + ';');
+ Assert.areSame('foo', (new Y.Template()).revive(precompiled)({a: 'foo'}));
+ },
+
+ "revive() should return the given template if the engine doesn't have a revive() method": function () {
+ eval('var precompiled = ' + Micro.precompile('<%=data.a%>') + ';');
+ Assert.areSame(precompiled, (new Y.Template({})).revive(precompiled));
+
+ }
+}));
+
+}, '@VERSION@', {
+ requires: ['handlebars', 'template', 'test']
+});
View
31 src/template/tests/unit/template.html
@@ -0,0 +1,31 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Test Page</title>
+</head>
+<body class="yui3-skin-sam">
+
+<div id="log"></div>
+
+<script src="../../../../build/yui/yui.js"></script>
+<script>
+var Y = YUI({
+ filter: (window.location.search.match(/[?&]filter=([^&]+)/) || [])[1] || 'raw',
+ coverage: [ 'template' ],
+ modules: {
+ 'template-test': {
+ fullpath: 'assets/template-test.js',
+ requires: ['handlebars', 'template', 'test']
+ }
+ },
+ useBrowserConsole: false
+}).use('template-test', 'test-console', function (Y) {
+ new Y.Test.Console().render('#log');
+ Y.Test.Runner.add(Y.TemplateTestSuite);
+ Y.Test.Runner.run();
+});
+</script>
+
+</body>
+</html>
Something went wrong with that request. Please try again.