Skip to content

Commit

Permalink
feat: support nested bindings
Browse files Browse the repository at this point in the history
This change is only feature changes (and subtle fixes), to fully support a todomvc demo using bind.js
  • Loading branch information
remy committed Apr 4, 2016
1 parent 5bf3eff commit 089ee6c
Show file tree
Hide file tree
Showing 7 changed files with 262 additions and 30 deletions.
97 changes: 71 additions & 26 deletions lib/bind.js
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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';
Expand Down Expand Up @@ -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
Expand All @@ -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) {
Expand All @@ -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');
}
Expand All @@ -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
Expand All @@ -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) {
Expand All @@ -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;
Expand All @@ -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('');
}
}
});
}
Expand All @@ -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 {
Expand All @@ -280,6 +316,7 @@ var Bind = (function Bind(global) {
};

var event = {
// select: 'change',
checkbox: 'change',
radio: 'change',
}[element.type];
Expand Down Expand Up @@ -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));
}
};
Expand All @@ -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
Expand All @@ -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 () {
Expand All @@ -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
Expand Down Expand Up @@ -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]') {

This comment has been minimized.

Copy link
@remy

remy Aug 20, 2016

Author Owner

@precious do you mean this line?

This comment has been minimized.

Copy link
@precious

precious Aug 20, 2016

@remy yes, this one

target[key] = __export(target[key] || {}, value);
} else {
target[key] = value;
Expand All @@ -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: [],
Expand Down Expand Up @@ -512,4 +557,4 @@ var Bind = (function Bind(global) {

if (typeof exports !== 'undefined') {
module.exports = Bind;
}
}
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
108 changes: 108 additions & 0 deletions test/augmented-array.test.js
Original file line number Diff line number Diff line change
@@ -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');
});
});
2 changes: 1 addition & 1 deletion test/checkboxes.browser.js
Original file line number Diff line number Diff line change
Expand Up @@ -45,4 +45,4 @@ describe('checkboxes', function () {


});
});
});
6 changes: 4 additions & 2 deletions test/radios.browser.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,9 @@ describe('radios', function () {
}, {
ts: {
dom: 'input[type=radio]',
callback: spy,
callback: function (arg) {
spy(arg);
},
},
});
});
Expand Down Expand Up @@ -46,4 +48,4 @@ describe('radios', function () {


});
});
});
Loading

1 comment on commit 089ee6c

@precious
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hello,

Is this statement correct
value.toString === '[Object object]'
?
Seems like missed parentheses for method call.

Please sign in to comment.