962107e9 by Michael Richards

Merge pull request #39 from mikeric/adapter-bypass

Additional syntax to bypass the adapter
2 parents 982e2361 15f661c9
......@@ -3,7 +3,7 @@
// author: Michael Richards
// license: MIT
(function() {
var Rivets, attributeBinding, bidirectionals, bindEvent, eventBinding, getInputValue, rivets, unbindEvent,
var Rivets, attributeBinding, bindEvent, eventBinding, getInputValue, rivets, unbindEvent,
__bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; },
__slice = [].slice,
__indexOf = [].indexOf || function(item) { for (var i = 0, l = this.length; i < l; i++) { if (i in this && this[i] === item) return i; } return -1; };
......@@ -18,13 +18,12 @@
Rivets.Binding = (function() {
function Binding(el, type, bindType, model, keypath, formatters) {
function Binding(el, type, model, keypath, options) {
this.el = el;
this.type = type;
this.bindType = bindType;
this.model = model;
this.keypath = keypath;
this.formatters = formatters != null ? formatters : [];
this.options = options != null ? options : {};
this.unbind = __bind(this.unbind, this);
this.publish = __bind(this.publish, this);
......@@ -35,41 +34,52 @@
this.formattedValue = __bind(this.formattedValue, this);
if (this.bindType === "event") {
if (this.options.special === "event") {
this.routine = eventBinding(this.type);
} else {
this.routine = Rivets.routines[this.type] || attributeBinding(this.type);
}
this.formatters = this.options.formatters || [];
}
Binding.prototype.bidirectionals = ['value', 'checked', 'unchecked'];
Binding.prototype.formattedValue = function(value) {
var args, formatter, id, _i, _len, _ref, _ref1;
var args, formatter, id, _i, _len, _ref, _ref1, _ref2;
_ref = this.formatters;
for (_i = 0, _len = _ref.length; _i < _len; _i++) {
formatter = _ref[_i];
args = formatter.split(/\s+/);
id = args.shift();
value = (_ref1 = Rivets.config.formatters)[id].apply(_ref1, [value].concat(__slice.call(args)));
value = Rivets.config.formatters && Rivets.config.formatters[id] ? (_ref1 = Rivets.config.formatters)[id].apply(_ref1, [value].concat(__slice.call(args))) : (_ref2 = this.model)[id].apply(_ref2, [value].concat(__slice.call(args)));
}
return value;
};
Binding.prototype.set = function(value) {
value = this.formattedValue(value);
if (this.bindType === "event") {
if (this.options.special === "event") {
this.routine(this.el, value, this.currentListener);
return this.currentListener = value;
} else {
if (value instanceof Function) {
value = value();
}
return this.routine(this.el, value);
}
};
Binding.prototype.bind = function() {
var _ref;
if (this.options.bypass) {
this.set(this.model[this.keypath]);
} else {
Rivets.config.adapter.subscribe(this.model, this.keypath, this.set);
if (Rivets.config.preloadData) {
this.set(Rivets.config.adapter.read(this.model, this.keypath));
}
if (this.bindType === "bidirectional") {
}
if (_ref = this.type, __indexOf.call(this.bidirectionals, _ref) >= 0) {
return bindEvent(this.el, 'change', this.publish);
}
};
......@@ -81,8 +91,9 @@
};
Binding.prototype.unbind = function() {
var _ref;
Rivets.config.adapter.unsubscribe(this.model, this.keypath, this.set);
if (this.bindType === "bidirectional") {
if (_ref = this.type, __indexOf.call(this.bidirectionals, _ref) >= 0) {
return this.el.removeEventListener('change', this.publish);
}
};
......@@ -127,13 +138,13 @@
bindingRegExp = this.bindingRegExp();
eventRegExp = /^on-/;
parseNode = function(node) {
var attribute, bindType, keypath, model, path, pipe, pipes, type, _i, _len, _ref, _results;
var attribute, keypath, model, options, path, pipe, pipes, type, _i, _len, _ref, _results;
_ref = node.attributes;
_results = [];
for (_i = 0, _len = _ref.length; _i < _len; _i++) {
attribute = _ref[_i];
if (bindingRegExp.test(attribute.name)) {
bindType = "attribute";
options = {};
type = attribute.name.replace(bindingRegExp, '');
pipes = (function() {
var _j, _len1, _ref1, _results1;
......@@ -145,16 +156,16 @@
}
return _results1;
})();
path = pipes.shift().split('.');
path = pipes.shift().split(/(\.|:)/);
options.formatters = pipes;
model = _this.models[path.shift()];
keypath = path.join('.');
options.bypass = path.shift() === ':';
keypath = path.join();
if (eventRegExp.test(type)) {
type = type.replace(eventRegExp, '');
bindType = "event";
} else if (__indexOf.call(bidirectionals, type) >= 0) {
bindType = "bidirectional";
options.special = "event";
}
_results.push(_this.bindings.push(new Rivets.Binding(node, type, bindType, model, keypath, pipes)));
_results.push(_this.bindings.push(new Rivets.Binding(node, type, model, keypath, options)));
} else {
_results.push(void 0);
}
......@@ -256,8 +267,6 @@
};
};
bidirectionals = ['value', 'checked', 'unchecked'];
Rivets.routines = {
enabled: function(el, value) {
return el.disabled = !value;
......
......@@ -2,4 +2,4 @@
// version: 0.2.5
// author: Michael Richards
// license: MIT
(function(){var a,b,c,d,e,f,g,h,i=function(a,b){return function(){return a.apply(b,arguments)}},j=[].slice,k=[].indexOf||function(a){for(var b=0,c=this.length;b<c;b++)if(b in this&&this[b]===a)return b;return-1};a={},String.prototype.trim||(String.prototype.trim=function(){return this.replace(/^\s+|\s+$/g,"")}),a.Binding=function(){function c(c,d,f,g,h,j){this.el=c,this.type=d,this.bindType=f,this.model=g,this.keypath=h,this.formatters=j!=null?j:[],this.unbind=i(this.unbind,this),this.publish=i(this.publish,this),this.bind=i(this.bind,this),this.set=i(this.set,this),this.formattedValue=i(this.formattedValue,this),this.bindType==="event"?this.routine=e(this.type):this.routine=a.routines[this.type]||b(this.type)}return c.prototype.formattedValue=function(b){var c,d,e,f,g,h,i;h=this.formatters;for(f=0,g=h.length;f<g;f++)d=h[f],c=d.split(/\s+/),e=c.shift(),b=(i=a.config.formatters)[e].apply(i,[b].concat(j.call(c)));return b},c.prototype.set=function(a){return a=this.formattedValue(a),this.bindType==="event"?(this.routine(this.el,a,this.currentListener),this.currentListener=a):this.routine(this.el,a)},c.prototype.bind=function(){a.config.adapter.subscribe(this.model,this.keypath,this.set),a.config.preloadData&&this.set(a.config.adapter.read(this.model,this.keypath));if(this.bindType==="bidirectional")return d(this.el,"change",this.publish)},c.prototype.publish=function(b){var c;return c=b.target||b.srcElement,a.config.adapter.publish(this.model,this.keypath,f(c))},c.prototype.unbind=function(){a.config.adapter.unsubscribe(this.model,this.keypath,this.set);if(this.bindType==="bidirectional")return this.el.removeEventListener("change",this.publish)},c}(),a.View=function(){function b(a,b){this.els=a,this.models=b,this.unbind=i(this.unbind,this),this.bind=i(this.bind,this),this.build=i(this.build,this),this.bindingRegExp=i(this.bindingRegExp,this),this.els.jquery||this.els instanceof Array||(this.els=[this.els]),this.build()}return b.prototype.bindingRegExp=function(){var b;return b=a.config.prefix,b?new RegExp("^data-"+b+"-"):/^data-/},b.prototype.build=function(){var b,d,e,f,g,h,i,j,l,m=this;this.bindings=[],b=this.bindingRegExp(),e=/^on-/,g=function(d){var f,g,h,i,j,l,n,o,p,q,r,s;r=d.attributes,s=[];for(p=0,q=r.length;p<q;p++)f=r[p],b.test(f.name)?(g="attribute",o=f.name.replace(b,""),n=function(){var a,b,c,d;c=f.value.split("|"),d=[];for(a=0,b=c.length;a<b;a++)l=c[a],d.push(l.trim());return d}(),j=n.shift().split("."),i=m.models[j.shift()],h=j.join("."),e.test(o)?(o=o.replace(e,""),g="event"):k.call(c,o)>=0&&(g="bidirectional"),s.push(m.bindings.push(new a.Binding(d,o,g,i,h,n)))):s.push(void 0);return s},j=this.els,l=[];for(h=0,i=j.length;h<i;h++)d=j[h],g(d),l.push(function(){var a,b,c,e;c=d.getElementsByTagName("*"),e=[];for(a=0,b=c.length;a<b;a++)f=c[a],e.push(g(f));return e}());return l},b.prototype.bind=function(){var a,b,c,d,e;d=this.bindings,e=[];for(b=0,c=d.length;b<c;b++)a=d[b],e.push(a.bind());return e},b.prototype.unbind=function(){var a,b,c,d,e;d=this.bindings,e=[];for(b=0,c=d.length;b<c;b++)a=d[b],e.push(a.unbind());return e},b}(),d=function(a,b,c){return window.addEventListener?a.addEventListener(b,c):a.attachEvent(b,c)},h=function(a,b,c){return window.removeEventListener?a.removeEventListener(b,c):a.detachEvent(b,c)},f=function(a){switch(a.type){case"text":case"textarea":case"password":case"select-one":case"radio":return a.value;case"checkbox":return a.checked}},e=function(a){return function(b,c,e){c&&d(b,a,c);if(e)return h(b,a,e)}},b=function(a){return function(b,c){return c?b.setAttribute(a,c):b.removeAttribute(a)}},c=["value","checked","unchecked"],a.routines={enabled:function(a,b){return a.disabled=!b},disabled:function(a,b){return a.disabled=!!b},checked:function(a,b){return a.type==="radio"?a.checked=a.value===b:a.checked=!!b},unchecked:function(a,b){return a.type==="radio"?a.checked=a.value!==b:a.checked=!b},show:function(a,b){return a.style.display=b?"":"none"},hide:function(a,b){return a.style.display=b?"none":""},html:function(a,b){return a.innerHTML=b||""},value:function(a,b){return a.value=b||""},text:function(a,b){return a.innerText!=null?a.innerText=b||"":a.textContent=b||""}},a.config={preloadData:!0},g={routines:a.routines,config:a.config,configure:function(b){var c,d,e;b==null&&(b={}),e=[];for(c in b)d=b[c],e.push(a.config[c]=d);return e},bind:function(b,c){var d;return c==null&&(c={}),d=new a.View(b,c),d.bind(),d}},typeof module!="undefined"&&module!==null?module.exports=g:this.rivets=g}).call(this);
\ No newline at end of file
(function(){var a,b,c,d,e,f,g,h=function(a,b){return function(){return a.apply(b,arguments)}},i=[].slice,j=[].indexOf||function(a){for(var b=0,c=this.length;b<c;b++)if(b in this&&this[b]===a)return b;return-1};a={},String.prototype.trim||(String.prototype.trim=function(){return this.replace(/^\s+|\s+$/g,"")}),a.Binding=function(){function f(c,e,f,g,i){this.el=c,this.type=e,this.model=f,this.keypath=g,this.options=i!=null?i:{},this.unbind=h(this.unbind,this),this.publish=h(this.publish,this),this.bind=h(this.bind,this),this.set=h(this.set,this),this.formattedValue=h(this.formattedValue,this),this.options.special==="event"?this.routine=d(this.type):this.routine=a.routines[this.type]||b(this.type),this.formatters=this.options.formatters||[]}return f.prototype.bidirectionals=["value","checked","unchecked"],f.prototype.formattedValue=function(b){var c,d,e,f,g,h,j,k;h=this.formatters;for(f=0,g=h.length;f<g;f++)d=h[f],c=d.split(/\s+/),e=c.shift(),b=a.config.formatters&&a.config.formatters[e]?(j=a.config.formatters)[e].apply(j,[b].concat(i.call(c))):(k=this.model)[e].apply(k,[b].concat(i.call(c)));return b},f.prototype.set=function(a){return a=this.formattedValue(a),this.options.special==="event"?(this.routine(this.el,a,this.currentListener),this.currentListener=a):(a instanceof Function&&(a=a()),this.routine(this.el,a))},f.prototype.bind=function(){var b;this.options.bypass?this.set(this.model[this.keypath]):(a.config.adapter.subscribe(this.model,this.keypath,this.set),a.config.preloadData&&this.set(a.config.adapter.read(this.model,this.keypath)));if(b=this.type,j.call(this.bidirectionals,b)>=0)return c(this.el,"change",this.publish)},f.prototype.publish=function(b){var c;return c=b.target||b.srcElement,a.config.adapter.publish(this.model,this.keypath,e(c))},f.prototype.unbind=function(){var b;a.config.adapter.unsubscribe(this.model,this.keypath,this.set);if(b=this.type,j.call(this.bidirectionals,b)>=0)return this.el.removeEventListener("change",this.publish)},f}(),a.View=function(){function b(a,b){this.els=a,this.models=b,this.unbind=h(this.unbind,this),this.bind=h(this.bind,this),this.build=h(this.build,this),this.bindingRegExp=h(this.bindingRegExp,this),this.els.jquery||this.els instanceof Array||(this.els=[this.els]),this.build()}return b.prototype.bindingRegExp=function(){var b;return b=a.config.prefix,b?new RegExp("^data-"+b+"-"):/^data-/},b.prototype.build=function(){var b,c,d,e,f,g,h,i,j,k=this;this.bindings=[],b=this.bindingRegExp(),d=/^on-/,f=function(c){var e,f,g,h,i,j,l,m,n,o,p,q;p=c.attributes,q=[];for(n=0,o=p.length;n<o;n++)e=p[n],b.test(e.name)?(h={},m=e.name.replace(b,""),l=function(){var a,b,c,d;c=e.value.split("|"),d=[];for(a=0,b=c.length;a<b;a++)j=c[a],d.push(j.trim());return d}(),i=l.shift().split(/(\.|:)/),h.formatters=l,g=k.models[i.shift()],h.bypass=i.shift()===":",f=i.join(),d.test(m)&&(m=m.replace(d,""),h.special="event"),q.push(k.bindings.push(new a.Binding(c,m,g,f,h)))):q.push(void 0);return q},i=this.els,j=[];for(g=0,h=i.length;g<h;g++)c=i[g],f(c),j.push(function(){var a,b,d,g;d=c.getElementsByTagName("*"),g=[];for(a=0,b=d.length;a<b;a++)e=d[a],g.push(f(e));return g}());return j},b.prototype.bind=function(){var a,b,c,d,e;d=this.bindings,e=[];for(b=0,c=d.length;b<c;b++)a=d[b],e.push(a.bind());return e},b.prototype.unbind=function(){var a,b,c,d,e;d=this.bindings,e=[];for(b=0,c=d.length;b<c;b++)a=d[b],e.push(a.unbind());return e},b}(),c=function(a,b,c){return window.addEventListener?a.addEventListener(b,c):a.attachEvent(b,c)},g=function(a,b,c){return window.removeEventListener?a.removeEventListener(b,c):a.detachEvent(b,c)},e=function(a){switch(a.type){case"text":case"textarea":case"password":case"select-one":case"radio":return a.value;case"checkbox":return a.checked}},d=function(a){return function(b,d,e){d&&c(b,a,d);if(e)return g(b,a,e)}},b=function(a){return function(b,c){return c?b.setAttribute(a,c):b.removeAttribute(a)}},a.routines={enabled:function(a,b){return a.disabled=!b},disabled:function(a,b){return a.disabled=!!b},checked:function(a,b){return a.type==="radio"?a.checked=a.value===b:a.checked=!!b},unchecked:function(a,b){return a.type==="radio"?a.checked=a.value!==b:a.checked=!b},show:function(a,b){return a.style.display=b?"":"none"},hide:function(a,b){return a.style.display=b?"none":""},html:function(a,b){return a.innerHTML=b||""},value:function(a,b){return a.value=b||""},text:function(a,b){return a.innerText!=null?a.innerText=b||"":a.textContent=b||""}},a.config={preloadData:!0},f={routines:a.routines,config:a.config,configure:function(b){var c,d,e;b==null&&(b={}),e=[];for(c in b)d=b[c],e.push(a.config[c]=d);return e},bind:function(b,c){var d;return c==null&&(c={}),d=new a.View(b,c),d.bind(),d}},typeof module!="undefined"&&module!==null?module.exports=f:this.rivets=f}).call(this);
\ No newline at end of file
......
......@@ -22,6 +22,41 @@ describe('Rivets.Binding', function() {
expect(binding.routine).toBe(rivets.routines.text);
});
describe('bind()', function() {
it('subscribes to the model for changes via the adapter', function() {
spyOn(rivets.config.adapter, 'subscribe');
binding.bind();
expect(rivets.config.adapter.subscribe).toHaveBeenCalled();
});
describe('with preloadData set to true', function() {
beforeEach(function() {
rivets.config.preloadData = true;
});
it('sets the initial value via the adapter', function() {
spyOn(binding, 'set');
spyOn(rivets.config.adapter, 'read');
binding.bind();
expect(binding.set).toHaveBeenCalled();
expect(rivets.config.adapter.read).toHaveBeenCalled();
});
});
describe('with the bypass option set to true', function() {
beforeEach(function() {
binding.options.bypass = true;
});
it('sets the initial value from the model directly', function() {
spyOn(binding, 'set');
binding.model.name = 'espresso';
binding.bind();
expect(binding.set).toHaveBeenCalledWith('espresso');
});
});
});
describe('set()', function() {
it('performs the binding routine with the supplied value', function() {
spyOn(binding, 'routine');
......@@ -41,7 +76,7 @@ describe('Rivets.Binding', function() {
describe('on an event binding', function() {
beforeEach(function() {
binding.bindType = 'event';
binding.options.special = 'event';
});
it('performs the binding routine with the supplied function and current listener', function() {
......
......@@ -12,14 +12,20 @@ unless String::trim then String::trim = -> @replace /^\s+|\s+$/g, ""
# A single binding between a model attribute and a DOM element.
class Rivets.Binding
# All information about the binding is passed into the constructor; the DOM
# element, the routine identifier, the model object and the keypath at which
# element, the type of binding, the model object and the keypath at which
# to listen for changes.
constructor: (@el, @type, @bindType, @model, @keypath, @formatters = []) ->
if @bindType is "event"
constructor: (@el, @type, @model, @keypath, @options = {}) ->
if @options.special is "event"
@routine = eventBinding @type
else
@routine = Rivets.routines[@type] || attributeBinding @type
@formatters = @options.formatters || []
# Bindings that should also observe the DOM element for changes in order to
# propagate those changes back to the model object.
bidirectionals: ['value', 'checked', 'unchecked']
# Applies all the current formatters to the supplied value and returns the
# formatted value.
formattedValue: (value) =>
......@@ -38,22 +44,26 @@ class Rivets.Binding
set: (value) =>
value = @formattedValue value
if @bindType is "event"
if @options.special is "event"
@routine @el, value, @currentListener
@currentListener = value
else
value = value() if value instanceof Function
@routine @el, value
# Subscribes to the model for changes at the specified keypath. Bi-directional
# routines will also listen for changes on the element to propagate them back
# to the model.
bind: =>
if @options.bypass
@set @model[@keypath]
else
Rivets.config.adapter.subscribe @model, @keypath, @set
if Rivets.config.preloadData
@set Rivets.config.adapter.read @model, @keypath
if @bindType is "bidirectional"
if @type in @bidirectionals
bindEvent @el, 'change', @publish
# Publishes the value currently set on the input element back to the model.
......@@ -65,7 +75,7 @@ class Rivets.Binding
unbind: =>
Rivets.config.adapter.unsubscribe @model, @keypath, @set
if @bindType is "bidirectional"
if @type in @bidirectionals
@el.removeEventListener 'change', @publish
# A collection of bindings built from a set of parent elements.
......@@ -90,20 +100,21 @@ class Rivets.View
parseNode = (node) =>
for attribute in node.attributes
if bindingRegExp.test attribute.name
bindType = "attribute"
options = {}
type = attribute.name.replace bindingRegExp, ''
pipes = (pipe.trim() for pipe in attribute.value.split '|')
path = pipes.shift().split '.'
path = pipes.shift().split(/(\.|:)/)
options.formatters = pipes
model = @models[path.shift()]
keypath = path.join '.'
options.bypass = path.shift() is ':'
keypath = path.join()
if eventRegExp.test type
type = type.replace eventRegExp, ''
bindType = "event"
else if type in bidirectionals
bindType = "bidirectional"
options.special = "event"
@bindings.push new Rivets.Binding node, type, bindType, model, keypath, pipes
@bindings.push new Rivets.Binding node, type, model, keypath, options
for el in @els
parseNode el
......@@ -140,7 +151,7 @@ getInputValue = (el) ->
when 'text', 'textarea', 'password', 'select-one', 'radio' then el.value
when 'checkbox' then el.checked
# Returns an element binding routine for the specified attribute.
# Returns an event binding routine for the specified event.
eventBinding = (event) -> (el, bind, unbind) ->
bindEvent el, event, bind if bind
unbindEvent el, event, unbind if unbind
......@@ -150,10 +161,6 @@ eventBinding = (event) -> (el, bind, unbind) ->
attributeBinding = (attr) -> (el, value) ->
if value then el.setAttribute attr, value else el.removeAttribute attr
# Bindings that should also be observed for changes on the DOM element in order
# to propagate those changes back to the model object.
bidirectionals = ['value', 'checked', 'unchecked']
# Core binding routines.
Rivets.routines =
enabled: (el, value) ->
......