Reactive template engine for robust real-time rendering of model data changes.
With component:
$ component install component/reactive
With the stand-alone browser build:
<script src="reactive.js"></script>
Bind object
to the given element
with optional view
object. When a view
object is present it will be checked first for overrides, which otherwise delegate to the model object
.
For example if you have the following HTML:
<h1 data-text="name"></h1>
And pass the following object
as the second argument:
{
name: 'Tobi'
}
The output will become:
<h1>Tobi</h1>
However if you wish to manipulate the output or provided computed properties thae view
object may be passed. For example an object
of:
{
first_name: "Tobi",
last_name: "Ferret"
}
And a view
of:
function UserView(user) {
this.user = user;
}
UserView.prototype.name = function(){
return this.user.first_name + ' ' + this.user.last_name;
}
Would produce:
<h1>Tobi Ferret</h1>
Typically a view object wraps a model to provide additional functionality, this may look something like the following:
function UserView(user) {
this.user = user;
this.el = reactive(tmpl, user, this);
}
UserView.prototype.name = function(){ ... }
Often a higher-level API is built on top of this pattern to keep things DRY but this is left to your application / other libraries.
Subscriptions allow reactive to know when an object's data has changed updating the DOM appropriately without re-rendering a static template. This means if you make manual DOM adjustments, append canvases etc they will remain intact.
By default reactive subscribes using .on("change <name>", callback)
however it's easy to define your own subscription methods:
reactive.subscribe(function(obj, prop, fn){
obj.bind(prop, fn);
});
reactive.unsubscribe(function(obj, prop, fn){
obj.unbind(prop, fn);
});
You can make reactive compatible with your favorite framework by defining how reactive gets and sets the model.
By default reactive supports obj[prop] = val
and obj[prop](val)
, but these can be changed with reactive.get(fn)
and reactive.set(fn)
. Here's how to make reactive compatible with backbone:
reactive.get(function(obj, prop) {
return obj.get(prop);
});
reactive.set(function(obj, prop, val) {
obj.set(prop, val);
});
Bindings may be applied via interoplation on attributes or text. For example here
is a simple use of this feature to react to changes of an article's .name
property:
<article>
<h2>{name}</h2>
</article>
Text interpolation may appear anywhere within the copy, and may contain complex JavaScript expressions for defaulting values or other operations.
<article>
<h2>{ name || 'Untitled' }</h2>
<p>Summary: { body.slice(0, 10) }</p>
</article>
Reactive is smart enough to pick out multiple properties that may be used, and react to any of their changes:
<p>Welcome { first + ' ' + last }.</p>
Interpolation works for attributes as well, reacting to changes as you'd expect:
<li class="file-{id}">
<h3>{filename}</h3>
<p><a href="/files/{id}/download">Download {filename}</a></p>
<li>
By default reactive supplies bindings for setting properties, listening to events, toggling visibility, appending and replacing elements. Most of these start with "data-*" however this is not required.
The data-text
binding sets the text content of an element.
The data-html
binding sets the inner html of an element.
The data-<attr>
bindings allows you to set an attribute:
<a data-href="download_url">Download</a>
The on-<event>
bindings allow you to listen on an event:
<li data-text="title"><a on-click="remove">x</a></li>
The data-append
binding allows you to append an existing element:
<div class="photo" data-append="histogram">
</div>
The data-replace
binding allows you to replace an existing element:
<div class="photo" data-replace="histogram">
</div>
The data-show
and data-hide
bindings conditionally add "show" or "hide" classnames so that you may style an element as hidden or visible.
<p data-show="hasDescription" data-text="truncatedDescription"></p>
Toggles checkbox state:
<input type="checkbox" data-checked="agreed_to_terms">
To author bindings simply call the reactive.bind(name, fn)
method, passing the binding name and a callback which is invoked with the element itself and the value. For example here is a binding which removes an element when truthy:
reactive.bind('remove-if', function(el, name){
el = $(el);
var parent = el.parent();
this.change(function(){
if (this.value(name)) {
el.remove();
}
});
});
Reactive supports computed properties denoted with the <
character. Here the fullname
property does not exist on the model, it is a combination of both .first
and .last
, however you must tell Reactive about the real properties in order for it to react appropriately:
<h1 data-text="fullname < first last"></h1>
NOTE: in the future Reactive may support hinting of computed properties from outside Reactive itself, as your ORM-ish library may already have this information.
Some bindings such as data-text
and data-<attr>
support interpolation. These properties are automatically added to the subscription, and react to changes:
<a data-href="/download/{id}" data-text="Download {filename}"></a>
Get creative! There's a lot of application-specific logic that can be converted to declarative Reactive bindings. For example here's a naive "auto-submit" form binding:
<div class="login">
<form action="/user" method="post" autosubmit>
<input type="text" name="name" placeholder="Username" />
<input type="password" name="pass" placeholder="Password" />
<input type="submit" value="Login" />
</form>
</div>
var reactive = require('reactive');
// bind
var view = reactive(document.querySelector('.login'));
// custom binding available to this view only
view.bind('autosubmit', function(el){
el.onsubmit = function(e){
e.preventDefault();
var path = el.getAttribute('action');
var method = el.getAttribute('method').toUpperCase();
console.log('submit to %s %s', method, path);
}
});
For more examples view the ./examples directory.
MIT