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 === 0
loop.last
is true when loop.index === loop.length - 1
loop.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 === 0
loop.last
is true when loop.index === loop.length - 1
loop.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, @include
d 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
@include
d 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");
}
);