npm install tectonic.js
npm install tectonic.js
Tectonic is a functional rendering engine for DOM nodes, heavily
inspired by Beebole’s PURE
rendering engine. It ascribes to PURE’s unobtusive philosophy,
whereby HTML code is completely free of any application logic or new
syntax and JavaScript code is uninhibited by presentational concerns.
This is achieved by both PURE and Tectonic by the use of a directive
object that marries the DOM referenced by CSS selectors to properties
in your application’s data. Where Tectonic departs from PURE is in
the use of functions, known as renderers, to directly manipulate DOM
nodes. This permits Tectonic to provide two-way data flow via a
parse
method, which makes use of an inverse function that
is attached to a renderer.
git clone https://github.com/tecknack/tectonic.git
Run the test suite and gauge its coverage.
npm test
To truly understand Tectonic read the annotated source code.
more tectonic.js
Perhaps the best way to explain Tectonic is by example.
The simplest usage is autoRender
. Here we have a HTML
snippet; the class name has been carefully chosen to match a property
in our data.
new Tectonic(document.getElementById('example-1'))
.autoRender(
{
'hello': 'Hello, World!'
}
);
This results in the value from our data subsituted in the chosen DOM node.
<span class="hello">Hello, World!</span>
That's all very well and good but autoRender
is not
very flexible (not to mention inefficient). Here we have the same HTML
snippet; but instead of our data needing to match the DOM, we pass a
directive with the data to render
in order to do the
equivalent matching.
new Tectonic(document.getElementById('example-2'))
.render(
{
'message': 'Hello, World!'
},
{
'.hello': 'message'
}
);
The result is the same.
<span class="hello">Hello, World!</span>
Directives can also be precompiled. Again here's the same example
but first we compile
our directive.
var t = new Tectonic(document.getElementById('example-3'));
var template = t.compile(
{
'.hello': 'message'
}
);
t.render(
{
'message': 'Hello, World!'
},
template
);
No surprises here.
<span class="hello">Hello, World!</span>
The precompiled template can also be used independently.
var t = new Tectonic(document.getElementById('example-4'));
var template = t.compile(
{
'.hello': 'message'
}
);
template(
{
'message': 'Hello, World!'
}
);
But this time it returns a new node, detached from the DOM! It's up to you to do what you want with it.
<span class="hello">Hi there!</span>
We've seen how to put data into the DOM for the simplest use case,
but Tectonic can also get data out of the DOM too. Note that this is
entirely independent from render
, we are not using any
tricks like storing data in the DOM during rendering.
new Tectonic(document.getElementById('example-5'))
.parse(
{
'.hello': 'message'
}
);
The return value from parse
is a plain old object.
{
"message": "Hi there!"
}
We can use parse
with precompiled templates too.
var t = new Tectonic(document.getElementById('example-6'));
var template = t.compile(
{
'.hello': 'message'
}
);
t.parse(template);
The return value is the same.
{
"message": "Hi there!"
}
The trick here is that compile
exports an inverse
function on its returned function.
Here we also use get
to return the original DOM node
that was given to Tectonic.
var t = new Tectonic(document.getElementById('example-7'));
var template = t.compile(
{
'.hello': 'message'
}
);
template.inverse(t.get());
Again, the return value is the same.
{
"message": "Hi there!"
}
As you've already seen a directive is a plain old object that tells Tectonic where to find the data (the object's values) and where to put it in the DOM (the object's key). The following examples explores the latter, that is the directive's selectors.
As you've already guessed, the selector of a directive can be any
valid selector accepted by
querySelector
.
new Tectonic(document.getElementById('example-8'))
.render(
{
'element': 'Element matched by name.',
'class': 'Element matched by class name.',
'attribute': 'Element matched by attribute.',
'pseudo-class': 'Element matched by pseudo-class'
},
{
'p': 'element',
'.msg': 'class',
'[data-msg]': 'attribute',
':last-child': 'pseudo-class'
}
);
But don't be fooled the selectors are not CSS selectors.
<p>Element matched by name.</p>
<p class="msg">Element matched by class name.</p>
<p data-msg="example">Element matched by attribute.</p>
<p>Element matched by pseudo-class</p>
Let's get solarized!
Tectonic allows you to update attributes in the DOM by using a
special @attribute
syntax.
new Tectonic(document.getElementById('example-9'))
.render(
{
'theme': 'solarized-dark'
},
{
'p@class': 'theme'
}
);
The attribute value is replaced.
<p class="solarized-dark">Let's get solarized!</p>
Let's get
Replacing values isn't always what you want, so Tectonic interprets
the pseudo-classes :before
and :after
differently.
new Tectonic(document.getElementById('example-10'))
.render(
{
'theme': 'solarized-dark',
'name': 'Solarized (Dark)'
},
{
'p@class:before': 'theme',
'p:after': 'name'
}
);
This time the original attribute value is retain and added to.
<p class="solarized-dark background">Let's get Solarized (Dark)</p>
I'm solarized
I'm not
There's an extra pseudo-class, :toggle
.
new Tectonic(document.getElementById('example-11'))
.render(
{
'theme': 'solarized-dark',
'message': "No you're not, I am"
},
{
'p@class:toggle': 'theme',
':last-child': 'message'
}
);
If the class was already there, it gets removed, otherwise it gets added.
<p class="">I'm solarized</p>
<p class="solarized-dark">No you're not, I am</p>
Input elements and dropdown boxes are treated specially. Since all
input elements should be empty, Tectonic updates the
value
property instead of its contents. Remember to use
@checked
for checkbox and radio input fields. Similarly,
@selected
should be used with option
elements.
new Tectonic(document.getElementById('example-12'))
.render(
{
'latest': '0.1.2',
'ready': 1,
'isMaster': 1
},
{
'[name="version"]': 'latest',
'[name="published"]@checked': 'ready',
'[value="master"]@selected': 'isMaster'
}
);
Notice that the input element doesn't receive a value attribute,
this is because only the value property of the DOM object is
set. To set the value attribute of the element use a
@value
selector.
<input type="text" name="version">
<input type="checkbox" name="published" checked="1">
<select name="branch">
<option value="gh-pages">github.io</option>
<option value="master" selected="1">master</option>
</select>
Let's get solarized!
If you want your selector to match the top level element (that is
to say the element being rendered) then use .
or an empty
string.
Oh, and you can use arrays as data too.
new Tectonic(document.getElementById('example-13'))
.render(
[ 'a', 'b', 'c', 'd', 'e', 'f' ],
{
'.': '0',
'': '1',
'.:after': '2',
':after': '3',
'.@data-e': '4',
'@data-f': '5'
}
);
None of the examples actually show the top level container, so you'll just have to open the console and try it for yourself.
bcd
We've explored the keys of a directive object, so let's now turn our attention to how Tectonic finds the data (the directive object's values).
{
:
}
Thus far we have always used the name of the property in the data object to specify the data, but we can also use a constant string or a combination of both.
The first value of this directive contains the string
'message'
, Tectonic see the single quotes and interprets
the value as a string literal. The second value of the directive uses
the same technique but this time Tectonic sees the double quotes as
the string delimiters and interpolates the value
property
of the data in the middle.
new Tectonic(document.getElementById('example-14'))
.render(
{
'value': 'Hello, World!'
},
{
'.hljs-attr': "'message'",
'.hljs-string': '"\'" value "\'"'
}
);
The result is message
inside the
hljs-attr
element and 'Hello, World!'
inside
the hljs-string
element.
<pre><code class="json hljs">{
<span class="hljs-attr">message</span>: <span class="hljs-string">'Hello, World!'</span>
}</code></pre>
Nested objects can be accessed with either a dot sepatated string or an array.
new Tectonic(document.getElementById('example-15'))
.render(
{
nested: {
greeting: 'Hello',
'.': 'World'
}
},
{
'.string': 'nested.greeting',
'.array': ['nested', '.']
}
);
Note this won't work for objects that have an empty string as a key.
<span class="string">Hello</span>,
<span class="array">World</span>
If you provide an object then Tectonic will create a loop.
Your object must have one key that contains a left pointing arrow,
<-
, on the righthand side of the arrow provide the
key of the property in your data that Tectonic should loop over and on
the lefthand side specify a name that you can use to refer to items in
the loop from within a nested directive. The value of this special key
is the nested directive.
new Tectonic(document.getElementById('example-16'))
.render(
{
libs: [
{ name: "PURE", url: "https://beebole.com/pure" },
{ name: "Tectonic.js", url: "http://tecknack.github.io/tectonic" },
{ name: "jQuery", url: "http://jquery.com" }
]
},
{
'li': {
'lib<-libs': {
'a': 'lib.name',
'a@href': 'lib.url'
}
}
}
);
The element selected by the outer directive is repeated as
necessary and the nested directive is applied to each, once per item
in the array. We have told Tectonic that that item will be known as
lib
from within the nested directive.
<ul>
<li><a target="_blank" href="https://beebole.com/pure">PURE</a></li>
<li><a target="_blank" href="http://tecknack.github.io/tectonic">Tectonic.js</a></li>
<li><a target="_blank" href="http://jquery.com">jQuery</a></li>
</ul>
Next to the <-
key, a filter
function
can be provided. For each item in the array, this function is called
with the item, the item's index and the array itself and if it returns
a truthy value the item will included in the loop, otherwise it will
be skipped
new Tectonic(document.getElementById('example-17'))
.render(
{
libs: [
{ name: "PURE", url: "https://beebole.com/pure" },
{ name: "Tectonic.js", url: "http://tecknack.github.io/tectonic" },
{ name: "jQuery", url: "http://jquery.com" }
]
},
{
'li': {
'lib<-libs': {
'a': 'lib.name',
'a@href': 'lib.url'
},
filter: function(lib) {
return lib.url.indexOf('http:') >= 0;
}
}
}
);
The library with https
URL is not rendered.
<ul>
<li><a target="_blank" href="http://tecknack.github.io/tectonic">Tectonic.js</a></li>
<li><a target="_blank" href="http://jquery.com">jQuery</a></li>
</ul>
Next to the <-
key, a sort
function
can be provided. This function is passed directly to
Array.prototype.sort
.
new Tectonic(document.getElementById('example-18'))
.render(
{
libs: [
{ name: "PURE", url: "https://beebole.com/pure" },
{ name: "Tectonic.js", url: "http://tecknack.github.io/tectonic" },
{ name: "jQuery", url: "http://jquery.com" }
]
},
{
'li': {
'lib<-libs': {
'a': 'lib.name',
'a@href': 'lib.url'
},
sort: function(libA, libB) {
var nameA = libA.name.toUpperCase();
var nameB = libB.name.toUpperCase();
return nameA < nameB ? -1 : nameA > nameB ? 1 : 0;
}
}
}
);
The jQuery library is rendered first.
<ul>
<li><a target="_blank" href="http://jquery.com">jQuery</a></li>
<li><a target="_blank" href="https://beebole.com/pure">PURE</a></li>
<li><a target="_blank" href="http://tecknack.github.io/tectonic">Tectonic.js</a></li>
</ul>
The minimal looping specifier is <-
and means that
the data object is expected to be an array and the nested directive
can access the properties of the items directly.
new Tectonic(document.getElementById('example-19'))
.render(
[
{ name: "PURE", url: "https://beebole.com/pure" },
{ name: "Tectonic.js", url: "http://tecknack.github.io/tectonic" },
{ name: "jQuery", url: "http://jquery.com" }
],
{
'li': {
'<-': {
'a': 'name',
'a@href': 'url'
}
}
}
);
The result is the same as example 16.
<ul>
<li><a target="_blank" href="https://beebole.com/pure">PURE</a></li>
<li><a target="_blank" href="http://tecknack.github.io/tectonic">Tectonic.js</a></li>
<li><a target="_blank" href="http://jquery.com">jQuery</a></li>
</ul>
Let's see what happens if we provide a function.
The function is passed the data object and the target element,
which in this case is #example-20
.
new Tectonic(document.getElementById('example-20'))
.render(
1.23,
{
'li': function(x, target) {
return x.toFixed(1);
}
}
);
The returned value from the function is subsituted into the DOM.
<ul>
<li>1.2</li>
</ul>
Functions are best without side-effects, but we all need a little global state at times.
By default formatting functions are bound to the Tectonic
instance, but you can set the context to a different object. Then
this
will be bound to your provided context object from
inside your formatting function. The same is also true of parsing
functions.
Typically, a plugin will define the context. For example, a Backbone plugin may set the context to be the view that owns the target element being rendered.
new Tectonic(document.getElementById('example-21'))
.context({
baseUrl: 'http://tecknack.github.io/tectonic'
})
.render(
{
links: [
{
name: 'Test suite',
href: '/spec/index.html'
},
{
name: 'Code coverage',
href: '/coverage/Chrome%2043.0.2357%20(Linux%200.0.0)/tectonic/index.html'
},
{
name: 'Source code',
href: '/docs/tectonic.html'
}
]
},
{
'li': {
'<-links': {
'a': 'name',
'a@href': function(link) {
return this.baseUrl + link;
}
}
}
}
);
<ul>
<li><a href="http://tecknack.github.io/tectonic/spec/index.html">Test suite</a></li>
<li><a href="http://tecknack.github.io/tectonic/coverage/Chrome%2043.0.2357%20(Linux%200.0.0)/tectonic/index.html">Code coverage</a></li>
<li><a href="http://tecknack.github.io/tectonic/docs/tectonic.html">Source code</a></li>
</ul>
The function in the previous example will only be called once, regardless of the number of elements that match the selector. But if this is the case, we can still control the output by returning a function.
This function will be called once per matching element, and that element will be passed to the function as the second argument (the first argument is the data object), the third argument will be the index of the element within the set of matched elements and the fourth argument is an array of all the matched elements.
new Tectonic(document.getElementById('example-22'))
.render(
1.23,
{
'li': function() {
var i = 0;
return function(x, li) {
return x.toFixed(i++);
};
}
}
);
The closure has been used to vary the precision, but the third argument could have also been used.
<ul>
<li>1</li>
<li>1.2</li>
<li>1.23</li>
<li>1.230</li>
</ul>
Providing a function in a looping directive is arguably more useful.
During a loop the function is given an item from the data array as the first argument, an element that corresponds to that item as the second, the index of the item in the array as the third, the fourth is the set of all matchings elements (the length of this array is the same as the length of the data array) and lastly the data array is provided.
new Tectonic(document.getElementById('example-23'))
.render(
[0.68, 0.92, 0.71, 0.84],
{
'li': {
'<-': {
'': function(x, element, i, elements, values) {
return x.toFixed(i);
}
}
}
}
);
The function has been called once per item in the array.
<ul>
<li>1</li>
<li>0.9</li>
<li>0.71</li>
<li>0.840</li>
</ul>
We can even use a precompiled renderer as a function. Notice that
we must use template.apply
to avoid the "chicken or the
egg" problem.
Note that the ul
has been used as the target of the
renderer, this is to ensure we don't end up with many
#example-24
elements.
var t = new Tectonic($('#example-24 ul'))
var template = t.compile({
'li': {
'<-children': {
'a': 'name',
'.children': function() {
return template.apply(void 0, arguments);
}
}
}
});
t.render(
{ "children": [
{ "name": "Europe",
"children": [
{ "name": "Belgium",
"children": [
{ "name": "Brussels",
"children": null
},
{ "name": "Namur" },
{ "name": "Antwerpen" }
]
},
{ "name": "Germany" },
{ "name": "UK"}
]
},
{ "name": "America",
"children": [
{ "name": "US",
"children": [
{ "name": "Alabama" },
{ "name": "Georgia"}
]
},
{ "name": "Canada" },
{ "name": "Argentina" }
]
},
{ "name": "Asia" },
{ "name": "Africa" },
{ "name": "Antarctica" }
]
},
template
);
The render has added comments where li
elements would
have gone if there was more data. This is in case you want to execute
the renderer again when you get more data.
<ul class="children">
<li>
<a class="name">Europe</a>
<ul class="children">
<li>
<a class="name">Belgium</a>
<ul class="children">
<li>
<a class="name">Brussels</a>
<ul class="children">
<!--li-->
</ul>
</li>
<li>
<a class="name">Namur</a>
<ul class="children">
<!--li-->
</ul>
</li>
<li>
<a class="name">Antwerpen</a>
<ul class="children">
<!--li-->
</ul>
</li>
</ul>
</li>
<li>
<a class="name">Germany</a>
<ul class="children">
<!--li-->
</ul>
</li>
<li>
<a class="name">UK</a>
<ul class="children">
<!--li-->
</ul>
</li>
</ul>
</li>
<li>
<a class="name">America</a>
<ul class="children">
<li>
<a class="name">US</a>
<ul class="children">
<li>
<a class="name">Alabama</a>
<ul class="children">
<!--li-->
</ul>
</li>
<li>
<a class="name">Georgia</a>
<ul class="children">
<!--li-->
</ul>
</li>
</ul>
</li>
<li>
<a class="name">Canada</a>
<ul class="children">
<!--li-->
</ul>
</li>
<li>
<a class="name">Argentina</a>
<ul class="children">
<!--li-->
</ul>
</li>
</ul>
</li>
<li>
<a class="name">Asia</a>
<ul class="children">
<!--li-->
</ul>
</li>
<li>
<a class="name">Africa</a>
<ul class="children">
<!--li-->
</ul>
</li>
<li>
<a class="name">Antarctica</a>
<ul class="children">
<!--li-->
</ul>
</li>
</ul>