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 @@
+
+
+
+