diff --git a/lib/bind.js b/lib/bind.js index 4fe7766..2d174ae 100644 --- a/lib/bind.js +++ b/lib/bind.js @@ -1,7 +1,6 @@ // author: Remy Sharp // license: http://rem.mit-license.org/ // source: https://github.com/remy/bind/ - var Bind = (function Bind(global) { 'use strict'; // support check @@ -11,9 +10,6 @@ var Bind = (function Bind(global) { var debug = false; - // this is a conditional because we're also supporting node environment - var $; - try { $ = document.querySelectorAll.bind(document); } catch (e) {} var array = []; var isArray = Array.isArray; var o = 'object'; @@ -112,6 +108,19 @@ var Bind = (function Bind(global) { if (settings.ready && object.__callback) { return target; } + + // don't rebind + if (object instanceof Bind) { + return object; + } + + // this is a conditional because we're also supporting node environment + var $; + try { + var context = settings.context || document; + $ = context.querySelectorAll.bind(context); + } catch (e) {} + // loop through each property, and make getters & setters for // each type of "regular" value. If the key/value pair is an // object, then recursively call extend with the target and @@ -135,6 +144,10 @@ var Bind = (function Bind(global) { var selector = settings.mapping[path.join('.')]; + if (debug) { + console.log('key: %s / %s', key, path.join('.'), selector); + } + // then we've got an advanced config - rather than 1-1 mapping if (selector && selector.toString() === '[object Object]') { if (selector.callback) { @@ -151,14 +164,20 @@ var Bind = (function Bind(global) { selector = selector.dom; } + var elements; + if (typeof selector === 'string') { + // cache the matched elements. Note the :) is because qSA won't allow an + // empty (or undefined) string so I like the smilie. + elements = $(selector || '☺'); + } else if (global.Element && selector instanceof global.Element) { + elements = [selector]; + } + // look for the path in the mapping arg, and if the gave // us a callback, use that, otherwise... if (typeof selector === 'function') { callback = selector; - } else if (typeof selector === 'string') { - // cache the matched elements. Note the :) is because qSA won't allow an - // empty (or undefined) string so I like the smilie. - var elements = $(selector || '☺'); + } else if (elements) { if (elements.length === 0) { console.warn('No elements found against "' + selector + '" selector'); } @@ -167,17 +186,20 @@ var Bind = (function Bind(global) { // matched from the selector (set up below), that checks // the node type, and either sets the input.value or // element.innerHTML to the value - var valueSetters = ['SELECT', 'INPUT', 'PROGRESS', 'TEXTAREA']; + var valueSetters = ['OPTION', 'INPUT', 'PROGRESS', 'TEXTAREA']; if (value === null || value === undefined) { if (valueSetters.indexOf(elements[0].nodeName) !== -1) { - value = parse(elements[0].value); + if (elements[0].hasOwnProperty('checked')) { + value = parse(elements[0].value === 'on' ? elements[0].checked : elements[0].value); + } else { + value = parse(elements[0].value); + } } else { value = parse(elements[0].innerHTML); } } - var oldCallback = callback; callback = function (value) { // make it a live selection @@ -193,7 +215,7 @@ var Bind = (function Bind(global) { if (valueSetters.indexOf(element.nodeName) !== -1) { // TODO select[multiple] // special case for multi-select items - var result = transform(value); + var result = transform(value, target); if (element.type === 'checkbox') { if (value instanceof Array) { var found = value.filter(function (value) { @@ -205,6 +227,8 @@ var Bind = (function Bind(global) { if (found.length === 0) { element.checked = false; } + } else if (typeof value === 'boolean') { + element.checked = value; } } else if (element.type === 'radio') { element.checked = element.value === result; @@ -222,11 +246,21 @@ var Bind = (function Bind(global) { if (!(value instanceof Array)) { value = [value]; } - var html = ''; + var html = []; + forEach(value, function (value) { - html += transform(value); + html.push(transform(value, target)); }); - element.innerHTML = html; + // peek the first item, if it's a node, append + // otherwise set the innerHTML + if (typeof html[0] === 'object') { + element.innerHTML = ''; // blow away original + html.forEach(function (el) { + element.appendChild(el); + }); + } else { + element.innerHTML = html.join(''); + } } }); } @@ -239,27 +273,29 @@ var Bind = (function Bind(global) { // note that this doesn't support event delegation forEach(elements, function (element) { if (element.nodeName === 'INPUT' || element.nodeName === 'SELECT' || element.nodeName === 'TEXTAREA') { + + // build up the event handler function var oninput = function () { // we set a dirty flag against this dom node to prevent a // circular update / max stack explode this.__dirty = true; var result; if (element.type === 'checkbox') { - var inputs = (element.form || document).querySelectorAll('input[name="' + element.name + '"][type="' + element.type + '"]'); + var inputs = (element.form || document).querySelectorAll('input[name="' + element.name + '"][type="checkbox"]'); if (target[key] instanceof Array) { var results = []; forEach(inputs, function (input) { if (input.checked) { - results.push(parse(input.value)); + results.push(parse(input.value === 'on' ? input.checked : input.value)); } }); result = results; } else { - result = this.checked ? parse(this.value) : null; + result = parse(this.value === 'on' ? this.checked : this.value); } } else { if (element.type === 'radio') { - result = this.checked ? parse(this.value) : null; + result = parse(this.value === 'on' ? this.checked : this.value); } if (typeof target[key] === 'number') { result = parse(this.value * 1); } else { @@ -280,6 +316,7 @@ var Bind = (function Bind(global) { }; var event = { + // select: 'change', checkbox: 'change', radio: 'change', }[element.type]; @@ -356,7 +393,7 @@ var Bind = (function Bind(global) { } if (dirty && always) { - var instance = always.instance; + instance = always.instance; always.callback.call(settings.instance, __export(instance instanceof Array ? [] : {}, instance)); } }; @@ -383,7 +420,9 @@ var Bind = (function Bind(global) { value = v; } - if (debug) console.log('set: key(%s): %s -> %s', key, JSON.stringify(old), JSON.stringify(v)); + if (debug) { + console.log('set: key(%s): %s -> %s', key, JSON.stringify(old), JSON.stringify(v)); + } // expose the callback so that child properties can call the // parent callback function @@ -398,7 +437,9 @@ var Bind = (function Bind(global) { findCallback(value); } else { // defer the callback until we're fully booted - settings.deferred.push(findCallback.bind(target, value, old)); + if (typeof settings.mapping[path.join('.')] !== 'undefined') { + settings.deferred.push(findCallback.bind(target, value, old)); + } } }, get: function () { @@ -410,7 +451,9 @@ var Bind = (function Bind(global) { try { Object.defineProperty(target, key, definition); } catch (e) { - // console.log(e.toString(), e.stack); + if (debug) { + console.log('failed on Object.defineProperty', e.toString(), e.stack); + } } // finally, set the target aka the returned value's property to the value @@ -454,7 +497,8 @@ var Bind = (function Bind(global) { __export(target[key] || {}, value) : value; }); - } else if (typeof value === o && value !== null && !isArray(value)) { + } else if (typeof value === o && value !== null && !isArray(value) && + value.toString === '[Object object]') { target[key] = __export(target[key] || {}, value); } else { target[key] = value; @@ -465,12 +509,13 @@ var Bind = (function Bind(global) { } - function Bind(obj, mapping) { + function Bind(obj, mapping, context) { if (!this || this === global) { return new Bind(obj, mapping); } var settings = { + context: context || global.document, mapping: mapping || {}, callbacks: {}, deferred: [], @@ -512,4 +557,4 @@ var Bind = (function Bind(global) { if (typeof exports !== 'undefined') { module.exports = Bind; -} \ No newline at end of file +} diff --git a/package.json b/package.json index 7c9b88f..8e592cf 100644 --- a/package.json +++ b/package.json @@ -9,7 +9,7 @@ "test": "karma start", "build": "uglifyjs --mangle --compress -- lib/bind.js > dist/bind.min.js", "test-node": "mocha test/*.test.js test/**/*.test.js", - "semantic-release": "semantic-release pre && npm publish && semantic-release post" + "semantic-release": "semantic-release pre && npm run build && npm publish && semantic-release post" }, "repository": { "type": "git", diff --git a/test/augmented-array.test.js b/test/augmented-array.test.js new file mode 100644 index 0000000..8c5e68f --- /dev/null +++ b/test/augmented-array.test.js @@ -0,0 +1,108 @@ +/*global describe, assert, beforeEach: true, Bind, sinon, it*/ +var sinon = require('sinon'); +var assert = require('assert'); +var bind = require('../'); + +describe('augmented array', function () { + 'use strict'; + var data; + var spy; + + beforeEach(function () { + spy = sinon.spy(); + + data = bind({ + list: [1,2,3] + }, { + list: spy, + }); + }); + + it('push', function () { + var cursor = spy.args.length; + var count = spy.callCount; + assert.ok(data.list.length === 3, 'length is right'); + data.list.push(4); + assert.ok(spy.callCount === count + 1, 'call count increased'); + assert.ok(data.list.length === 4, 'length is right'); + assert.ok(spy.args[cursor][0].length === 4, 'passed in was 4'); + }); + + it('pop', function () { + var cursor = spy.args.length; + var count = spy.callCount; + data.list.pop(); + assert.ok(spy.callCount === count + 1, 'call count increased'); + assert.ok(data.list.length === 2, 'length is right'); + assert.ok(spy.args[cursor][0].length === 2, 'new array accepted'); + }); + + it('shift', function () { + var cursor = spy.args.length; + var count = spy.callCount; + var first = data.list.shift(); + assert.ok(spy.callCount === count + 1, 'call count increased'); + assert.ok(data.list.length === 2, 'length is right'); + assert.ok(spy.args[cursor][0].length === 2, 'new array accepted'); + assert.ok(first === 1, 'got first value'); + }); + + it('unshift', function () { + var cursor = spy.args.length; + var count = spy.callCount; + data.list.unshift(0); + assert.ok(spy.callCount === count + 1, 'call count increased'); + assert.ok(data.list.length === 4, 'length is right'); + assert.ok(spy.args[cursor][0].length === 4, 'new array accepted'); + assert.ok(spy.args[cursor][0][0] === 0, 'got first value'); + }); + + it('splice', function () { + var cursor = spy.args.length; + var count = spy.callCount; + data.list.splice(1, 1); + assert.ok(spy.callCount === count + 1, 'call count increased'); + assert.ok(data.list.length === 2, 'length is right'); + assert.ok(spy.args[cursor][0].length === 2, 'new array accepted'); + assert.ok(spy.args[cursor][0].toString() === '1,3', 'got first value'); + }); +}); + + +describe('augmented array containing binds', function () { + 'use strict'; + var data; + var spy; + + beforeEach(function () { + function makeBind(n) { + return bind({ + data: n, + }, { + data: function () {} + }); + } + + spy = sinon.spy(); + + data = bind({ + list: [1,2,3].map(makeBind) + }, { + list: spy, + }); + }); + + it('shift', function () { + var cursor = spy.args.length; + var count = spy.callCount; + data.list.shift(); + assert.ok(spy.callCount === count + 1, 'call count increased'); + assert.ok(data.list.length === 2, 'length is right'); + assert.ok(spy.args[cursor][0].length === 2, 'new array accepted'); + var values = data.list.map(function (v) { + return v.data; + }).join(','); + console.log(values); + assert.equal(values, '2,3', 'source data is correct'); + }); +}); diff --git a/test/checkboxes.browser.js b/test/checkboxes.browser.js index a38a9f1..308b79f 100644 --- a/test/checkboxes.browser.js +++ b/test/checkboxes.browser.js @@ -45,4 +45,4 @@ describe('checkboxes', function () { }); -}); \ No newline at end of file +}); diff --git a/test/radios.browser.js b/test/radios.browser.js index 7007962..6df6213 100644 --- a/test/radios.browser.js +++ b/test/radios.browser.js @@ -18,7 +18,9 @@ describe('radios', function () { }, { ts: { dom: 'input[type=radio]', - callback: spy, + callback: function (arg) { + spy(arg); + }, }, }); }); @@ -46,4 +48,4 @@ describe('radios', function () { }); -}); \ No newline at end of file +}); diff --git a/test/select.-browser.js b/test/select.-browser.js new file mode 100644 index 0000000..3924574 --- /dev/null +++ b/test/select.-browser.js @@ -0,0 +1,56 @@ +'use strict'; +var assert = require('assert'); +var sinon = require('sinon'); +var html = require('./html'); +var Bind = require('../'); + +/* globals describe, beforeEach, it */ +describe('select', function () { + var data; + var spy; + + beforeEach(function () { + html(window.__html__['test/select.html']); + spy = sinon.spy(); + + data = new Bind({ + cats: ['one', 'two', 'three'], + }, { + 'me.cats': { + dom: '#select', + transform: function (cat) { + console.log('cat', cat.map(_ => ``).join(',')); + spy(cat); + return cat.map(_ => ``).join(','); + // return cat.map(function (_) { + // return ''; + // }); + } + }, + }); + }); + + it('should update DOM as defined by data', function () { + var nodes = document.querySelectorAll('select'); + var found = nodes.length; + assert.ok(found === 1, 'found ' + found); + assert.ok(nodes[0].value === 'one', 'found node value: ' + nodes[0].value); + assert.ok(spy.called, 'spy called ' + spy.callCount); + }); + + it('should update the data when the DOM is changed', function (done) { + var node = document.querySelector('select'); + + node.value = 'three'; + var event = document.createEvent('HTMLEvents'); + event.initEvent('change', true, true); + node.dispatchEvent(event); + + setTimeout(function () { + assert.equal(data.ts, 'three', 'ts: ' + data.ts); + done(); + }, 10); + + + }); +}); diff --git a/test/select.html b/test/select.html new file mode 100644 index 0000000..6c475ee --- /dev/null +++ b/test/select.html @@ -0,0 +1,21 @@ + + + +