Skip to content

Commit 089ee6c

Browse files
committed
feat: support nested bindings
This change is only feature changes (and subtle fixes), to fully support a todomvc demo using bind.js
1 parent 5bf3eff commit 089ee6c

File tree

7 files changed

+262
-30
lines changed

7 files changed

+262
-30
lines changed

lib/bind.js

Lines changed: 71 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
// author: Remy Sharp
22
// license: http://rem.mit-license.org/
33
// source: https://github.com/remy/bind/
4-
54
var Bind = (function Bind(global) {
65
'use strict';
76
// support check
@@ -11,9 +10,6 @@ var Bind = (function Bind(global) {
1110

1211
var debug = false;
1312

14-
// this is a conditional because we're also supporting node environment
15-
var $;
16-
try { $ = document.querySelectorAll.bind(document); } catch (e) {}
1713
var array = [];
1814
var isArray = Array.isArray;
1915
var o = 'object';
@@ -112,6 +108,19 @@ var Bind = (function Bind(global) {
112108
if (settings.ready && object.__callback) {
113109
return target;
114110
}
111+
112+
// don't rebind
113+
if (object instanceof Bind) {
114+
return object;
115+
}
116+
117+
// this is a conditional because we're also supporting node environment
118+
var $;
119+
try {
120+
var context = settings.context || document;
121+
$ = context.querySelectorAll.bind(context);
122+
} catch (e) {}
123+
115124
// loop through each property, and make getters & setters for
116125
// each type of "regular" value. If the key/value pair is an
117126
// object, then recursively call extend with the target and
@@ -135,6 +144,10 @@ var Bind = (function Bind(global) {
135144

136145
var selector = settings.mapping[path.join('.')];
137146

147+
if (debug) {
148+
console.log('key: %s / %s', key, path.join('.'), selector);
149+
}
150+
138151
// then we've got an advanced config - rather than 1-1 mapping
139152
if (selector && selector.toString() === '[object Object]') {
140153
if (selector.callback) {
@@ -151,14 +164,20 @@ var Bind = (function Bind(global) {
151164
selector = selector.dom;
152165
}
153166

167+
var elements;
168+
if (typeof selector === 'string') {
169+
// cache the matched elements. Note the :) is because qSA won't allow an
170+
// empty (or undefined) string so I like the smilie.
171+
elements = $(selector || '☺');
172+
} else if (global.Element && selector instanceof global.Element) {
173+
elements = [selector];
174+
}
175+
154176
// look for the path in the mapping arg, and if the gave
155177
// us a callback, use that, otherwise...
156178
if (typeof selector === 'function') {
157179
callback = selector;
158-
} else if (typeof selector === 'string') {
159-
// cache the matched elements. Note the :) is because qSA won't allow an
160-
// empty (or undefined) string so I like the smilie.
161-
var elements = $(selector || '☺');
180+
} else if (elements) {
162181
if (elements.length === 0) {
163182
console.warn('No elements found against "' + selector + '" selector');
164183
}
@@ -167,17 +186,20 @@ var Bind = (function Bind(global) {
167186
// matched from the selector (set up below), that checks
168187
// the node type, and either sets the input.value or
169188
// element.innerHTML to the value
170-
var valueSetters = ['SELECT', 'INPUT', 'PROGRESS', 'TEXTAREA'];
189+
var valueSetters = ['OPTION', 'INPUT', 'PROGRESS', 'TEXTAREA'];
171190

172191
if (value === null || value === undefined) {
173192
if (valueSetters.indexOf(elements[0].nodeName) !== -1) {
174-
value = parse(elements[0].value);
193+
if (elements[0].hasOwnProperty('checked')) {
194+
value = parse(elements[0].value === 'on' ? elements[0].checked : elements[0].value);
195+
} else {
196+
value = parse(elements[0].value);
197+
}
175198
} else {
176199
value = parse(elements[0].innerHTML);
177200
}
178201
}
179202

180-
181203
var oldCallback = callback;
182204
callback = function (value) {
183205
// make it a live selection
@@ -193,7 +215,7 @@ var Bind = (function Bind(global) {
193215
if (valueSetters.indexOf(element.nodeName) !== -1) {
194216
// TODO select[multiple]
195217
// special case for multi-select items
196-
var result = transform(value);
218+
var result = transform(value, target);
197219
if (element.type === 'checkbox') {
198220
if (value instanceof Array) {
199221
var found = value.filter(function (value) {
@@ -205,6 +227,8 @@ var Bind = (function Bind(global) {
205227
if (found.length === 0) {
206228
element.checked = false;
207229
}
230+
} else if (typeof value === 'boolean') {
231+
element.checked = value;
208232
}
209233
} else if (element.type === 'radio') {
210234
element.checked = element.value === result;
@@ -222,11 +246,21 @@ var Bind = (function Bind(global) {
222246
if (!(value instanceof Array)) {
223247
value = [value];
224248
}
225-
var html = '';
249+
var html = [];
250+
226251
forEach(value, function (value) {
227-
html += transform(value);
252+
html.push(transform(value, target));
228253
});
229-
element.innerHTML = html;
254+
// peek the first item, if it's a node, append
255+
// otherwise set the innerHTML
256+
if (typeof html[0] === 'object') {
257+
element.innerHTML = ''; // blow away original
258+
html.forEach(function (el) {
259+
element.appendChild(el);
260+
});
261+
} else {
262+
element.innerHTML = html.join('');
263+
}
230264
}
231265
});
232266
}
@@ -239,27 +273,29 @@ var Bind = (function Bind(global) {
239273
// note that this doesn't support event delegation
240274
forEach(elements, function (element) {
241275
if (element.nodeName === 'INPUT' || element.nodeName === 'SELECT' || element.nodeName === 'TEXTAREA') {
276+
277+
// build up the event handler function
242278
var oninput = function () {
243279
// we set a dirty flag against this dom node to prevent a
244280
// circular update / max stack explode
245281
this.__dirty = true;
246282
var result;
247283
if (element.type === 'checkbox') {
248-
var inputs = (element.form || document).querySelectorAll('input[name="' + element.name + '"][type="' + element.type + '"]');
284+
var inputs = (element.form || document).querySelectorAll('input[name="' + element.name + '"][type="checkbox"]');
249285
if (target[key] instanceof Array) {
250286
var results = [];
251287
forEach(inputs, function (input) {
252288
if (input.checked) {
253-
results.push(parse(input.value));
289+
results.push(parse(input.value === 'on' ? input.checked : input.value));
254290
}
255291
});
256292
result = results;
257293
} else {
258-
result = this.checked ? parse(this.value) : null;
294+
result = parse(this.value === 'on' ? this.checked : this.value);
259295
}
260296
} else {
261297
if (element.type === 'radio') {
262-
result = this.checked ? parse(this.value) : null;
298+
result = parse(this.value === 'on' ? this.checked : this.value);
263299
} if (typeof target[key] === 'number') {
264300
result = parse(this.value * 1);
265301
} else {
@@ -280,6 +316,7 @@ var Bind = (function Bind(global) {
280316
};
281317

282318
var event = {
319+
// select: 'change',
283320
checkbox: 'change',
284321
radio: 'change',
285322
}[element.type];
@@ -356,7 +393,7 @@ var Bind = (function Bind(global) {
356393
}
357394

358395
if (dirty && always) {
359-
var instance = always.instance;
396+
instance = always.instance;
360397
always.callback.call(settings.instance, __export(instance instanceof Array ? [] : {}, instance));
361398
}
362399
};
@@ -383,7 +420,9 @@ var Bind = (function Bind(global) {
383420
value = v;
384421
}
385422

386-
if (debug) console.log('set: key(%s): %s -> %s', key, JSON.stringify(old), JSON.stringify(v));
423+
if (debug) {
424+
console.log('set: key(%s): %s -> %s', key, JSON.stringify(old), JSON.stringify(v));
425+
}
387426

388427
// expose the callback so that child properties can call the
389428
// parent callback function
@@ -398,7 +437,9 @@ var Bind = (function Bind(global) {
398437
findCallback(value);
399438
} else {
400439
// defer the callback until we're fully booted
401-
settings.deferred.push(findCallback.bind(target, value, old));
440+
if (typeof settings.mapping[path.join('.')] !== 'undefined') {
441+
settings.deferred.push(findCallback.bind(target, value, old));
442+
}
402443
}
403444
},
404445
get: function () {
@@ -410,7 +451,9 @@ var Bind = (function Bind(global) {
410451
try {
411452
Object.defineProperty(target, key, definition);
412453
} catch (e) {
413-
// console.log(e.toString(), e.stack);
454+
if (debug) {
455+
console.log('failed on Object.defineProperty', e.toString(), e.stack);
456+
}
414457
}
415458

416459
// finally, set the target aka the returned value's property to the value
@@ -454,7 +497,8 @@ var Bind = (function Bind(global) {
454497
__export(target[key] || {}, value) :
455498
value;
456499
});
457-
} else if (typeof value === o && value !== null && !isArray(value)) {
500+
} else if (typeof value === o && value !== null && !isArray(value) &&
501+
value.toString === '[Object object]') {
458502
target[key] = __export(target[key] || {}, value);
459503
} else {
460504
target[key] = value;
@@ -465,12 +509,13 @@ var Bind = (function Bind(global) {
465509
}
466510

467511

468-
function Bind(obj, mapping) {
512+
function Bind(obj, mapping, context) {
469513
if (!this || this === global) {
470514
return new Bind(obj, mapping);
471515
}
472516

473517
var settings = {
518+
context: context || global.document,
474519
mapping: mapping || {},
475520
callbacks: {},
476521
deferred: [],
@@ -512,4 +557,4 @@ var Bind = (function Bind(global) {
512557

513558
if (typeof exports !== 'undefined') {
514559
module.exports = Bind;
515-
}
560+
}

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
"test": "karma start",
1010
"build": "uglifyjs --mangle --compress -- lib/bind.js > dist/bind.min.js",
1111
"test-node": "mocha test/*.test.js test/**/*.test.js",
12-
"semantic-release": "semantic-release pre && npm publish && semantic-release post"
12+
"semantic-release": "semantic-release pre && npm run build && npm publish && semantic-release post"
1313
},
1414
"repository": {
1515
"type": "git",

test/augmented-array.test.js

Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
/*global describe, assert, beforeEach: true, Bind, sinon, it*/
2+
var sinon = require('sinon');
3+
var assert = require('assert');
4+
var bind = require('../');
5+
6+
describe('augmented array', function () {
7+
'use strict';
8+
var data;
9+
var spy;
10+
11+
beforeEach(function () {
12+
spy = sinon.spy();
13+
14+
data = bind({
15+
list: [1,2,3]
16+
}, {
17+
list: spy,
18+
});
19+
});
20+
21+
it('push', function () {
22+
var cursor = spy.args.length;
23+
var count = spy.callCount;
24+
assert.ok(data.list.length === 3, 'length is right');
25+
data.list.push(4);
26+
assert.ok(spy.callCount === count + 1, 'call count increased');
27+
assert.ok(data.list.length === 4, 'length is right');
28+
assert.ok(spy.args[cursor][0].length === 4, 'passed in was 4');
29+
});
30+
31+
it('pop', function () {
32+
var cursor = spy.args.length;
33+
var count = spy.callCount;
34+
data.list.pop();
35+
assert.ok(spy.callCount === count + 1, 'call count increased');
36+
assert.ok(data.list.length === 2, 'length is right');
37+
assert.ok(spy.args[cursor][0].length === 2, 'new array accepted');
38+
});
39+
40+
it('shift', function () {
41+
var cursor = spy.args.length;
42+
var count = spy.callCount;
43+
var first = data.list.shift();
44+
assert.ok(spy.callCount === count + 1, 'call count increased');
45+
assert.ok(data.list.length === 2, 'length is right');
46+
assert.ok(spy.args[cursor][0].length === 2, 'new array accepted');
47+
assert.ok(first === 1, 'got first value');
48+
});
49+
50+
it('unshift', function () {
51+
var cursor = spy.args.length;
52+
var count = spy.callCount;
53+
data.list.unshift(0);
54+
assert.ok(spy.callCount === count + 1, 'call count increased');
55+
assert.ok(data.list.length === 4, 'length is right');
56+
assert.ok(spy.args[cursor][0].length === 4, 'new array accepted');
57+
assert.ok(spy.args[cursor][0][0] === 0, 'got first value');
58+
});
59+
60+
it('splice', function () {
61+
var cursor = spy.args.length;
62+
var count = spy.callCount;
63+
data.list.splice(1, 1);
64+
assert.ok(spy.callCount === count + 1, 'call count increased');
65+
assert.ok(data.list.length === 2, 'length is right');
66+
assert.ok(spy.args[cursor][0].length === 2, 'new array accepted');
67+
assert.ok(spy.args[cursor][0].toString() === '1,3', 'got first value');
68+
});
69+
});
70+
71+
72+
describe('augmented array containing binds', function () {
73+
'use strict';
74+
var data;
75+
var spy;
76+
77+
beforeEach(function () {
78+
function makeBind(n) {
79+
return bind({
80+
data: n,
81+
}, {
82+
data: function () {}
83+
});
84+
}
85+
86+
spy = sinon.spy();
87+
88+
data = bind({
89+
list: [1,2,3].map(makeBind)
90+
}, {
91+
list: spy,
92+
});
93+
});
94+
95+
it('shift', function () {
96+
var cursor = spy.args.length;
97+
var count = spy.callCount;
98+
data.list.shift();
99+
assert.ok(spy.callCount === count + 1, 'call count increased');
100+
assert.ok(data.list.length === 2, 'length is right');
101+
assert.ok(spy.args[cursor][0].length === 2, 'new array accepted');
102+
var values = data.list.map(function (v) {
103+
return v.data;
104+
}).join(',');
105+
console.log(values);
106+
assert.equal(values, '2,3', 'source data is correct');
107+
});
108+
});

test/checkboxes.browser.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,4 +45,4 @@ describe('checkboxes', function () {
4545

4646

4747
});
48-
});
48+
});

test/radios.browser.js

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,9 @@ describe('radios', function () {
1818
}, {
1919
ts: {
2020
dom: 'input[type=radio]',
21-
callback: spy,
21+
callback: function (arg) {
22+
spy(arg);
23+
},
2224
},
2325
});
2426
});
@@ -46,4 +48,4 @@ describe('radios', function () {
4648

4749

4850
});
49-
});
51+
});

0 commit comments

Comments
 (0)