Skip to content

Commit

Permalink
added basic functionality to binding class
Browse files Browse the repository at this point in the history
pertains to #20
  • Loading branch information
aaronj1335 committed Aug 14, 2012
1 parent f084908 commit 728a7bc
Show file tree
Hide file tree
Showing 3 changed files with 239 additions and 7 deletions.
138 changes: 135 additions & 3 deletions src/util/binding.js
@@ -1,6 +1,13 @@
define([
'bedrock/class'
], function(Class) {
'vendor/underscore',
'vendor/t',
'bedrock/class',
'bedrock/settable',
'./../widgets/widget',
'./../widgets/formwidget'
], function(_, t, Class, Settable, Widget, FormWidget) {
var textContent = document.createElement('span').textContent?
'textContent' : 'innerText';

// # Binding
//
Expand Down Expand Up @@ -80,8 +87,133 @@ define([
// widget and the model field corresponding to the widget's element's
// 'name' attribute
//
// ### stuff that `Binding` does not support
//
// - changing the UI (either the `widget` or the `el`): i mean.. this
// would be kind of silly, right?
//
// [formwidget]: https://github.com/siq/gloss/blob/master/src/widgets/formwidget.js
//
return Class.extend({
});
_settableOnChange: '_onOptionsChange',

init: function(options) {
var explicitBindings = options.bindings;

_.bindAll(this, '_onModelChange');

delete (options = _.extend({}, options)).bindings;

this.set(options);

this._setBindings(explicitBindings);
},

_onModelChange: function(eventName, model, changed) {
var self = this, bindings = self.get('bindings');

if (!bindings) {
return;
}

_.each(changed, function(___, prop) {
if (bindings[prop]) {
self._setUIFromModelForBinding(prop);
}
});
},

_onOptionsChange: function(changed, opts) {
if (changed.model) {
this.get('model').on('change', this._onModelChange);
if (this.previous('model')) {
this.previous('model').off('change', this._onModelChange);
}
this._setUIFromModel();
}

if (changed.el || changed.widget) {
if (this.get('el') && this.get('widget')) {
throw Error(
'Binding object has either `widget` or `el`, not both');
}

}
},

// this walks through the DOM element (either from `widgt` or `el`) and
// sets up bindings for everything it finds. this is where we
// implement the algorithm for 'automatic binding'
_setBindings: function(explicitBindings) {
var el, widget, bindings = {},
root = (widget = this.get('widget'))? widget.node :
(el = this.get('el'))? el.jquery && el[0] :
el,

// set up the binding, overriding any automatically discovered
// settings w/ the explicit settings
setUpBinding = function(bindings, name, newBinding, explicit) {
bindings[name] = explicit && explicit[name]?
_.extend(newBinding, explicit[name]) : newBinding;
};

t.dfs(root, function(el, parentEl, ctrl) {
var id, widget, name, newBinding,
dataBind = el.getAttribute('data-bind');

if (dataBind) {
setUpBinding(bindings, dataBind, {el: el},
explicitBindings);

// dont traverse any further into this DOM node
ctrl.cutoff = true;

} else if (
(widget = Widget.registry.get(el.getAttribute('id'))) &&
widget instanceof FormWidget) {

setUpBinding(bindings, widget.$node.attr('name'),
{widget: widget}, explicitBindings);
}
});

// add any bindings that were in the explicitBindings, but not
// discovered during the automatic binding
bindings = _.reduce(explicitBindings, function(bindings, binding, name) {
if (!bindings[name]) {
if (binding.el && binding.el.jquery) {
binding.el = binding.el[0];
}
bindings[name] = binding;
}
return bindings;
}, bindings);

this.set('bindings', bindings);

this._setUIFromModel();
},

// update the UI with all of the values from the model
_setUIFromModel: function() {
var self = this;
_.each(self.get('bindings') || [], function(___, prop) {
self._setUIFromModelForBinding(prop);
});
},

// update the UI with the value from the model for a specific binding
// (i.e. just update one field)
_setUIFromModelForBinding: function(binding) {
var bindings = this.get('bindings');
if (bindings[binding].el) {
bindings[binding].el[textContent] =
this.get('model').get(binding) || '';
} else if (bindings[binding].widget) {
bindings[binding].widget.$el.text(
this.get('model').get(binding) || '');
}
}

}, {mixins: [Settable]});
});
104 changes: 100 additions & 4 deletions src/util/binding/test.js
@@ -1,10 +1,106 @@
/*global test, asyncTest, ok, equal, deepEqual, start, module, strictEqual, notStrictEqual, raises*/
define([
'./../binding'
], function(Binding) {
'vendor/jquery',
'vendor/underscore',
'mesh/model',
'./../binding',
'text!./testDataBindingAttribute.html'
], function($, _, Model, Binding, testDataBindHtml) {

test('instantiation', function() {
ok(Binding);
var assertThatModelMatchesUI = function(binding) {
var model = binding.get('model');
_.each(binding.get('bindings'), function(b, name) {
if (b.el) {
equal($(b.el).text(), model.get(name) || '',
'element with `data-bind="'+name+'"` text is "'+model.get(name)+'"');
}
});
};

module('use cases');

test('explicit binding', function() {
var $el = $('<span class=bound-field></span>').appendTo('#qunit-fixture'),
myModel = Model.Model({myField: 'foo'}),
binding = Binding({
model: myModel,
bindings: {

// this is the model's field name, '.' is expanded to
// nested model fields
'myField': {

// any HTML snippet, either jQuery collection or
// bare HTMLElement
el: $el

}
}
});

$('#qunit-fixture').css({position: 'static'});

assertThatModelMatchesUI(binding);

equal(_.keys(binding.get('bindings')).length, 1);

equal($el.text(), 'foo');

});

test('automatic binding to some fields in an HTML snippet', function() {
var $el = $(testDataBindHtml).appendTo('#qunit-fixture'),
myModel = Model.Model({field1: 'before set'}),
binding = Binding({
el: $el,
model: myModel
});

$('#qunit-fixture').css({position: 'static'});

ok(binding);

assertThatModelMatchesUI(binding);
equal(_.keys(binding.get('bindings')).length, 2);

myModel.set('field1', 'foo1');
myModel.set('field2', 'foo2');

assertThatModelMatchesUI(binding);
});

module('corner cases');

test('nested attribute', function() {
var $el = $('<span class=bound-field></span>').appendTo('#qunit-fixture'),
myModel = Model.Model({
myModelField: {
subField: 'foo'
}
}),
binding = Binding({
model: myModel,
bindings: {

// this is the model's field name, '.' is expanded to
// nested model fields
'myModelField.subField': {

// any HTML snippet, either jQuery collection or
// bare HTMLElement
el: $el

}
}
});

$('#qunit-fixture').css({position: 'static'});

assertThatModelMatchesUI(binding);

equal(_.keys(binding.get('bindings')).length, 1);

equal($el.text(), 'foo');
});

start();
Expand Down
4 changes: 4 additions & 0 deletions src/util/binding/testDataBindingAttribute.html
@@ -0,0 +1,4 @@
<div>
<span data-bind=field1></span>
<span data-bind=field2></span>
</div>

0 comments on commit 728a7bc

Please sign in to comment.