Documentation
article
header
h1 "{{ title }}"
p.articleParagraph
"{{ text }}"
<script> tag.
<script id="example-template" type="text/x-ist">
article
header
h1 "{{ title }}"
p.articleParagraph
"{{ text }}"
</script>
ist() with the content of this tag, or you can use
ist.script().
var template = ist($("#example-template").html());
var template = ist.script("example-template");
ist().
var template = ist('h1 "{{ title }}"');
template in the examples above is called a compiled template.
Passing a context object to the render() method of a compiled template renders
it into a DOM node tree.
var context = {
title: "ist.js released",
text: "ist.js has just been released"
};
var node = template.render(context);
document.body.appendChild(node);
render()
to render nodes into an other document. This is mainly useful when using
multiple windows or frames.
var popup = window.open();
var node = template.render(context, popup.document);
popup.document.body.appendChild(node);
@ symbol and
take an expression as parameter.
ul.menu
@if isAdmin
li
a.adminZone "Administration zone"
@each menuItems
li
a[href={{ url }}] "{{ label }}"
var context = {
isAdmin: true,
menuItems: [
{ url: "home.html", label: "Home" },
{ url: "news.html", label: "News" },
{ url: "contact.html", label: "Contact" }
]
};
document.body.appendChild(menuTemplate.render(context));
ul.menu
/* Only display admin link if user is an administrator */
@if isAdmin
li
a.adminZone "Administration zone"
@each menuItems
li
a[href={{ url }}] "{{ label }}"
update() method.
var context = {
isAdmin: true,
menuItems: [
{ url: "home.html", label: "Home" },
{ url: "news.html", label: "News" },
{ url: "contact.html", label: "Contact" }
]
};
var rendered = menuTemplate.render(context);
document.body.appendChild(rendered);
context.isAdmin = false;
context.menuItems.push({ url: "shop.html", label: "Shop" });
rendered.update();
ist.create().
var myLink = ist.create("a.link[href=#]");
<script> tag, ist.js registers as window.ist (or
just ist).
var template = ist('h1 "{{ title }}"');
noConflict() method to restore the previous value of
window.ist if necessary.
var istjs = ist.noConflict();
// window.ist is now back to its previous value
require(['ist'], function(ist) {
var template = ist('h1 "{{ title }}"'),
node = template.render({
title: "Title"
});
document.body.appendChild(node);
});
.ist extension and directly load the compiled template using the plugin
syntax. Note that ist.js adds the extension automatically.
require(['ist!path/to/template'], function(template) {
var node = template.render({
title: "Title"
});
document.body.appendChild(node);
});
ist() with a template string compiles it and returns the compiled
template.
var compiled = ist('h1 "{{ title }}"');
<script> tag are compiled when you get them using
ist.script().
<script id="example-template" type="text/x-ist">
article
h1 "{{ title }}"
p "{{ text }}"
</script>
<script>
var compiled = ist.script("example-template");
</script>
ist! AMD plugin, what you get are also
compiled templates.
define(['ist!path/to/template'], function(compiledTemplate) {
/* ... */
});
render() method. When
your template uses expressions, you can pass a context object to
use as an argument.
var compiled = ist('h1 "{{ title }}"'),
rendered = compiled.render({ title: "Hello, world !" });
var popup = window.open(),
compiled = ist('h1 "{{ title }}"'),
rendered = compiled.render(
{ title: "Hello, world !" },
popup.document
);
render() is a DOM DocumentFragment. If you are not familiar
with them, a DocumentFragment is a transparent container node that can contain
child nodes as any other DOM node, but that disappears once you insert it in an
other node.
<!-- DocumentFragment content -->
<h1>Hello, world !</h1>
<!-- Document content -->
<div id="container"></div>
<!-- After calling container.appendChild(fragment): -->
<div id="container">
<!-- The fragment still exists, but outside of
the document, and it is now empty -->
<h1>Hello, world !</h1>
</div>
render() returns into your document.
var container = document.querySelector("#container"),
compiled = ist('h1 "{{ title }}"'),
rendered = compiled.render({ title: "Hello, world !" });
container.appendChild(rendered);
render() has an additional update() method.
If you keep a reference to the fragment even after having inserted it in your
document, you can use this method to update the rendered nodes by passing it
an updated context object.
var container = document.querySelector("#container"),
compiled = ist('h1 "{{ title }}"'),
rendered = compiled.render({ title: "Hello, world !" });
container.appendChild(rendered);
// <h1>Hello, world !</h1>
rendered.update({ title: "My Web App" });
// <h1>My Web App</h1>
update() without any argument, in which case it will reuse
the same context object.
var container = document.querySelector("#container"),
compiled = ist('h1 "{{ title }}"'),
context = { title: "Hello, world !" },
rendered = compiled.render(context);
container.appendChild(rendered);
// <h1>Hello, world !</h1>
context.title = "My Web App";
rendered.update();
// <h1>My Web App</h1>
div.parent
div.child
div.grandchild
div.child
div.parent
div.child
div.grandchild
div.grandchild
div.invalid
div.invalid
div.parent
div.child
div.child
div.parent
div.child
div.child
/* Comment */
div.parent
div.child /* Comment */
div.grandchild
/* Multi-line
comment */
/* Error-prone
comment
*/ div.child
article
header
h1
ul.menu
li#item1
a
ul
li
a[href=index.html]
. prefix. You can also specify nested property paths (ist.js will
create intermediate objects if necessary).
div[.className=header]
div[.path.to.property=value]
ul.menu.header
li
a.menuitem#item1[href=index.html]
<div>s, ist.js allows omitting the tag name in
selectors. Of course you will need at least one qualifier.
.implicitDiv
#implicitDiv
[implicit=yes]
.div-with-very-long-selector-qualifier-list#and-long-id \
[and-long-attribute-list=value1][and-long-attribute-list-2=value2] \
[and-long-attribute-list-3=value3][and-long-attribute-list-4=value4]
h1
"Title"
h2
'Subtitle'
h1 "Title"
h2 'Subtitle'
div "Text content"
div.child
div
"Text content"
div.child
h1
"you 'can' include \"escaped\" ch\x41ra\u0043ters"
h2
'you \'can\' include "escaped" ch\x41ra\u0043ters'
ul.links
li
a[href={{ url }}] "{{ label }}"
article[style=color: {{ read ? "blue" : "red" }}]
h1
"{{ title.toUpperCase() }}"
h2
"{{ \"don't forget escaping quotes\" }}"
"{{ path.to.nested.property }}"
this in expressions, and thus
these two examples are equivalent.
h1 "{{ title }}"
h1 "{{ this.title }}"
this keyword is mainly useful to access properties that are not valid
identifiers, using the square bracket notation.
"{{ this[12] }}"
"{{ this['weird property name'] }}"
"{{ this[\"typeof\"] }}"
ist.global("name", "value").
ist.global("upper", function(text) {
return text.toUpperCase();
});
"{{ upper('will be uppercased') }}"
div[class={{ cssClass }}]
div[.className={{ cssClass }}]
div[id={{ id }}]
ul#menu
@each menu
li[!click=action]
"{{ label }}"
myTemplate.render({
menu: [
{
label: "about",
action: function() {
alert("About this application");
}
},
{
label: "quit",
action: function() {
location.href = "/";
}
}
]
});
parameter can be any
ist.js expression. Note that expressions don't need braces when used with
directives.
You can also define your own directives.
div.parent
@directive parameter
div.subtemplate
@if directive to toggle rendering of a section of template
depending on the value of an expression.
@if user.isAdmin
a[href=admin.html] "Administration zone"
@unless directive has the same goal, just reversed.
@unless user.isRegistered
a[href=register.html] "Sign up now !"
@else directive can be used just after a @if or @unless directive to
match the opposite condition.
@if user.isAdmin
a[href=admin.html] "Administration zone"
@else
"No admin zone for you :("
@with directive allows context switching. When accessing the same part of
the context repeatedly, you may want to use it to make the template more
readable.
div.userData
a[href=/userpanel/{{ user.id }}] "{{ user.name }}"
div.userData
@with user
a[href=/userpanel/{{ id }}] "{{ name }}"
@with directive can also be used to hard-code some parts of the template.
@with { version: '0.5.4', built: '2012-11-20' }
footer
"ist.js version {{ version }} built on {{ built }}"
@each directive allows rendering a section of template repeatedly for each
element of an array. For every iteration, the rendering context is switched to
a new element in the array.
ul.menu
@each menu
a[href={{ url }}] "{{ label }}"
var context = {
menuItems: [
{ url: "home.html", label: "Home" },
{ url: "news.html", label: "News" },
{ url: "contact.html", label: "Contact" }
]
};
document.body.appendChild(menuTemplate.render(context));
@each directive, the special loop variable is set to an object
with details about the iteration.
loop.index is the 0-based index of the rendered item in the arrayloop.length is the size of the arrayloop.first is true when loop.index === 0loop.last is true when loop.index === loop.length - 1loop.outer is a reference to the outer context object
@each array
@if loop.first
"First item"
"Item {{ loop.index + 1 }} of {{ loop.length }}"
" is {{ this }}"
@if loop.last
"Last item"
loop variable hides any loop property on the rendering context, but you
can still access it using this.
@each array
"Item.loop = {{ this.loop }}"
@eachkey directive is very similar to the @each directive, but it loops
over an objects own properties, setting the variables key and value.
@eachkey { home: "home.html", news: "news.html" }
a[href={{ value }}]
"{{ key }}"
@eachkey, the loop variable is set to an object with details
about the iteration.
loop.index is the 0-based index of the rendered item in the arrayloop.length is the size of the arrayloop.first is true when loop.index === 0loop.last is true when loop.index === loop.length - 1loop.outer is a reference to the outer context objectloop.object is a reference to the enumerated object
@define and @use directives can be used to define components (or what
other templating engines also call macros). This enables reusing parts of
templates. To define a component, use the @define directive with the
component name as a parameter.
@define "article"
.article
h1 "{{ title }}"
.content "{{ text }}"
@use
directive.
/* articles should be an array of objects with title and text properties */
@each articles
@use "article"
@use-ing it, but you can use the @with directive to achieve the same.
@with { title: "My article", text: "Hello, world !" }
@use "article"
@include directive enables including an other template that will be
rendered using the current rendering context. When using <script> tags, you
can pass an ID as a string to the @include directive. Note that the order of
definition of <script> tags does not matter.
<script type="text/x-ist" id="menu">
ul#menu
@each items
@include "menu-item"
</script>
<script type="text/x-ist" id="menu-item">
li
a[href={{ url }}] "{{ label }}"
</script>
<script type="text/javascript">
function renderMenu() {
ist.script("menu").render([
{ label: "Home", url: "index.html" },
{ label: "News", url: "news.html" },
{ label: "Contact", url: "contact.html" }
});
}
</script>
ist! plugin syntax. Simply pass their relative
path as a string parameter to @include. You can omit the .ist extension.
/* templates/main.ist */
@include "common/header"
section#main
"Main content, yay!"
@include "common/footer.ist" /* Extension is optional */
/* templates/common/header.ist */
header
h1 "My Website"
/* templates/common/footer.ist */
footer
"Copyright (c) 2012 My Company"
ist! AMD plugin, @included templates
are automatically loaded as dependencies.
require(['ist!templates/main'], function(mainTemplate) {
/* templates/common/{header,footer} are added as AMD
dependencies to 'ist!templates/main', and thus
are loaded automatically */
mainTemplate.render(/* ... */);
});
<script> tag however, any
@included template must be either an other <script> tag ID, or an already
loaded AMD module name.
<script type="text/x-ist" id="main">
@include "included-template"
</script>
define("included-template", ["ist!some/template"], function(tmpl) {
return tmpl;
});
require(["ist", "included-template"], function(ist) {
ist.script("main").render(/* ... */);
});
define("included-template", [], function() {
return "div\n h1 'included content'";
});
require(["ist", "included-template"], function(ist) {
ist.script("main").render(/* ... */);
});
@each directive
that you need to update.
Partials are declared with the !name notation next to an element.
div.liveTweets
@each tweets
/* Tweet partial */
div.tweet !tweet
span.author
"@{{ author }}"
span.text
"{{ text }}"
They can be accessed using the partial() method of a compiled template, which
takes the partial name as argument. It returns the first matching partial,
which can be manipulated as any other compiled template.
var myTemplate = ist(...);
function initialRender(tweets) {
document.body.appendChild(
myTemplate.render({ tweets: tweets });
);
}
function addNewTweet(author, text) {
var container = document.querySelector(".liveTweets"),
partial = myTemplate.partial("tweet");
container.appendChild(
partial.render({ author: author, text: text })
);
}
ist.create() and pass it an
element selector.
var myDiv = ist.create(
"div.class#id[attribute=Value]"
);
var myDiv = ist.create(
"div[class={{ cls }}]",
{ cls: 'myclass' }
);
ist.create() is also able to create several nodes at once using a CSS-like
angle-bracket syntax.
var myParentDiv = ist.create(
'div.parent > div.child > "{{ text }}"',
{ text: "Text node content" }
);
Please note however that create has a quite naive angle-bracket parser, and as
such does not support angle brackets anywhere else than between nodes.
Therefore you should only use it for trivial node tree creation.
var myParentDiv = ist.create(
'div.parent > @each children > "{{ name }}"',
{
children: [
{ name: 'alice' },
{ name: 'bob' }
]
}
);
var popupDiv = ist.create(
'div.inPopup',
{},
popup.document
);
A directive helper is registered by calling ist.helper() and passing it a
directive name (case-sensitive) and a helper function.
/* Helper for '@foo' */
ist.helper("foo", function(context, value, template, fragment) {
/* Do stuff */
});
context: a Context object for the outer rendering
context, ie. the value of the rendering context where the directive is called;value: the value that is passed as an argument to the directive; this
argument is undefined when no parameter is passed;template: an ist compiled template that enables access to nodes defined as
children of the directive;fragment: a DocumentFragment where the directive should render nodes. When
updating a previously rendered template, it contains all nodes the directive
rendered; otherwise it is empty.Here are some example calls to @foo with the parameters the helper receives:
@with { hello: "world" }
/* @foo called with a parameter, the handler receives:
- context: a Context object for { hello: "world" }
- value: "world"
- template: a compiled ist template for
div.childA
div.childB
*/
@foo hello
div.childA
div.childB
/* @foo called without a parameter, the handler receives:
- context: a Context object for { hello: "world" }
- value: undefined
- template: a compiled ist template for
div.child
div.grandChild
*/
@foo
div.child
div.grandChild
document or any of
its methods, as you don't know which DOM document your directive will be used
to create nodes in. You can use helper properties and methods of the context
argument instead.
/* Always create a text node containing the value passed as argument, ignoring
children template nodes */
ist.helper(
"echo",
function(context, value, template, fragment) {
var node = context.createTextNode(
value || "no value passed to @echo !"
);
fragment.appendChild(node);
}
);
fragment argument to directive helpers is
empty. However, when updating, it contains previously rendered nodes. When the
helper returns, whatever the fragment contains will be added to the rendered
node tree. It is up to directive helpers to decide what to do based on the
fragment contents.
A naive approach to handling node updates is to just empty the fragment and
render nodes in all cases. This approach works, but it is not very efficient.
Here is how the @echo directive above could handle updates using this
approach.
ist.helper(
'echo',
function(context, value, template, fragment) {
/* Empty the fragment first */
while (fragment.hasChildNodes()) {
fragment.removeChild(fragment.firstChild);
}
/* Render node */
var node = context.createTextNode(
value || "no value passed to @echo !"
);
fragment.appendChild(node);
}
);
ist.helper(
'echo',
function(context, value, template, fragment) {
var node = fragment.firstChild;
if (!node) {
/* No previously rendered node available */
node = context.createTextNode("");
fragment.appendChild(node);
}
/* Update node content */
node.textContent = value || "no value passed to @echo !";
}
);
update()
method when updating. Therefore they need to save the rendered fragment
returned by calling render() and retrieve it when updating. The fragment
argument to helpers comes with two additional methods to help with that:
fragment.appendRenderedFragment(rendered) appends the contents from
rendered (just like calling fragment.appendChild(rendered)) to the
fragment, but also saves rendered itself for later retrieval.fragment.extractRenderedFragment() looks for a previously saved rendered
fragment, and returns it. When no rendered fragment has been saved, it
returns undefined. Otherwise, all previously rendered nodes are removed
from fragment and returned in the resulting rendered fragment. In that
case, you will need to re-append the nodes to fragment after updating.Here is an example showing how this works with the @with directive.
ist.helper(
'with',
function(context, value, template, fragment) {
/* Retrieve previously saved rendered fragment */
var rendered = fragment.extractRenderedFragment();
if (rendered) {
/* A rendered fragment was found, update it */
rendered.update(value);
} else {
/* Nothing was saved, render the template */
rendered = template.render(value);
}
/* Put nodes back in the fragment */
fragment.appendRenderedFragment(rendered);
}
);
appendRenderedFragment and extractRenderedFragment to
distinguish them. Keys can be any Javascript value.
Here is an example of a doubleWith directive that renders its inner template
twice and is able to update correctly.
ist.helper(
'doubleWith',
function(context, value, template, fragment) {
var first = fragment.extractRenderedFragment("first"),
second = fragment.extractRenderedFragment("second");
if (first) {
first.update(value);
} else {
first = template.render(value);
}
if (second) {
second.update(value);
} else {
second = template.render(value);
}
fragment.appendRenderedFragment(first, "first");
fragment.appendRenderedFragment(second, "second");
}
);
@with.
ist.helper(
"with",
function(context, value, template, fragment) {
if (!value) {
throw new Error("No data passed to @with");
}
/* Retrieve previously saved rendered fragment */
var rendered = fragment.extractRenderedFragment();
if (rendered) {
/* A rendered fragment was found, update it */
rendered.update(value);
} else {
/* Nothing was saved, render the template */
rendered = template.render(value);
}
/* Put nodes back in the fragment */
fragment.appendRenderedFragment(rendered);
}
);
Context objects to encapsulate rendering contexts as
well as the DOM document where templates are rendered. The following members
give access to the DOM document where the template is being rendered:
Context.doc is a reference to the DOM document where the template is being
rendered;Context#createDocumentFragment() is an alias to the same method of the
target document;Context#createTextNode(text) is an alias to the same method of the target
document;Context#createElement(tagName[, namespace]) is an alias to either
createElement or createElementNS on the target document.Context#importNode(node) is an alias to the same method of the target
document
/* Creates a <div> with a text child node containing
the value passed as a parameter, ie @divtext "foo"
renders to <div>foo</div>.
Note: for the sake of simplicity, this example does
not handle updates */
ist.helper(
"divtext",
function(context, value, template, fragment) {
var div = context.createElement("div");
var text = context.createTextNode(value);
div.appendChild(text);
fragment.appendChild(div);
}
);
Context.value contains the value wrapped by the Context object.Context#createContext(newValue) returns a new Context object with the same
target document but a new value.
ist.helper(
'test',
function(context, value, template, fragment) {
var testValue = { foo: "bar" },
ctx = context.createContext(testValue);
assert(testValue === ctx.value);
}
);
Context#pushScope(scope) pushes properties of the scope object on the
scope used when evaluating expressions, possibly hiding previously existing
variables with the same name (from previously pushed scopes of from the
rendering context).Context#popScope() undoes what pushScope did, popping the last pushed scope
object.Context#pushValue(value) (documentation pending)
ist.helper(
'test',
function(outer, inner, template, fragment) {
var ctx = outer.createContext({ foo: "bar" });
assert(ctx.evaluate("foo.toUpperCase()") === "BAR");
ctx.pushScope({ foo: "baz", hello: "world" });
assert(ctx.evaluate("foo.toUpperCase()") === "BAZ");
assert(ctx.evaluate("hello") === "world");
ctx.pushScope({ foo: "ding" });
assert(ctx.evaluate("foo.toUpperCase()") === "DING");
assert(ctx.evaluate("hello") === "world");
ctx.popScope();
assert(ctx.evaluate("foo.toUpperCase()") === "BAZ");
assert(ctx.evaluate("hello") === "world");
ctx.popScope();
assert(ctx.evaluate("foo.toUpperCase()") === "BAR");
}
);