A model-agnostic live data binding template layer for the DOM.
JavaScript HTML CSS
Fetching latest commit…
Cannot retrieve the latest commit at this time.
Permalink
Failed to load latest commit information.
css
images
js
modules
package
performance
plugins
test
.eslintrc.json
.gitignore
.gitmodules
Gruntfile.js
LICENSE
README.md
config-package.js
index-old.html
index.html
karma.conf.js
package.json
test-forms.html
tests.html

README.md

Sparky

alt tag

Sparky is a live data binding templating engine that enhances the DOM with declarative tags and composeable templates, updating tags and rendering changes in batches at the browser frame rate for performance.

labs.cruncher.ch/sparky/

Setup

If you just want to clone the repo to use Sparky in a project:

git clone https://github.com/cruncher/sparky.git

If you're going to develop it, Sparky has submodules. Clone the repo recursively:

git clone https://github.com/cruncher/sparky.git --recursive

Install node modules for building and testing:

npm install

API

Sparky(node, scope, fn)

var sparky = Sparky(node, scope, fn)

To bind a node call Sparky with a node or id, a scope object and/or a function.

node node | document fragment | selector

Required parameter. If it is a DOM node or document fragment, Sparky parses it for tags. If it is a selector string Sparky selects a node (only #id selectors currently supported) and parses that for tags.

scope object | string | undefined

An object who's properties are used to render tags in the node. (Sparky's scope may also be replaced at a later time via sparky.scope(object).) If the scope parameter is a string it defines path to an object in the Sparky.data.

fn function | string | undefined

A function to run upon instantiating the node, or a string defining a name or names of function(s) stored in sparky.fn or in Sparky.fn. Log Sparky.fn in the console to see the default functions available.

sparky

A sparky object is an array-like object of DOM nodes that have been bound to data models. It emits lifecycle events and exposes a few methods for interacting with the template.

Methods

.create()

Create a new sparky as a dependent of the current sparky.

var child = sparky.create(node, scope, fn);

Parameters are the same as for the Sparky() constructor. Child sparkies inherit data and fn objects, and are updated and destroyed along with their parents.

.destroy()

The nuke option. Destroys the data bindings, removes nodes from the DOM and removes any event handlers. Also destroys any child sparkies.

.render(path1, path2, ...)

Force all tags to update with current values. This should rarely be needed, as Sparky handles render updates automatically, but it can be useful in cases where unobserved data changes and you need to give Sparky a nudge to display it.

If no path arguments are passed in then all tags are updated.

.scope(object)

Swap the scope being used by this sparky for a new object. Sparky simply updates it's DOM with data from the new scope.

.interrupt()

Stops Sparky from running functions and parsing it's nodes. Typically called when content is being replaced, for eaxmple, the built-in function Sparky.fn.each calls interrupt before cloning a node for each item in a collection.

Returns a function that calls all functions in the data-fn list that have not yet been called.

.tojQuery()

Where jQuery is available, returns sparky's element nodes (but not text nodes) wrapped as a jQuery object.

Sparky templates are reasonably tolerant to being manipulated in the DOM. Nodes in a template will stay bound to data when they are moved around or removed from the DOM, or when other nodes are inserted between them.

You should be aware, though, that changing text content or attributes of nodes that had Sparky tags in them when they were bound will likely cause problems – Sparky will overwrite your changes the next time it's data is updated. The exception is the class attribute: you can add and remove your own classes as much as you like without fear of upsetting Sparky.

.on(type, fn)

Listen to event type with fn.

.off(type, fn)

Stop listening to event type with fn. All parameters are optional.

Events

  • scope: triggered when scope is initialised or changed
  • destroy: triggered when data bindings have been destroyed and the node removed from the DOM.

Sparky

Sparky.render(template, object)

Where template is a string, replaces the Sparky tags in the string with matching properties of object.

Sparky.render('{{ bossname }} loves wooo!', {
    bossname: "Sparky"
});

// Returns: 'Sparky loves wooo!'

Where template is a regular expression, composes the regexp with regexp properties of object.

    Sparky.render(/{{ropen}}\s*(\w+)\s*{{rclose}}/g, {
        ropen:  /\{{2,3}/,
        rclose: /\}{2,3}/
    });

    // Returns: /\{{2,3}\s*(\w+)\s*\}{2,3}/g

Where template is a function, and that function contains a single JS comment, the contents of the comment are whitespace-cropped and treated as a template string.

Sparky.render(function(){/*
    {{ boss }} loves wooo!
*/}, {
    boss: "Sparky"
});

// -> 'Sparky loves wooo!'

This is a nice hacky technique for writing multiline templates in JS, although now superseded by ES6 multiline strings.

Note that Sparky.render is not used by Sparky to update the DOM. Sparky does not treat the DOM as strings, it treats the DOM as the DOM, keeping an internal map of node attributes and text node content bound directly to data changes.

Sparky.tags(ropen, rclose)

Change the opening and closing template tag brackets. ropen and rclose must be regular expressions.

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

The regular expressions used to test for tags (Sparky.rtags, Sparky.rsimpletags) are updated with the new opening and closing tags. Sparky.rtags and Sparky.rsimpletags are read-only properties.

Sparky.observe(object, property, fn)

Sparky.unobserve(object, property, fn)

Sparky.observePath(object, path, fn)

Sparky.observePathOnce(object, path, fn)

Sparky.unobservePath(object, path, fn)

Sparky.template(id)

Given the id of a template tag in the DOM, Sparky.template(id) returns the cloned contents of that template as a document fragment.

Supports older browsers where <template> does not have the associated JavaScript property template.content.

Sparky.dom

A small library of DOM helper functions.

  • .query(node, selector) -
  • .tag(node) - Returns the element's tag name
  • .create(type, text) - Creates a 'text', 'comment', 'fragment' or element node
  • .append(parent, node || collection) - Append node to parent
  • .after(node1, node2) -
  • .before(node1, node2) -
  • .empty(node) -
  • .remove(node || collection) -
  • .closest(node, selector [, root]) - Finds closest ancestor matching selector
  • .matches(node, selector) -
  • .classes(node) - Returns a classList object
  • .style(node, name) - Returns the computed style for named CSS property
  • .fragmentFromTemplate(id) - Returns cloned fragment from a template's content
  • .fragmentFromContent(node) - Returns a fragment containing a node's content or children

Nodes

  • .isElementNode(node) -
  • .isTextNode(node) -
  • .isCommentNode(node) -
  • .isFragmentNode(node) -

Events

  • .on(node, type, fn) -
  • .off(node, type, fn) -
  • .trigger(node, type) -
  • .delegate(selector, fn) -
  • .isPrimaryButton(e) -

Sparky.attributes

An array of attributes where Sparky looks for template tags.

  • class
  • href
  • title
  • id
  • style
  • src
  • alt

For labels, inputs, selects and textareas Sparky also looks in:

  • for
  • name
  • value
  • max
  • min

Boolean properties are set on an element according to the truthiness of a single Sparky tag found in their attributes:

  • required="{{object.required}}"
  • disabled="{{object.enabled|yesno:false,true}}"

Sparky.rsimpletags

A Regular expression matching tags of the form {{ path.to.property }}. The path is stored in capturing group 1.

Sparky.rtags

A Regular expression matching tags of the form {{ path.to.property|filter:'param' }}. The opening brackets, the path and the filter string are stored in capturing groups 1,2 and 3.

Sparky.filter

An object containing template filters.

Display the date, formatted:

<h1 class="language-{{lang}}" data-scope="text">
    {{title}}
    <time>{{date|date:'d M Y'}}</time>
</h1>

Sparky has a number of template filters for modifying and formatting data. You can also create your own. Sparky template filter syntax is similar to Django template filter syntax:

<p>{{ date|date:'d M Y' }}</p>
  • add
  • capfirst
  • cut: string – cuts matching string from a value
  • date
  • decibels – Takes a number as a ratio of powers and performs 20log10(number) to render it on the decibel scale.
  • decimals: number – Alias of floatformat.
  • divide: number – Divides by number.
  • escape
  • find-in: path - finds an object in a collection at path by it's indexed key (usually id).
  • first –
  • floatformat: number –
  • floor
  • get: string – Takes an object and renders the named property.
  • greater-than: value, stringTrue, stringFalse
  • invert – Returns 1/property.
  • is: value – Strictly compares property to value, returns a boolean.
  • equals: value - Deeply compares property to value, returns a boolean.
  • join
  • json
  • last
  • length
  • less-than: value, stringTrue, stringFalse
  • lower
  • lowercase – Alias of lower.
  • mod: number – Performs value % number.
  • multiply: number
  • parseint
  • percent: number – Takes a number and multiplies by 100 to render it as a percentage.
  • pluralize: stringSingular, stringPlural, lang –
  • postpad: number, string –
  • prepad: number, string –
  • random
  • replace
  • round
  • slice
  • slugify
  • striptags
  • switch: string, … – Takes a number and returns the string at that index.
  • symbolise – Converts common values to symbolic equivalents: JavaScript's number Infinity becomes '∞'.
  • truncatechars
  • type – Returns type of value
  • uppercase –
  • yesno

Templates

[data-scope]

The data-scope attribute is a path to an object to be used as scope to render the tags in this elements. Paths are written in dot notation.

<div data-scope="path.to.object">
    <h1>{{title}}</h1>
</div>

This will look for the object in the current sparky's sparky.data object, or in the global data object Sparky.data. A tagged path makes Sparky look for an object in the current scope.

<div data-scope="{{path.to.object}}">
    <h1>{{title}}</h1>
</div>

If no object is found at path.to.object, the <div> is removed from the DOM. Sparky puts it back as soon as the path can be resolved to an object. Sparky even updates the DOM if any of the objects in the path are swapped for new objects.

Object names can contain - characters, or be numbers.

{{path.0.my-object}}

[data-fn]

The data-fn attribute tells Sparky to run one or more functions when it wires up this element.

<form data-fn="submit-validate">...</form>

Sparky looks for functions in the current sparky's sparky.fn store, or in the global function store Sparky.fn. Functions are powerful. They can modify or replace the scope, change and listen to the DOM, define new data and fn stores and so on. Sparky deliberately permits anything in a function so that you may organise your app as you please.

More than one fn can be defined. They are run in order.

<form data-fn="my-app-scope validate-on-submit">...</form>

The return value of each function is passed along the chain as the scope argument of the next.

Define a ctrl function.

{{tag}}

Sparky replaces template tags with data, and updates them when the data changes.

<h1 data-scope="object">{{ first-page.title }}</h1>

The text in the <h1> is now updated whenever object.first-page.title changes.

Sparky will find tags in text nodes, class, href, title, id, style, src, alt, for, value, min, max and name attributes. This list can be modified by pushing to Sparky.attributes.

Sparky treats tags in the class attribute as individual tokens, so it is safe to modify the class attribute outside of Sparky. Sparky avoids overwriting any new classes that are added.

{{tag|filter}}

Modify scope values with filters:

<h1 data-scope="my-model" class="{{selected|yesno:'active','inactive'}}">
    {{title|uppercase}}
</h1>

More about filters.

{{{tag}}}

A triple bracket tag updates from the scope once only.

<h1 data-scope="my-model">{{{ title }}}</h1>

These tags are updated once from the scope (in this case my-model), but they don't live bind to changes. If you know where you can do it, this can be good for performance.

Input, select and textarea elements

By putting a Sparky tag in the name attribute, inputs, selects and textareas are 2-way bound to a scope property. When the scope changes, the input's values is updated, and when the input is changed the scope property is updated.

<form class="user-form" data-scope="{{user}}">
    <input type="text" name="{{username}}" placeholder="Sparky" />
    <input type="number" name="{{age}}" />
</form>

By default Sparky is strict about type in form elements. The first input above is type="text" and it will only display the username property if that property is a string. Other types will display as an empty value.

scope.username = 'Sparky';    // input.value === 'Sparky'
scope.username = 3;           // input.value === ''

The second input is type="number". It will only display the age property if that property is a number.

scope.age = 'Fourteen';       // input.value === ''
scope.age = 32;               // input.value === '32'

Similarly, type="range" only gets and sets numbers, and type="checkbox" only gets and sets booleans, unless the value attribute contains a string (in which case the property must be a string matching the value for the input to be checked).

Other types get and set strings by default. To force the input to get and set a different type use one of Sparky's value-xxx functions&hellips;

data-fn="value-xxx"

To data-bind a specific type, give the element one of Sparky's value functions:

  • value-string sets a string
  • value-float sets a float
  • value-float-log sets a float with a log x transform
  • value-float-pow-2 sets a float with a x2 transform
  • value-float-pow-3 sets a float with a x3 transform
  • value-int sets an integer, rounding where necessary
  • value-int-log sets an integer with a log transform
  • value-bool sets true or false
  • value-any gets any type, sets strings

Here are some examples. Radio inputs that sets scope.property to integers 1 or 2:

<input type="radio" data-fn="value-int" name="{{property}}" value="1" />
<input type="radio" data-fn="value-int" name="{{property}}" value="2" />

A select that sets scope.property to true or false:

<select data-fn="value-bool" name="{{property}}">
    <option value="true">Yes</option>
    <option value="false">No</option>
</select>

A checkbox that is checked when scope.property === 3:

<input type="checkbox" data-fn="value-int" name="{{property}}" value="3" />

A range slider that sets scope.property as a string:

<input type="range" data-fn="value-string" name="{{property}}" min="0" max="1" step="any" />

A range slider that sets scope.property as a float, with a logarithmic transform across it's range from 1 to 10:

<input type="range" data-fn="value-string" name="{{property}}" min="1" max="10" step="any" />

data-fn="each"

The each function loops over a scope that is a collection or array, cloning a new node for each item in the collection. This...

<ul id="list">
    <li data-scope="{{contributors}}" data-fn="each">
        <a href="{{url}}">{{name}}</a>
    </li>
</ul>

Sparky('list', {
    contributors: Collection([
        { name: "Sparky", url: "http://github.com/cruncher/sparky" },
        { name: "Cruncher", url: "http://cruncher.ch" }
    ])
});

...results in a DOM that looks like this:

<ul id="list">
    <li>
        <a href="http://github.com/cruncher/sparky">Sparky</a>
    </li>
    <li>
        <a href="http://cruncher.ch">Cruncher</a>
    </li>
</ul>

Sparky says thankwoo

to Mariana Alt (www.alt-design.ch) for drawing me for my logo.