domt -- simple DOM Templating tool
Merge plain javascript objects into DOM.
Support following workflow :
add template attributes like bind, repeat, or [acc.sess.ors] to static
html document
load document, merge data into it using those attributes and fields
unload document (serialize it)
reload document later and merge new data
And stick with these constraints :
light (5.8KB minified)
simple
thorough test suites
force separation between logic (js), template (html), and data (obj)
pure js and DOM, cross browsers (well, hmm, mostly)
extensible with filters
Quick start
Starting with a template and a call to Domt:
- first item
Domt('#test').merge({
"class": "list",
items: [
{color: "blue", text: "the sea"},
{color: null, text: "the void"}
]
});
We get:
- the sea
- the void
The API is chainable and calling it a second time is possible:
Domt('#test').merge({
"class": "list"
}).merge({
items: [
{color: "blue", text: "the sea"},
{color: null, text: "the void"}
]
});
One can create another instance of Domt to update a previously merged node:
Domt('#test').merge({
"class": null
}).merge({
items: [
{color: "red", text: "another one"}
]
});
The second call with remove attribute "class" from #test, and append a
list item named "another one".
It is also possible to empty the repeated lists:
Domt('#test').empty().merge({
items: [
{color: "red", text: "another one"}
]
});
Domt accepts a jquery object
Domt($('div > .test').not('.out'))
Domt accepts a documentFragment
Domt(myTemplateNode.content).merge(data).nodes == myTemplateNode.content
though in this case one typically would prefer to clone the content fragment.
Attributes
bind
replace current context data with the data found by the accessor in the value.
If empty, used as a way to make sure the node will be merged.
bind-xxx
will create a xxx attribute (or merge it if already present)
Several attributes are search by default. To add more, just do
Domt.query.push('xxx') so that a node having a bind-xxx attribute will
be found if the only recognizable attribute is this one.
repeat
repeat-with
See below.
repeat-name
name a repeated fragment, so that Domt.template(name) return its template.
The fragment must have been merged once before.
Usage: Domt.template('myfrag').clone().merge(data).
Which DOM nodes are processed ?
Attributes "bind-*" that are not descendents of a node with a "repeat"
attribute are not processed unless they have a "bind" (empty or not)
attribute.
Example: Domt('body').merge({myval: 'test'}); won't merge the span.
Either call Domt on that span, or add a bind attribute to it.
the target given as argument of Domt(target)
target is either a node, a list of nodes, or a selector expression
supported by document.querySelectorAll.
instance.nodes keeps the actual list.
the descendants with attribute "bind"
in itself the node doesn't change, the "bind" attributes set the current
base path for all accessors in the same node or its descendants.
A "bind" attribute is always processed before a "repeat" attribute when
they are on the same node.
For example , or
is a value filter like function(val) { return val.slice(-2); }.
the descendants with attributes starting with "bind-"
The attribute value is an object accessor of the form "path.to.key",
and it can be empty (in which case parent object is used).
If a value is :
undefined or null, obj isn't changed
string, path is used as an accessor of obj
function, obj is the result of fun(obj, paths)
the descendants with attribute "repeat"
repeats the node (and its content) by iterating over accessed data.
If a value is :
undefined or null, obj isn't changed
string, path is used as an accessor of obj
function, obj is the result of fun(obj, paths)
Current item has two special properties: #key and #val.
Accessed data can be iterated if it is :
an array
an object with length property and .item(i) method
finally the object is looped as an hash array
the next siblings with attribute "repeat-with" of a node with attribute "repeat"
this allows repeating several nodes at once, as a fragment.
When doing this, the block filter context.node argument is a fragment.
Operations on instances
The actual data merging is controled by these attributes:
bind-/attributeName/
where attribute is the name of the attribute to process.
bind-text
node.innerHTML = escaped_text(value)
bind-html
node.innerHTML = value
If a value is :
undefined, expressions in the target are evaluated
null, target is removed
string, target is replaced by the value accessed in obj
function, the value is replaced by the result of fun(obj, paths)
When merged, a repeated node looses its bind and repeat attributes (but
doesn't remove attributes added by a "bind-bind-att" trick) and
non-repeated nodes keep their bind-* attributes.
Expressions are written as "[path.to.val|optional_filter]" and are
replaced by their accessed value in the target.
A node is repeated in natural order. To invert that order, just add a block
filter like 'invert'. A custom block filter can sort items in any desired order.
- first item
Note that if node contains other nodes that are targets of Domt,
the usage of bind-html or bind-text on that parent node is not defined.
It is strongly advised to avoid that situation:
[data.text]
link
Instead, wrap the first text node in a span:
[data.text]
link
Global settings
Domt.ns object is used to set the prefixes used for the attribute names,
or change delimiters, like this: Domt.ns.expr = '{{*}}';.
It's also possible to change the "bind-" prefix and other names, just have
a look in Domt's code and tests to find out about Domt.ns.
Domt.ns.query holds a list of bind-* attributes to search for,
those special ones do not need "repeat" or "bind" to be set at all.
As for filters, query options can be passed in Domt(node, {query: ["size"]});,
which will search for nodes having a bind-size attribute.
Note that all attributes of a node are checked when there is one bind, repeat,
or bind-* attribute, so typically
bothe attributes are merged correctly.
Domt.filters stores filters prototype (shared by all domt.filters instances).
To add per-instance filters, use either
Domt(parent, {
myFilter: function(str) {
return '' + this.text(str) + '';
}
});
or
var inst = Domt(parent);
inst.filters.myFilter = function(str) {...};
or even
var inst = Domt(parent);
inst.merge(data, {
myBlockFilter: function(item, key, context) {...}
});
In a filter function, the first argument is always the data being merged,
the last argument an object giving some access to current context, see below.
Data getters
As mentioned above, merging a function will call it with parameters
(obj, paths). This cannot be used to modify the DOM, only the data.
Value Filters
Value filters are called before merging the value in the target.
A value filter is a simple function returning a string.
If the a value (returned by filter or not) is undefined, the target is not merged.
If the a value (returned by filter or not) is null, it is merged as empty string
but if it's an attribute it doesn't create it (use empty string for that).
Domt.filters.myfilter = function(val, context) {
// this will always merge something since it casts undefined to string
return "my" + val;
};
Where context contains {
node: the node where the value is being merged,
att: the name of the target attribute (it cannot be "text" or "html"), if any,
path: array of path components,
index: current index in array of path,
name: the last name of the path before this filter was called,
scope: the root object being accessed by path,
filters: the available filters
}
Value filters are not called if the first key in the accessor matches undefined
data. Any other case (> first key, null or empty data) will call the filter.
Some filters are already availables:
upper : toUpperCase
lower : toLowerCase
br : replace newlines by
, to be used with in bind-html.
text : escape html, the combination text|br is useful
esc : encodeURIComponent
unesc : decodeURIComponent
json : JSON.stringify
int : parseInt or ""
float : parseFloat or ""
att : return attribute name if current value evaluates to true
drop : remove target attribute if current value evaluates to true - does not
modify val.
always: return true
never : return false
! : return !val
? : return val.toString() if it's not empty, else return null or undefined or ""
log : log value and context path, name, index
More filters could be written, but they would typically depend on features
like Element.classList or jQuery - they're not fit to be in domt.
Note that escaping xml entities is usually not needed because we use the
DOM methods and they do the conversions for us.
Filters can accept objects as well, see test/replacement.html.
Block Filters
While value filters are declarable when merging values, a block filter can
be declared on the repeated accessor
- [items.text]
Block filters are called after the current data has been merged into the
cloned node and before the cloned node is inserted into the list.
Domt.filters.myBlockFilter = function(row, key, context) {
if (row.selected) node.selected = true;
// node can be inserted manually, sibling holds the node before which it would
// be inserted by default
context.tail.parentNode.insertBefore(context.node, context.tail); // default insert
// can be replaced by
if (row.invalid) {
// prevent context.node from being inserted into the list
return false;
} else {
var oldnode = document.getElementById(row.nodeid);
oldnode.parentNode.replaceChild(context.node, oldnode);
}
};
Where context contains {
node: the documentFragment (or the only child node of it) being merged,
parent: the parent where the node will be inserted by default,
path: array of path components,
index: current index in array of path,
name: the last name of the path before this filter was called,
scope: the root object being accessed by path,
filters: the available filters,
head: the comment node bounding the start of the list,
tail: the comment node bounding the end of the list
}
A block filter can control how context.node is going to be inserted:
by returning a fragment, it replaces the previous fragment
by returning a new node, it replaces context.node (if there was only one
node in the fragment)
by returning false, it prevents context.node from being inserted
and it stops further block filters from being called
by inserting context.node itself somewhere else
Some filters are already available:
invert: insert nodes in reverse order
Tables
It is possible to merge columns, then merge rows, to achieve merging
table data with arbitrary columns.
There's an example of this in
test/repeat.html#should repeat within repeat
Utilities
Domt.each(obj, iter)
obj can be an array, an object, a NodeList, a jQuery object
iter is a function with signature (item, index) called on each element of obj.
Domt.template(name|selector|node)
where name is defined by repeat-name.
returns an instance of a template, upon which the following methods are useful
Domt.createFragment()
creates a fragment from given document or from a new context as far as possible
Domt.import(node, )
import a node in a document or fall back to cloneNode
domt.import()
import current nodes of Domt instance into the given document
template.head, template.tail, template.fragment
the nodes defining a template
template.attach(selector|element|comment)
attach a template to a node.
If it is a comment node, it is used as the new head of the fragment.
If an element is passed, append a comment node to it and work with it.
See test/fragment.html for usages.
template.clone(selector|element|comment)
return a new template with cloned head, fragment and tail
See test/fragment.html for usage.
template.merge(...)
alias of Domt(template).merge(...)
Access to templates is usefull when they are defined in a place and merged
in another place.
It is possible to merge them outside the DOM, in a DocumentFragment like
var inst = Domt.template('myfrag').clone().merge(data);
document.body.appendChild(inst.nodes);
or attach them like
Domt.template('myfrag').clone('#thatid').merge(data)
Best practices
Use bind-data-*
Avoid
of having $(div).data('test') returning "[test|json]", in case
the data wasn't merged.