Sparky

A live data binding templating engine that enhances HTML/SVG with tags and scopes and composeable templates, and applies granular data changes to the DOM on browser frames for performance.

How it works

  • Cable Ties

    1 x packet of 50 £2.49

    Change

Total £2.49

This example progressively enhances an imaginary shopping cart. Here's the HTML from the server with one item in the cart.

data-fn

Sparky's three attributes data-scope, data-fn and data-template wire up the DOM. data-fn="cart" calls a function that returns an object for Sparky to use as render scope.

data-scope

Next, data-scope="{{items}}" finds items in the current scope and uses that as scope for the ul. Except that it doesn't yet, becuase cart.items has not been defined. Instead, Sparky waits for that data to become available, and in the meantime continues parsing.

data-template

At data-template="cart-items" Sparky looks for a template with that id and parses it's contents instead of the contents of the ul.

<template id="cart-items">
  
</template>

Load some data…

Sparky now has a model of the enhanced DOM in his head ready to be populated with data, but the template is not yet inserted because the scope for the ul is not defined. So let's go ahead and see what happens when data is 'loaded':

cart.items = Collection([{
  title: "Cable Ties",
  url: "images/product-cable-ties.png",
  quantity: 1,
  unit: 'packet of 50',
  price: 2.49
}]);

…and Sparky renders

The static contents of the ul were replaced and the list is now dynamic. Objects added to cart.items make Sparky update the list.

cart.items.add({
			title: "Gaffa Tape",
			url: "images/product-gaffa-tape.png",
			quantity: 1,
			unit: 'roll',
			price: 3.19
		});

Changes made to the inputs in the list are reflected in the data. For example, in the cart try changing the quantity of Gaffa Tape.

cart.items[1].quantity = {{quantity}};

That's it.

That's Sparky in a nutshell.

The cart object is exposed on window in case you would like to play with it in the console. Try replacing cart.items with an entirely new Collection() and notice that Sparky picks it right up.

Sparky is designed to be simple. It a number of other nifty features – but that's the gist of it.

HTML

data-scope

data-scope="path.to.object"

Looks for an object inside the current Sparky's data object or inside Sparky.data to use as scope for the current node.

data-scope="{{path.to.object}}"

Looks for an object inside the current scope to use as scope for the node.

data-fn

data-fn="name"

Looks for a function by name inside the current Sparky's fn object or inside Sparky.fn and runs it. If no function is found an error is thrown.

data-fn="name1 name2"

Runs multiple functions, in order.

data-template

data-template="id"

Replaces the content of an element with the content of a template referenced by id.

{{ tag }}

{{path.to.property}}

Rendered whenever the value of the property at the end of the path is changed.

{{path.to.property|filter:''}}

Processes the value with one of the filters found in Sparky.filters.

name="{{path.to.property}}"

Bi-directional data binding for values of input, select and textarea elements.

{{{ tag }}}

{{{path.to.property}}}

Rendered once, the first time the property has value.

Oops.

Tag delimiters clashing with another template engine? Change them:

Sparky.tags(/\[{2,3}/, /\]{2,3}/);

Now tags look like [[this]]. Probably best to do this before any of the DOM is parsed, ie. before DOMContentLoaded.

Functions

Built ins

Sparky comes with a bunch of functions built in.

Roll your own

Sparky's functions are called with a Sparky instance as context, giving them access to lifecycle events and control methods, and they are passed the node as the first parameter. In MVC-speak, they are view-controllers.

Functions are run before the node's content and attributes are scanned and they watch for initialisation and changes using Sparky's lifecycle events. They may modify or overwrite the instance's fn and data objects, providing descendent content access to 'private' functions and data. They may also return an object to be used as a scope, in which case changes to the surrounding scope are ignored.

Read more: Sparky API.

Technical

DOM

Sparky is robust against DOM manipulations made outside of Sparky. It does not require you to work with the DOM in a prescribed way. When a Sparky instance is constructed Sparky stores an internal reference to only those nodes and attributes where template tags are found. It does not then care if those nodes are moved, classes added or attributes changed by other means – they stay bound until the Sparky instance is destroyed.

Data

Sparky observes changes to muteable data structures. The observation system uses getters and setters on object properties, and comes with a Collection([]) constructor for making observable array-like objects. Arrays can be used, but as there is no sane way to observe them, they are observed by polling. Sparky was disappointed when Object.observe was dropped from ES7, but he's getting ready for ES6 Proxies.

If you don't like this observation mechanism you can entirely replace it to suit your own data by overwriting Sparky.observe(object, property, fn).