diff --git a/CHANGELOG.md b/CHANGELOG.md index 7627088..843d978 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,13 @@ # Changelog +### 1.0.0 (2016-05-30) +- Breaking change: remove `defineBinding` function, now the default exported function does only one thing: binding an object to the DOM. +- Breaking change: removed `return false` behavior to retain DOM element, it should instead return `simulacra.retainElement`. +- Feature: change function may accept a return value, which sets `textContent`, `value`, or `checked`. Returning `undefined` will have no effect. +- Polish: rename *mutator* function to *change* function. + + ### 0.16.1 (2016-05-26) - Polish: remove redundant logic in mutator function. diff --git a/LICENSE b/LICENSE index bea4c28..57d9ef1 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ The MIT License (MIT) -Copyright (c) 2015 0x8890 <0x8890@airmail.cc> (http://0x8890.com) +Copyright (c) 2016 0x8890 <0x8890@airmail.cc> (http://0x8890.com) Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/README.md b/README.md index 24eda31..47db35c 100644 --- a/README.md +++ b/README.md @@ -12,7 +12,7 @@ $ npm i simulacra --save ## Synopsis -Simulacra.js makes the DOM react to changes in data. When data changes, it maps those changes to the DOM by adding and removing elements after invoking mutator functions, which by default, assign plain text and form input values. +Simulacra.js makes the DOM react to changes in data. When data changes, it maps those changes to the DOM by adding and removing elements and invoking *change* functions, which by default, assign plain text and form input values. Fundamentally, it is a low-cost abstraction over the DOM that optimizes calls to `Node.insertBefore` and `Node.removeChild`. Its performance is comparable to hand-written DOM manipulation code, see the [benchmarks](#benchmarks). @@ -43,69 +43,76 @@ var data = { } ``` -Simulacra.js exports only a single function, which can either define bindings to the DOM, or apply bindings to an object (this is also exposed as `simulacra.defineBinding` and `simulacra.bindObject`). If the first argument is an object, it will try to bind the second argument onto the object. If the first argument is either a DOM Node or a CSS selector string, it will return a definition object that is used by Simulacra.js internally, and the second argument then defines either a nested definition or a mutator function. This can be combined in a single expression: +Simulacra.js exports only a single function, which binds an object to the DOM. The first argument must be a singular object, and the second argument is a data structure that defines the bindings. The definition must be a single value or an array with at most three elements: -```js -var $ = require('simulacra') // or `window.simulacra` +- **Index 0**: either a DOM element or a CSS selector string. +- **Index 1**: either a nested definition array, or a *change* function. +- **Index 2**: if index 1 is a nested definition, this should be an optional *mount* function. +```js +var simulacra = require('simulacra') // or `window.simulacra` var fragment = document.getElementById('product').content -var content = $(data, $(fragment, { - name: $('.name'), - details: $('.details', { - size: $('.size'), - vendor: $('.vendor') - }) -})) +var node = simulacra(data, [ fragment, { + name: '.name', + details: [ '.details', { + size: '.size', + vendor: [ '.vendor', function change () { ... } ] + }, function mount () { ... } ] +} ]) -document.body.appendChild(content) +document.body.appendChild(node) ``` The DOM will update if any of the bound keys are assigned a different value, or if any `Array.prototype` methods on the value are invoked. Arrays and single values may be used interchangeably, the only difference is that Simulacra.js will iterate over array values. -## Mutator Function +## Change Function -By default, the value will be assigned to the element's `textContent` property (or `value` or `checked` for inputs), a user-defined mutator function may be used for arbitrary element manipulation. The mutator function may be passed as the second argument to Simulacra.js, it has the signature (`node`, `value`, `previousValue`, `path`): +By default, the value will be assigned to the element's `textContent` property (or `value` or `checked` for inputs). A user-defined *change* function may be passed for arbitrary element manipulation, and its return value may affect the value used in the default behavior. The *change* function may be passed as the second position, it has the signature (`element`, `value`, `previousValue`, `path`): -- `node`: the local DOM node. -- `value`: the value assigned to the key of the bound object. -- `previousValue`: the previous value assigned to the key of the bound object. -- `path`: an array containing the full path to the value. For example: `[ 'users', 2, 'email' ]`. Integer values indicate array indices. The root object is accessible at the `root` property of the path array, i.e. `path.root`, and the deepest bound object is accessible at the `target` property, i.e. `path.target`. +- **`element`**: the local DOM element. +- **`value`**: the value assigned to the key of the bound object. +- **`previousValue`**: the previous value assigned to the key of the bound object. +- **`path`**: an array containing the full path to the value. For example: `[ 'users', 2, 'email' ]`. Integer values indicate array indices. The root object is accessible at the `root` property of the path array, i.e. `path.root`, and the deepest bound object is accessible at the `target` property, i.e. `path.target`. -To manipulate a node in a custom way, one may define a mutator function like so: +To manipulate an element in a custom way, one may define a *change* function like so: ```js -$(node || selector, function mutator (node, value) { - node.textContent = 'Hi ' + value + '!' -}) +[ element || selector, function change (element, value) { + return 'Hi ' + value + '!' +} ] ``` -A mutator function can be determined to be an insert, mutate, or remove operation based on whether the value or previous value is `null`: +A *change* function can be determined to be an insert, mutate, or remove operation based on whether the value or previous value is `null`: -- Value but not previous value: insert operation. -- Value and previous value: mutate operation. -- No value: remove operation. +- **Value but not previous value**: insert operation. +- **Value and previous value**: mutate operation. +- **No value**: remove operation. -There are some special cases for the mutator function: +There are some special cases for the *change* function: -- If the bound node is the same as its parent, its value will not be iterated over if it is an array. -- If the mutator function returns `false` for a remove operation, then `Node.removeChild` will not be called. This is useful for implementing animations when removing a Node from the DOM. +- If the bound element is the same as its parent, its value will not be iterated over if it is an array. +- If the *change* function returns `simulacra.retainElement` for a remove operation, then `Node.removeChild` will not be called. This is useful for implementing animations when removing an element from the DOM. ## Mount Function -A mount function can be defined on a bound object, as the third argument. Its signature is very similar the mutator function, except that it does not provide `previousValue`. Instead, it can be determined if there was a mount or unmount based on whether `value` is an object or not. +A *mount* function can be defined as the third position. Its signature is similar to the *change* function, except that it does not provide `previousValue`. Instead, it can be determined if there was a mount or unmount based on whether `value` is an object or `null`. ```js -$(node || selector, { ... }, function mount (node, value) { +[ element || selector, { ... }, function mount (element, value) { if (value !== null) { - // Mounting a node, maybe attach event listeners here. + // Mounting an element, maybe attach event listeners here. + } + else { + // Unmounting an element, may return `simulacra.retainElement` + // to skip removal from the DOM. } -}) +} ] ``` -If the mount function returns false for an unmount, it will skip removing the node from the DOM. This is useful for implementing animations. +If the *mount* function returns `simulacra.retainElement` for an unmount, it will skip removing the element from the DOM. This is useful for implementing animations. ## State Management @@ -114,15 +121,15 @@ Since Simulacra.js is intended to be deterministic, the bound object can be clon ```js var clone = require('clone') -var $ = require('simulacra') +var simulacra = require('simulacra') -var data = { ... }, bindings = $( ... ) +var data = { ... }, bindings = [ ... ] -var node = $(data, bindings) +var node = simulacra(data, bindings) var initialData = clone(data) // Do some mutations, and then reset to initial state. -node = $(initialData, bindings) +node = simulacra(initialData, bindings) ``` This is just one way to implement time travel, but not the most efficient. @@ -147,9 +154,9 @@ To run the benchmarks, you will have to clone the repository and build it by run ## How it Works -On initialization, Simulacra.js removes bound elements from the document and replaces them with an empty text node (marker) for memoizing its position. Based on a value in the bound data object, it clones template elements and applies the mutator function on the cloned elements, and appends them near the marker or adjacent nodes. +On initialization, Simulacra.js removes bound elements from the document and replaces them with an empty text node (marker) for memoizing its position. Based on a value in the bound data object, it clones template elements and applies the *change* function on the cloned elements, and appends them near the marker or adjacent nodes. -When a bound key is assigned, it gets internally casted into an array if it is not an array already, and the values of the array are compared with previous values. Based on whether a value at an index has changed, Simulacra.js will remove, insert, or mutate a DOM Node corresponding to the value. This is faster and simpler than diffing changes between DOM trees. +When a bound key is assigned, it gets internally casted into an array if it is not an array already, and the values of the array are compared with previous values. Based on whether a value at an index has changed, Simulacra.js will remove, insert, or mutate a DOM element corresponding to the value. This is faster and simpler than diffing changes between DOM trees. ## Caveats @@ -164,11 +171,11 @@ When a bound key is assigned, it gets internally casted into an array if it is n This library is written in ES5 syntactically, and makes use of: -- Object.defineProperty (ES5) -- WeakMap (ES6) -- TreeWalker (DOM Level 2) -- Node.isEqualNode (DOM Level 3) -- Node.contains (DOM Living Standard) +- **Object.defineProperty** (ES5) +- **WeakMap** (ES6) +- **TreeWalker** (DOM Level 2) +- **Node.isEqualNode** (DOM Level 3) +- **Node.contains** (DOM Living Standard) No shims are included. At the bare minimum, it works in IE9+ with a WeakMap polyfill, but otherwise it should work in IE11+. @@ -184,9 +191,9 @@ const simulacra = require('simulacra') const window = domino.createWindow('

') const $ = simulacra.bind(window) const data = { message: 'Hello world!' } -const binding = $('body', { - message: $('h1') -}) +const binding = [ 'body', { + message: 'h1' +} ] console.log($(data, binding).innerHTML) ``` diff --git a/benchmark/simulacra.html b/benchmark/simulacra.html index b350e6d..f1e49e6 100644 --- a/benchmark/simulacra.html +++ b/benchmark/simulacra.html @@ -7,7 +7,7 @@

To run an execution time test on this page, run the profiler from your browser's developer tools and measure the running time of a page refresh. (Lower is better)

diff --git a/lib/bind_keys.js b/lib/bind_keys.js index bfa78bf..afd6c36 100644 --- a/lib/bind_keys.js +++ b/lib/bind_keys.js @@ -28,9 +28,10 @@ function bindKeys (scope, obj, def, parentNode, path) { function define (key) { var initialValue = obj[key] var branch = def[key] - var mutator = branch.mutator - var mount = branch.mount - var definition = branch.definition + var node = branch[0] + var change = !branch.__hasDefinition && branch[1] + var definition = branch.__hasDefinition && branch[1] + var mount = branch[2] // Keeping state in this closure. var keyPath = path.concat(key) @@ -65,7 +66,7 @@ function bindKeys (scope, obj, def, parentNode, path) { if (definition && x != null) bindKeys(scope, x, definition, parentNode, keyPath) - else if (mutator) mutator(parentNode, x, previousValue, keyPath) + else if (change) change(parentNode, x, previousValue, keyPath) return null } @@ -99,9 +100,8 @@ function bindKeys (scope, obj, def, parentNode, path) { for (i = 0, j = Math.max(previousValues.length, value.length); i < j; i++) checkValue(value, i) - // Reset length to current values, implicitly deleting indices from - // `previousValues` and `activeNodes` and allowing for garbage - // collection. + // Reset length to current values, implicitly deleting indices and + // allowing for garbage collection. previousValues.length = activeNodes.length = value.length return x @@ -143,8 +143,8 @@ function bindKeys (scope, obj, def, parentNode, path) { endPath.target = path.target } - if (mutator) - returnValue = mutator(activeNode, null, previousValue, endPath) + if (change) + returnValue = change(activeNode, null, previousValue, endPath) else if (definition && mount) { endPath.target = endPath.root @@ -154,17 +154,20 @@ function bindKeys (scope, obj, def, parentNode, path) { returnValue = mount(activeNode, null, endPath) } - // If a mutator function returns false, skip removing from DOM. - if (returnValue !== false) - branch.marker.parentNode.removeChild(activeNode) + // If a change or mount function returns the retain element symbol, + // skip removing the element from the DOM. + if (returnValue !== bindKeys.retainElement) + branch.__marker.parentNode.removeChild(activeNode) delete activeNodes[i] } } function addNode (value, previousValue, i) { - var j, k, node, nextNode, activeNode = activeNodes[i] + var activeNode = activeNodes[i] + var currentNode = node var endPath = keyPath + var j, k, nextNode, returnValue // Cast previous value to null if undefined. if (previousValue === void 0) previousValue = null @@ -185,29 +188,39 @@ function bindKeys (scope, obj, def, parentNode, path) { if (definition) { if (activeNode) removeNode(value, previousValue, i) - node = processNodes(scope, branch.node.cloneNode(true), definition, i) + currentNode = processNodes(scope, node.cloneNode(true), definition, i) endPath.target = isArray ? value[i] : value - bindKeys(scope, value, definition, node, endPath) + bindKeys(scope, value, definition, currentNode, endPath) if (mount) { endPath.target = endPath.root for (j = 0; j < keyPath.length - 1; j++) endPath.target = endPath.target[keyPath[j]] - mount(node, value, endPath) + mount(currentNode, value, endPath) } } - else if (mutator) { - if (activeNode) { - mutator(activeNode, value, previousValue, endPath) - return - } + else { + currentNode = activeNode || node.cloneNode(true) + returnValue = change ? + change(currentNode, value, previousValue, endPath) : + value !== void 0 ? value : null + } - node = branch.node.cloneNode(true) - mutator(node, value, previousValue, endPath) + if (returnValue !== void 0) switch (branch.__replaceAttribute) { + case 'checked': + if (returnValue) currentNode.checked = 'checked' + else currentNode.removeAttribute('checked') + break + default: + currentNode[branch.__replaceAttribute] = returnValue } + // Do not actually add an element to the DOM if it's only a change + // between non-empty values. + if (!definition && activeNode) return + // Find the next node. for (j = i + 1, k = activeNodes.length; j < k; j++) if (activeNodes[j]) { @@ -215,8 +228,8 @@ function bindKeys (scope, obj, def, parentNode, path) { break } - activeNodes[i] = branch.marker.parentNode.insertBefore( - node, nextNode || branch.marker) + activeNodes[i] = branch.__marker.parentNode + .insertBefore(currentNode, nextNode || branch.__marker) } diff --git a/lib/index.js b/lib/index.js index 225e7cc..eb04d10 100644 --- a/lib/index.js +++ b/lib/index.js @@ -4,12 +4,18 @@ var processNodes = require('./process_nodes') var bindKeys = require('./bind_keys') var useCommentNode = false +var retainElement = '__retain__' + + ('00000000' + Math.floor(Math.random() * Math.pow(2, 32)).toString(16)) + .slice(-8) -module.exports = simulacra +// Upgrade to Symbol if possible, it guarantees uniqueness. +if (typeof Symbol === 'function') retainElement = Symbol(retainElement) -// Expose the internal functions so that dynamic dispatch isn't required. -simulacra.defineBinding = defineBinding -simulacra.bindObject = bindObject +// Symbol for retaining an element instead of removing it. +bindKeys.retainElement = retainElement +Object.defineProperty(simulacra, 'retainElement', { + enumerable: true, value: retainElement +}) // Option to use comment nodes. processNodes.useCommentNode = useCommentNode @@ -21,174 +27,136 @@ Object.defineProperty(simulacra, 'useCommentNode', { enumerable: true }) - -/** - * Dynamic dispatch function. - * - * @param {Node|String|Object} - * @param {Function|Object} - * @param {Function} - */ -function simulacra (a, b, c) { - var Node = this ? this.Node : window.Node - - if (typeof a === 'string' || a instanceof Node) return defineBinding(a, b, c) - if (typeof a === 'object' && a !== null) return bindObject.call(this, a, b) - - throw new TypeError('First argument must be either ' + - 'a DOM Node, string, or an Object.') -} - - -/** - * Define a binding. - * - * @param {Node|String} - * @param {Function|Object} - * @param {Function} - */ -function defineBinding (node, a, b) { - // Memoize the selected node. - var obj = { node: node } - - if (typeof a === 'function') - obj.mutator = a - - else if (typeof a === 'object') { - obj.definition = a - if (b !== void 0) - if (typeof b === 'function') obj.mount = b - else throw new TypeError('Third argument must be a function.') - } - - else if (a !== void 0) - throw new TypeError('Second argument must be either ' + - 'a function or an object.') - - return obj -} +// Main export. +module.exports = simulacra /** - * Bind an object to a Node. + * Bind an object to the DOM. * * @param {Object} obj * @param {Object} def * @return {Node} */ -function bindObject (obj, def) { - var Node = this ? this.Node : window.Node +function simulacra (obj, def) { var document = this ? this.document : window.document var node, query, path = [] - if (Array.isArray(obj)) + if (typeof obj !== 'object' || Array.isArray(obj)) throw new TypeError('First argument must be a singular object.') - if (!def || typeof def.definition !== 'object') - throw new TypeError('Top-level binding must be an object.') + if (!Array.isArray(def)) + throw new TypeError('Second argument must be an array.') - if (!(def.node instanceof Node)) { - query = def.node - def.node = document.querySelector(query) - if (!def.node) throw new Error( - 'Top-level Node "' + query + '" could not be found in the document.') + if (typeof def[0] === 'string') { + query = def[0] + def[0] = document.querySelector(query) + if (!def[0]) throw new Error( + 'Top-level element "' + query + '" could not be found in the document.') } - ensureNodes(def.node, def.definition) + ensureNodes(this, def[0], def[1]) - node = processNodes(this, def.node.cloneNode(true), def.definition) + node = processNodes(this, def[0].cloneNode(true), def[1]) // Assign root object. path.root = obj - bindKeys(this, obj, def.definition, node, path) + bindKeys(this, obj, def[1], node, path) return node } -// Default DOM mutation functions. -function replaceText (node, value) { node.textContent = value } -function replaceValue (node, value) { node.value = value } -function replaceChecked (node, value) { - if (value) node.checked = 'checked' - else node.removeAttribute('checked') -} - -// Private static property, used for checking parent binding function. -Object.defineProperty(replaceText, '__isDefault', { value: true }) -Object.defineProperty(replaceValue, '__isDefault', { value: true }) -Object.defineProperty(replaceChecked, '__isDefault', { value: true }) - - -function noop (key) { - return function () { - console.warn( // eslint-disable-line - 'Undefined mutator function for key "' + key + '".') - } -} - - /** * Internal function to mutate string selectors into Nodes and validate that * they are allowed. * + * @param {Object} scope * @param {Node} parentNode * @param {Object} def */ -function ensureNodes (parentNode, def) { - var i, j, key, query, branch, boundNode, ancestorNode +function ensureNodes (scope, parentNode, def) { + var Element = scope ? scope.Element : window.Element var adjacentNodes = [] + var i, j, key, query, branch, boundNode, ancestorNode + var replaceAttribute, nodeName, inputType for (key in def) { + if (!Array.isArray(def[key])) + def[key] = [ def[key] ] + branch = def[key] - if (typeof branch.node === 'string') { - query = branch.node + // Internal value used while processing nodes. + Object.defineProperty(branch, '__marker', { value: null, writable: true }) + + if (typeof branch[0] === 'string') { + query = branch[0] // May need to get the node above the parent, in case of binding to // the parent node. ancestorNode = parentNode.parentNode || parentNode - branch.node = ancestorNode.querySelector(query) - if (!branch.node) throw new Error( - 'The Node for selector "' + query + '" was not found.') + branch[0] = ancestorNode.querySelector(query) + if (!branch[0]) throw new Error( + 'The element for selector "' + query + '" was not found.') } + else if (!(branch[0] instanceof Element)) + throw new TypeError('The first position on key "' + key + + '" must be a DOM element or a CSS selector string.') - boundNode = branch.node + boundNode = branch[0] + + if (typeof branch[1] === 'object' && branch[1] !== null) { + Object.defineProperty(branch, '__hasDefinition', { value: true }) + if (branch[2] && typeof branch[2] !== 'function') + throw new TypeError('The third position on key "' + key + + '" must be a function.') + } + else if (branch[1] && typeof branch[1] !== 'function') + throw new TypeError('The second position on key "' + key + + '" must be an object or a function.') // Special case for binding to parent node. if (parentNode === boundNode) { Object.defineProperty(branch, '__isBoundToParent', { value: true }) - if (branch.mutator && branch.mutator.__isDefault) - branch.mutator = noop(key) - else if (branch.definition) - ensureNodes(boundNode, branch.definition) + if (branch.__hasDefinition) ensureNodes(scope, boundNode, branch[1]) + else if (typeof branch[1] !== 'function') + console.warn( // eslint-disable-line + 'A change function was not defined on the key "' + key + '".') continue } else adjacentNodes.push([ key, boundNode ]) if (!parentNode.contains(boundNode)) - throw new Error('The bound DOM Node must be either ' + - 'contained in or equal to its parent binding.') - - if (branch.definition) ensureNodes(boundNode, branch.definition) - else if (!branch.mutator) - if (boundNode.nodeName === 'INPUT' || boundNode.nodeName === 'SELECT') - if (boundNode.type === 'checkbox' || boundNode.type === 'radio') - branch.mutator = replaceChecked - else branch.mutator = replaceValue - else branch.mutator = replaceText + throw new Error('The bound DOM element must be either ' + + 'contained in or equal to the element in its parent binding.') + + if (branch.__hasDefinition) { + ensureNodes(scope, boundNode, branch[1]) + continue + } + + nodeName = boundNode.nodeName + inputType = boundNode.type + replaceAttribute = (nodeName === 'INPUT' || nodeName === 'SELECT') ? + (inputType === 'checkbox' || inputType === 'radio') ? + 'checked' : 'value' : 'textContent' + + Object.defineProperty(branch, '__replaceAttribute', + { value: replaceAttribute }) } // Need to invalidate containment in adjacent nodes, after the adjacent // nodes are found. for (key in def) { - boundNode = def[key].node + boundNode = def[key][0] for (i = 0, j = adjacentNodes.length; i < j; i++) if (adjacentNodes[i][1].contains(boundNode) && adjacentNodes[i][1] !== boundNode) - throw new Error('The Node for key "' + key + '" is contained in the ' + - 'Node for the adjacent key "' + adjacentNodes[i][0] + '".') + throw new Error( + 'The element for key "' + key + '" is contained in the ' + + 'element for the adjacent key "' + adjacentNodes[i][0] + '".') } } diff --git a/lib/process_nodes.js b/lib/process_nodes.js index 53f446a..2b4364a 100644 --- a/lib/process_nodes.js +++ b/lib/process_nodes.js @@ -19,16 +19,17 @@ function processNodes (scope, node, def) { for (key in def) { branch = def[key] if (branch.__isBoundToParent) continue - mirrorNode = map.get(branch.node) + + mirrorNode = map.get(branch[0]) parent = mirrorNode.parentNode if (processNodes.useCommentNode) { - branch.marker = parent.insertBefore( - document.createComment(' end "' + key + '" '), mirrorNode) + branch.__marker = parent.insertBefore(document.createComment( + ' end "' + key + '" '), mirrorNode) parent.insertBefore(document.createComment( - ' begin "' + key + '" '), branch.marker) + ' begin "' + key + '" '), branch.__marker) } - else branch.marker = parent.insertBefore( + else branch.__marker = parent.insertBefore( document.createTextNode(''), mirrorNode) parent.removeChild(mirrorNode) @@ -54,7 +55,7 @@ function matchNodes (scope, node, def) { var nodes = [] var i, j, key, currentNode - for (key in def) nodes.push(def[key].node) + for (key in def) nodes.push(def[key][0]) while (treeWalker.nextNode() && nodes.length) for (i = 0, j = nodes.length; i < j; i++) { diff --git a/package.json b/package.json index 3faf364..ea92945 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "simulacra", "description": "One-way data binding for web applications.", - "version": "0.16.1", + "version": "1.0.0", "license": "MIT", "author": { "email": "0x8890@airmail.cc", @@ -33,7 +33,7 @@ "browserify-istanbul": "^2.0.0", "cssnano": "^3.6.2", "domino": "^1.0.25", - "eslint": "^2.10.2", + "eslint": "^2.11.1", "eslint-config-0x8890": "^1.0.2", "highlight.js": "^9.4.0", "html-minifier": "^2.1.3", diff --git a/test/index.js b/test/index.js index 93c8e1d..640504e 100644 --- a/test/index.js +++ b/test/index.js @@ -18,9 +18,9 @@ run(function () { document.body.appendChild(template) data = { name: 'Babby' } - bindings = $('div', { - name: $('.name') - }) + bindings = [ 'div', { + name: '.name' + } ] result = document.body.appendChild($(data, bindings)) @@ -57,16 +57,16 @@ run(function () { ] } - bindings = $(fragment, { - name: $('.name', function (node, value, previousValue, path) { + bindings = [ fragment, { + name: [ '.name', function (node, value, previousValue, path) { ok(path.length === 1, 'path length is correct') ok(path.root === data, 'root is correct') ok(path.target === data, 'target is correct') ok(path[0] === 'name', 'path is correct') node.textContent = value + '!' - }), - details: $(selector('.details'), { - size: $('.size', function (node, value, previousValue, path) { + } ], + details: [ selector('.details'), { + size: [ '.size', function (node, value, previousValue, path) { if (value !== 'Large') { if (i < 1) { i++ @@ -80,8 +80,8 @@ run(function () { ok(path[2] === 'size', 'path value is correct') } node.textContent = value - }), - color: $(selector('.color'), + } ], + color: [ selector('.color'), function (node, value, previousValue, path) { ok(path.length === 3, 'path length is correct') ok(path.root === data, 'root is correct') @@ -89,15 +89,15 @@ run(function () { ok(path[0] === 'details', 'path value is correct') ok(path[1] === 'color', 'path value is correct') ok(typeof path[2] === 'number', 'array path is a number') - }) - }), - prices: $(selector('.price'), { - amount: $(selector('.amount')), - currency: $(selector('.currency'), function (node, value) { + } ] + } ], + prices: [ selector('.price'), { + amount: selector('.amount'), + currency: [ selector('.currency'), function (node, value) { node.textContent = value.toUpperCase() - }) - }) - }) + } ] + } ] + } ] outlet = document.createElement('div') outlet.id = 'outlet' diff --git a/website/build.js b/website/build.js index a3ef0fd..24e1a16 100644 --- a/website/build.js +++ b/website/build.js @@ -71,13 +71,13 @@ fs.writeFileSync(path.join(outputPath, 'index.html'), minify( version: pkg.version, name: pkg.name, description: pkg.description - }, $('body', { - name: $('header h1', (node, value) => node.textContent = - value.charAt(0).toUpperCase() + value.slice(1) + '.js'), - description: $('header h2'), - version: $('.version'), - content: $('article', (node, value) => node.innerHTML = value) - })).innerHTML ].join(''), + }, [ 'body', { + name: [ 'header h1', (node, value) => + value.charAt(0).toUpperCase() + value.slice(1) + '.js' ], + description: 'header h2', + version: '.version', + content: [ 'article', (node, value) => { node.innerHTML = value } ] + } ]).innerHTML ].join(''), { collapseWhitespace: true } )) diff --git a/website/example.html b/website/example.html index 9abfe57..1fccd8c 100644 --- a/website/example.html +++ b/website/example.html @@ -23,44 +23,44 @@

diff --git a/website/head.html b/website/head.html index 0d85bba..4f07a5f 100644 --- a/website/head.html +++ b/website/head.html @@ -10,4 +10,4 @@ - +