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

ENH: .template() supports sub-templates

parent 9900de27
# $.template
`$.template()` renders HTML templates with JavaScript mixed inside them.
`$(selector).template()` renders HTML templates with JavaScript mixed inside them.
Example:
......@@ -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
`<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:
<!-- render:html -->
......@@ -32,12 +32,55 @@ using as `.template({var1: value, var2: value, ...})`. For example:
<% }) %>
</template>
<script>
$('script.example').template({list: ['a', 'b', 'c']})
$('.example').template({list: ['a', 'b', 'c']})
</script>
```
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
......@@ -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 type="text/html" data-engine="vdom" class="bouncing-ball">
<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>
</script>
<script>
......@@ -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:
```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:
The same template is rendered in <%- heading %>
</script>
<script>
$('script.targeted')
$('.targeted')
.template({heading: 'panel 1'}, {target: '.panel1'})
.template({heading: 'panel 2'}, {target: '.panel2'})
</script>
......@@ -117,7 +160,7 @@ is called, it appends rather than replaces. For example:
<li>New item #<%- n %> appended</li>
</script>
<script>
$('script.list')
$('.list')
.template({n: 1})
.template({n: 2})
</script>
......@@ -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 -->
</ul>
<script>
$('script.list')
$('.list')
.template({n: 1}, {append: true, target: '.existing-list'})
.template({n: 2}, {append: true, target: '.existing-list'})
</script>
......@@ -147,7 +190,7 @@ Template containers can have an `src=` attribute that loads the template from a
```html
<script type="text/html" src="template.html" class="source"></script>
<script>
$('script.source').template()
$('.source').template()
</script>
```
......@@ -166,7 +209,7 @@ For example:
Data is <%= data %>
</script>
<script>
$('script.missing').template({data: [1, 2, 3]})
$('.missing').template({data: [1, 2, 3]})
</script>
```
......@@ -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 render">This will render</script>
<script>
$('script.try').template({}, {selector: '.render', target: '.selector-target'})
$('.try').template({}, {selector: '.render', target: '.selector-target'})
</script>
```
......@@ -211,12 +254,12 @@ relevant.
```js
$.getJSON('data').then(function (data) {
// When we get data, show the dashboard, dispose any previous error
$('script.dashboard').template({ data: data })
$('script.error').template('dispose')
$('.dashboard').template({ data: data })
$('.error').template('dispose')
}).fail(function (xhr, error, msg) {
// If there's an error, dispose any previous dashboard, show the error
$('script.dashboard').template('dispose')
$('script.error').template({ error: msg })
$('.dashboard').template('dispose')
$('.error').template({ error: msg })
})
```
......@@ -239,7 +282,7 @@ For example:
<pre>Event e.templatedata = <span class="data">filled by event handler</span></pre>
</script>
<script>
$('script.event')
$('.event')
.on('template', function(e) { // When the template is rendered,
e.target.find('.data') // find the .data class inside target nodes
.html(JSON.stringify(e.templatedata)) // and enter the template data
......
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) {
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
// Store this in .data('template.function')
findall(this, selector).each(function () {
findall(self, selector).each(function () {
var $this = $(this)
// If we want to dispose the last target, just dispose it.
if (data === 'dispose') {
......@@ -20,34 +35,59 @@ export function template(data, options) {
})
}
var renderer = $this.data(_renderer)
// If there's no template function cached, cache it
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
// If the renderer is already present, just use it. Else compile it
if (renderer)
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
}
var _renderer = 'template.render'
var _prev_created = 'template.prev_created'
function dependent_templates(selector, self, data, options) {
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')
// This renderer function accepts (data, options) and creates
......@@ -56,11 +96,21 @@ var _prev_created = 'template.prev_created'
// - stores the target node in $this.data('template.target')
// - triggers a template event (with .templatedata, .target)
// - 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 $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) {
html = compiled_template(data)
html = $this.data(_compiled)(data)
// Get options. DOM data-* over-rides JS options
var append = $this.data('append') || (options ? options.append : default_options.append)
var target = $this.data('target') || (options ? options.target : default_options.target)
......@@ -69,7 +119,7 @@ function make_template($this, html, data, default_options) {
engine = template.engines[engine] || template.engines['default']
// If we're appending the contents, just add the text
if (append) {
$created = $($.parseHTML(html))
$created = $($.parseHTML(html.trim()))
// If we're appending to a target node, just append to it.
if (target)
$(target).append($created)
......@@ -89,7 +139,7 @@ function make_template($this, html, data, default_options) {
return $created
}
$this.data(_renderer, renderer)
return renderer(data)
return $this.data('target') !== false ? renderer(data) : null
}
......@@ -107,7 +157,7 @@ template.engines = {}
template.engines['default'] = template.engines['jquery'] = function ($this, target, html) {
// Parse the template output and create a node collection
// $.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)
$(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>
This diff is collapsed.
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment