Commit 22ebd710 authored by S Anand's avatar S Anand
Browse files

ENH: .template() supports sub-templates

parent 9900de27
# $.template # $.template
`$.template()` renders HTML templates with JavaScript mixed inside them. `$(selector).template()` renders HTML templates with JavaScript mixed inside them.
Example: Example:
...@@ -21,7 +21,7 @@ This displays `Your platform is ...` and shows the userAgent just below the scri ...@@ -21,7 +21,7 @@ This displays `Your platform is ...` and shows the userAgent just below the scri
Note: the `<template>` tag is not supported by Internet Explorer. Use Note: the `<template>` tag is not supported by Internet Explorer. Use
`<script type="text/html">` for IE compatibility. `<script type="text/html">` for IE compatibility.
The template can use all global variables. You can pass additional variables Templates can access any global variable. You can pass additional variables
using as `.template({var1: value, var2: value, ...})`. For example: using as `.template({var1: value, var2: value, ...})`. For example:
<!-- render:html --> <!-- render:html -->
...@@ -32,12 +32,55 @@ using as `.template({var1: value, var2: value, ...})`. For example: ...@@ -32,12 +32,55 @@ using as `.template({var1: value, var2: value, ...})`. For example:
<% }) %> <% }) %>
</template> </template>
<script> <script>
$('script.example').template({list: ['a', 'b', 'c']}) $('.example').template({list: ['a', 'b', 'c']})
</script> </script>
``` ```
To re-render the template, run `.template(data)` again with different data. To re-render the template, run `.template(data)` again with different data.
## $.template subtemplates
You can use sub-templates as follows:
<!-- render:html -->
```html
<template class="row" data-target="false">
<li class="<%- classes %>"><a href="<%- link %>"><%- text %></a></li>
</template>
<template class="main-template" data-template-item=".row">
<ul>
<%= item({classes: "active", link: '..', text: 'Parent'}) %>
<%= item({classes: "", link: '.', text: 'Current'}) %>
</ul>
</template>
<script>
$('.main-template').template()
</script>
```
`data-target="false"` ensures that the template `.row` is not rendered.
(This is typically used by sub-templates.)
`data-template-item=".row"` creates a function `item()` inside `.main-template`.
Calling `item()` renders `.roww` as a sub-template.
**Notes**:
- The sub-template `name` in `data-template-<name>` can only contain
lowercase letters, numbers and underscore.
- Sub-templates may themselves depend on, and call, other sub-templates.
- Sub-templates require a [Promise polyfill](https://www.npmjs.com/package/es6-promise) for IE.
Sub-templates can be from an [external source](#template-external-source) as
well. For example, this sub-template is loaded from `heading.html`:
```html
<template class="tmpl-head" src="heading.html" data-target="false"></template>
<template class="main-template" data-template-header=".tmpl-head">
<%= header({title: "Page title"}) %>
</template>
```
## $.template animation ## $.template animation
...@@ -58,7 +101,7 @@ For example, this shows a circle in SVG bouncing around smoothly. ...@@ -58,7 +101,7 @@ For example, this shows a circle in SVG bouncing around smoothly.
<script src="../../ui/morphdom/dist/morphdom-umd.min.js"></script> <script src="../../ui/morphdom/dist/morphdom-umd.min.js"></script>
<script type="text/html" data-engine="vdom" class="bouncing-ball"> <script type="text/html" data-engine="vdom" class="bouncing-ball">
<svg width="500" height="50"> <svg width="500" height="50">
<circle cx="<%= x %>" cy="<%= y %>" r="5" fill="red"/> <circle cx="<%= x %>" cy="<%= y %>" r="5" fill="red"></circle>
</svg> </svg>
</script> </script>
<script> <script>
...@@ -73,7 +116,7 @@ For example, this shows a circle in SVG bouncing around smoothly. ...@@ -73,7 +116,7 @@ For example, this shows a circle in SVG bouncing around smoothly.
You can also specify a `data-engine` via an option. For example: You can also specify a `data-engine` via an option. For example:
```js ```js
$('script.animate').template(data, {engine: 'vdom'}) $('.animate').template(data, {engine: 'vdom'})
``` ```
...@@ -91,7 +134,7 @@ once and apply them across the body. For example: ...@@ -91,7 +134,7 @@ once and apply them across the body. For example:
The same template is rendered in <%- heading %> The same template is rendered in <%- heading %>
</script> </script>
<script> <script>
$('script.targeted') $('.targeted')
.template({heading: 'panel 1'}, {target: '.panel1'}) .template({heading: 'panel 1'}, {target: '.panel1'})
.template({heading: 'panel 2'}, {target: '.panel2'}) .template({heading: 'panel 2'}, {target: '.panel2'})
</script> </script>
...@@ -117,7 +160,7 @@ is called, it appends rather than replaces. For example: ...@@ -117,7 +160,7 @@ is called, it appends rather than replaces. For example:
<li>New item #<%- n %> appended</li> <li>New item #<%- n %> appended</li>
</script> </script>
<script> <script>
$('script.list') $('.list')
.template({n: 1}) .template({n: 1})
.template({n: 2}) .template({n: 2})
</script> </script>
...@@ -133,7 +176,7 @@ append to an [existing target](#template-targets). For example: ...@@ -133,7 +176,7 @@ append to an [existing target](#template-targets). For example:
<!-- Every time .template() is called, the result is added as a list item here --> <!-- Every time .template() is called, the result is added as a list item here -->
</ul> </ul>
<script> <script>
$('script.list') $('.list')
.template({n: 1}, {append: true, target: '.existing-list'}) .template({n: 1}, {append: true, target: '.existing-list'})
.template({n: 2}, {append: true, target: '.existing-list'}) .template({n: 2}, {append: true, target: '.existing-list'})
</script> </script>
...@@ -147,7 +190,7 @@ Template containers can have an `src=` attribute that loads the template from a ...@@ -147,7 +190,7 @@ Template containers can have an `src=` attribute that loads the template from a
```html ```html
<script type="text/html" src="template.html" class="source"></script> <script type="text/html" src="template.html" class="source"></script>
<script> <script>
$('script.source').template() $('.source').template()
</script> </script>
``` ```
...@@ -166,7 +209,7 @@ For example: ...@@ -166,7 +209,7 @@ For example:
Data is <%= data %> Data is <%= data %>
</script> </script>
<script> <script>
$('script.missing').template({data: [1, 2, 3]}) $('.missing').template({data: [1, 2, 3]})
</script> </script>
``` ```
...@@ -195,7 +238,7 @@ You can also use the `selector: ...` option. For example: ...@@ -195,7 +238,7 @@ You can also use the `selector: ...` option. For example:
<script type="text/html" class="try no-render">This will not render</script> <script type="text/html" class="try no-render">This will not render</script>
<script type="text/html" class="try render">This will render</script> <script type="text/html" class="try render">This will render</script>
<script> <script>
$('script.try').template({}, {selector: '.render', target: '.selector-target'}) $('.try').template({}, {selector: '.render', target: '.selector-target'})
</script> </script>
``` ```
...@@ -211,12 +254,12 @@ relevant. ...@@ -211,12 +254,12 @@ relevant.
```js ```js
$.getJSON('data').then(function (data) { $.getJSON('data').then(function (data) {
// When we get data, show the dashboard, dispose any previous error // When we get data, show the dashboard, dispose any previous error
$('script.dashboard').template({ data: data }) $('.dashboard').template({ data: data })
$('script.error').template('dispose') $('.error').template('dispose')
}).fail(function (xhr, error, msg) { }).fail(function (xhr, error, msg) {
// If there's an error, dispose any previous dashboard, show the error // If there's an error, dispose any previous dashboard, show the error
$('script.dashboard').template('dispose') $('.dashboard').template('dispose')
$('script.error').template({ error: msg }) $('.error').template({ error: msg })
}) })
``` ```
...@@ -239,7 +282,7 @@ For example: ...@@ -239,7 +282,7 @@ For example:
<pre>Event e.templatedata = <span class="data">filled by event handler</span></pre> <pre>Event e.templatedata = <span class="data">filled by event handler</span></pre>
</script> </script>
<script> <script>
$('script.event') $('.event')
.on('template', function(e) { // When the template is rendered, .on('template', function(e) { // When the template is rendered,
e.target.find('.data') // find the .data class inside target nodes e.target.find('.data') // find the .data class inside target nodes
.html(JSON.stringify(e.templatedata)) // and enter the template data .html(JSON.stringify(e.templatedata)) // and enter the template data
......
import { findall } from './_util.js' import { findall } from './_util.js'
var _renderer = 'g1.template.render'
var _compiled = 'g1.template.compiled'
var _prev_created = 'g1.template.prev_created'
function subtemplates($main) {
// Takes a main template as a jQuery node with data-template-<name>="selector" attributes.
// Return an object of {name: selector}
return _.chain($main.data())
.pickBy(function (val, key) { return key.match(/^template/) })
.mapKeys(function (val, key) { return key.replace(/^template/, '').toLowerCase() })
.value()
}
export function template(data, options) { export function template(data, options) {
options = options || {} options = options || {}
var selector = options.selector || this.data('selector') || 'script[type="text/html"],template' var self = this
var selector = options.selector || self.data('selector') || 'script[type="text/html"],template'
// Pre-create the template rendering function // Pre-create the template rendering function
// Store this in .data('template.function') // Store this in .data('template.function')
findall(this, selector).each(function () { findall(self, selector).each(function () {
var $this = $(this) var $this = $(this)
// If we want to dispose the last target, just dispose it. // If we want to dispose the last target, just dispose it.
if (data === 'dispose') { if (data === 'dispose') {
...@@ -20,34 +35,59 @@ export function template(data, options) { ...@@ -20,34 +35,59 @@ export function template(data, options) {
}) })
} }
var renderer = $this.data(_renderer) var renderer = $this.data(_renderer)
// If there's no template function cached, cache it // If the renderer is already present, just use it. Else compile it
if (!renderer) { if (renderer)
var html = $this.html()
// Contents of script are regular strings. Contents of template are escaped
if (!$this.is('script'))
html = _.unescape(html)
var src = $this.attr('src')
if (src) {
// If the AJAX load succeeds, render the loaded template
// Else render the contents, with an additional xhr variable
$.get(src)
.done(function (contents) { make_template($this, contents, data, options) })
.fail(function (xhr) {
data.xhr = xhr
make_template($this, html, data, options)
})
} else
// If no src= is specified, just render the contents
make_template($this, html, data, options)
} else
// If the renderer is already present, just use it
renderer(data, options) renderer(data, options)
// If there are subtemplate dependencies, compile them
else if (_.size(subtemplates($this)))
dependent_templates($this, self, data, options)
// If there aren't, then compile immediately.
else
make_template($this, data, options)
}) })
return this return this
} }
var _renderer = 'template.render' function dependent_templates(selector, self, data, options) {
var _prev_created = 'template.prev_created' var uncompiled_selectors = _.pickBy(subtemplates(selector), function (sel) {
return !$(sel).data(_compiled)
})
return Promise.all(
_.map(uncompiled_selectors, function (sel) { return dependent_templates($(sel), self, data, options) })
).then(function () {
return Promise.all(
_.union(
_.map(uncompiled_selectors, function (sel) { return make_template($(sel), data, options) }),
make_template(selector, data, options)
)
)
})
.catch(function (error) { console.error(error) }) // eslint-disable-line no-console
}
function make_template($this, data, options) {
// Compile templates. If the template has src="", load it and then compile it.
// The result is in $this.data(_compiled) (via make_template_sync).
var html = $this.html()
// Contents of script are regular strings. Contents of template are escaped
if (!$this.is('script'))
html = _.unescape(html)
var src = $this.attr('src')
if (src) {
// If the AJAX load succeeds, render the loaded template
// Else render the contents, with an additional xhr variable
return $.get(src).done(function (html) {
make_template_sync($this, html, data, options)
}).fail(function (xhr) {
data.xhr = xhr
make_template_sync($this, html, data, options)
})
}
// If no src= is specified, just render the contents
else make_template_sync($this, html, data, options)
}
// Bind a template renderer to the node $this.data('template.render') // Bind a template renderer to the node $this.data('template.render')
// This renderer function accepts (data, options) and creates // This renderer function accepts (data, options) and creates
...@@ -56,11 +96,21 @@ var _prev_created = 'template.prev_created' ...@@ -56,11 +96,21 @@ var _prev_created = 'template.prev_created'
// - stores the target node in $this.data('template.target') // - stores the target node in $this.data('template.target')
// - triggers a template event (with .templatedata, .target) // - triggers a template event (with .templatedata, .target)
// - returns the target node // - returns the target node
function make_template($this, html, data, default_options) { function make_template_sync($this, html, data, default_options) {
var compiled_template = _.template(html) var compiled_template = _.template(html)
var $created var $created
// $this.data(_compiled) has the compiled template. This adds sub-template
// variables from data-template-* attributes.
$this.data(_compiled, function (subtemplate_data) {
subtemplate_data = _.extend(subtemplate_data || {}, _.mapValues(subtemplates($this), function (selector) {
return $(selector).data(_compiled)
}))
return compiled_template(subtemplate_data)
})
function renderer(data, options) { function renderer(data, options) {
html = compiled_template(data) html = $this.data(_compiled)(data)
// Get options. DOM data-* over-rides JS options // Get options. DOM data-* over-rides JS options
var append = $this.data('append') || (options ? options.append : default_options.append) var append = $this.data('append') || (options ? options.append : default_options.append)
var target = $this.data('target') || (options ? options.target : default_options.target) var target = $this.data('target') || (options ? options.target : default_options.target)
...@@ -69,7 +119,7 @@ function make_template($this, html, data, default_options) { ...@@ -69,7 +119,7 @@ function make_template($this, html, data, default_options) {
engine = template.engines[engine] || template.engines['default'] engine = template.engines[engine] || template.engines['default']
// If we're appending the contents, just add the text // If we're appending the contents, just add the text
if (append) { if (append) {
$created = $($.parseHTML(html)) $created = $($.parseHTML(html.trim()))
// If we're appending to a target node, just append to it. // If we're appending to a target node, just append to it.
if (target) if (target)
$(target).append($created) $(target).append($created)
...@@ -89,7 +139,7 @@ function make_template($this, html, data, default_options) { ...@@ -89,7 +139,7 @@ function make_template($this, html, data, default_options) {
return $created return $created
} }
$this.data(_renderer, renderer) $this.data(_renderer, renderer)
return renderer(data) return $this.data('target') !== false ? renderer(data) : null
} }
...@@ -107,7 +157,7 @@ template.engines = {} ...@@ -107,7 +157,7 @@ template.engines = {}
template.engines['default'] = template.engines['jquery'] = function ($this, target, html) { template.engines['default'] = template.engines['jquery'] = function ($this, target, html) {
// Parse the template output and create a node collection // Parse the template output and create a node collection
// $.parseHTML ensures that "hello" is parsed as HTML, not a selector // $.parseHTML ensures that "hello" is parsed as HTML, not a selector
var $target = $($.parseHTML(html)) var $target = $($.parseHTML(html.trim()))
// If target exists, replace the HTML. Otherwise, create new nodes before the template. // If target exists, replace the HTML. Otherwise, create new nodes before the template.
if (target) if (target)
$(target).html($target) $(target).html($target)
......
<div class="result">
<%= 'obj:' + JSON.stringify(obj) + ', shuny:'+ JSON.stringify(shuny) %>
</div>
<div class="sub-template">
<%= sub2async({life: life/2, shuny: 0 }) %>
</div>
<div class="result">
<%= 'Subtemplate2.html ::: obj:' + JSON.stringify(obj) + ', shuny:'+ JSON.stringify(shuny) %>
</div>
<!DOCTYPE html> <!DOCTYPE html>
<html> <html>
<head> <head>
<title>template tests</title> <title>template tests</title>
<script src="../node_modules/jquery/dist/jquery.min.js"></script> <script src="../node_modules/jquery/dist/jquery.min.js"></script>
<script src="../node_modules/lodash/lodash.min.js"></script> <script src="../node_modules/lodash/lodash.js"></script>
<script src="../node_modules/morphdom/dist/morphdom-umd.min.js"></script> <script src="../node_modules/morphdom/dist/morphdom-umd.min.js"></script>
<script src="../dist/g1.js"></script> <script src="../dist/g1.js"></script>
<script src="tape.js"></script> <script src="tape.js"></script>
<script src="tape-stream.js"></script> <script src="tape-stream.js"></script>
<style> <style>
circle, rect { transition: all 0.5s ease; } circle,
rect {
transition: all 0.5s ease;
}
</style> </style>
</head> </head>
<body> <body>
<h1>Lodash templates</h1> <h1>Lodash templates</h1>
<script type="text/html" id="t1">Your platform is <%= navigator.userAgent %></script> <template id="t1">Your platform is <%= navigator.userAgent %></template>
<script> <script>
function strip(text) { function strip(text) {
return text.replace(/^\s+/, '').replace(/\s+$/, '') return text.replace(/^\s+/, '').replace(/\s+$/, '')
...@@ -23,7 +28,7 @@ ...@@ -23,7 +28,7 @@
var node = target.get(0) var node = target.get(0)
return node ? strip(node.textContent) : '' return node ? strip(node.textContent) : ''
} }
tape('$().template() renders plain text with variables', function(t) { tape('$().template() renders plain text with variables', function (t) {
$('#t1') $('#t1')
.one('template.g1', function (e) { .one('template.g1', function (e) {
t.equal(e.target.length, 1) t.equal(e.target.length, 1)
...@@ -35,16 +40,16 @@ ...@@ -35,16 +40,16 @@
t.equal(text(e.target), 'Your platform is abc') t.equal(text(e.target), 'Your platform is abc')
t.end() t.end()
}) })
.template({navigator: {userAgent: 'abc'}}) .template({ navigator: { userAgent: 'abc' } })
}) })
</script> </script>
<div id="t2"> <div id="t2">
<script type="text/html"> <template>
<% list.forEach(function(item) { %> <% list.forEach(function(item) { %>
<div><%= item %></div> <div><%= item %></div>
<% }) %> <% }) %>
</script> </template>
</div> </div>
<script> <script>
tape('$().template(data) passes data to the template', function (t) { tape('$().template(data) passes data to the template', function (t) {
...@@ -57,7 +62,7 @@ ...@@ -57,7 +62,7 @@
t.equal($divs.length, list.length, 'Correct number of nodes are created') t.equal($divs.length, list.length, 'Correct number of nodes are created')
var text = $divs.map(function () { return this.innerHTML }).get().join(' ') var text = $divs.map(function () { return this.innerHTML }).get().join(' ')
t.equal(text, list.join(' '), 'Template content is correct') t.equal(text, list.join(' '), 'Template content is correct')
t.deepEqual(e.templatedata, {list: list}) t.deepEqual(e.templatedata, { list: list })
$divs.attr('class', 'my-unique-item') $divs.attr('class', 'my-unique-item')
t.equal($('.my-unique-item').length, 3, 'Repeated calls over-write the same node') t.equal($('.my-unique-item').length, 3, 'Repeated calls over-write the same node')
}).template({ list: list }) }).template({ list: list })
...@@ -65,11 +70,11 @@ ...@@ -65,11 +70,11 @@
}) })
</script> </script>
<script type="text/html" id="t3" src="sample-template.html"></script> <template id="t3" src="sample-template.html"></template>
<script> <script>
tape('$().template() renders src= via AJAX load', function(t) { tape('$().template() renders src= via AJAX load', function (t) {
var data = { data: ['x', 'y'] } var data = { data: ['x', 'y'] }
$('#t3').one('template', function(e) { $('#t3').one('template', function (e) {
t.equal(text(e.target), data.data.join(' ')) t.equal(text(e.target), data.data.join(' '))
t.deepEqual(e.templatedata, data) t.deepEqual(e.templatedata, data)
t.end() t.end()
...@@ -77,13 +82,13 @@ ...@@ -77,13 +82,13 @@
}) })
</script> </script>
<script type="text/html" id="t4" src="nonexistent.html"> <template id="t4" src="nonexistent.html">
<%= xhr.status %>: Not found. <%= data.join(' ') %> <%= xhr.status %>: Not found. <%= data.join(' ') %>
</script> </template>
<script> <script>
tape('$().template() renders contents if src= returns an error', function (t) { tape('$().template() renders contents if src= returns an error', function (t) {
var data = { data: ['x', 'y'] } var data = { data: ['x', 'y'] }
$('#t4').one('template', function(e) { $('#t4').one('template', function (e) {
t.equal(text(e.target), '404: Not found. x y') t.equal(text(e.target), '404: Not found. x y')
t.deepEqual(e.templatedata.data, data.data) t.deepEqual(e.templatedata.data, data.data)
t.equal(e.templatedata.xhr.status, 404) t.equal(e.templatedata.xhr.status, 404)
...@@ -92,34 +97,34 @@ ...@@ -92,34 +97,34 @@
}) })
</script> </script>
<script type="text/html" id="t5"> <template id="t5">
<%= heading %> <%= heading %>
</script> </template>
<div class="dashboard1">has <em>old content</em></div> <div class="dashboard1">has <em>old content</em></div>
<div class="dashboard2">has <em>old content</em></div> <div class="dashboard2">has <em>old content</em></div>
<script> <script>
tape('$().template(data, target) renders template to any DOM element', function(t) { tape('$().template(data, target) renders template to any DOM element', function (t) {
$('#t5') $('#t5')
.one('template', function(e) { .one('template', function (e) {
t.equal(text(e.target), 'Dashboard 1', 'renders with data') t.equal(text(e.target), 'Dashboard 1', 'renders with data')
t.equal(strip($('.dashboard1').html()), 'Dashboard 1', 'renders to target') t.equal(strip($('.dashboard1').html()), 'Dashboard 1', 'renders to target')
}) })