e63ff384 by Nicklas Ansman Giertz

Merge remote-tracking branch 'upstream/master' into feature/tests

Conflicts:
	build/rivets.js
	src/rivets.coffee
2 parents 9b284cf9 b5e51cfc
1 # Rivets.js 1 # Rivets.js
2 2
3 Rivets.js is a declarative, observer-based DOM-binding facility that plays well with existing frameworks such as [Backbone.js](http://backbonejs.org), [Spine.js](http://spinejs.com) and [Stapes.js](http://hay.github.com/stapes/). It aims to be lightweight (1.2KB minified and gzipped), extensible, and configurable to work with any event-driven model. 3 Rivets.js is a declarative data binding facility that plays well with existing frameworks such as [Backbone.js](http://backbonejs.org), [Spine.js](http://spinejs.com) and [Stapes.js](http://hay.github.com/stapes/). It aims to be lightweight (1.2KB minified and gzipped), extensible, and configurable to work with any event-driven model.
4 4
5 --- 5 ---
6 6
7 Describe your UI directly in the DOM using data attributes: 7 Describe your UI in plain HTML using data attributes:
8 8
9 <div id='auction'> 9 <div id='auction'>
10 <h1 data-text='auction.title'></h1> 10 <h1 data-text='auction.title'></h1>
...@@ -28,11 +28,11 @@ Describe your UI directly in the DOM using data attributes: ...@@ -28,11 +28,11 @@ Describe your UI directly in the DOM using data attributes:
28 </dl> 28 </dl>
29 </div> 29 </div>
30 30
31 Then tell Rivets.js what model(s) to bind to what part of the DOM: 31 Then tell Rivets.js what model(s) to bind to it:
32 32
33 rivets.bind($('auction'), {auction: auction, user: currentUser}); 33 rivets.bind($('#auction'), {auction: auction, user: currentUser});
34 34
35 ## Configuring 35 ## Configure
36 36
37 Use `rivets.configure` to configure Rivets.js for your app. There are a few handy configuration options, such as setting the data attribute prefix and adding formatters that you can pipe binding values to, but setting the adapter is the only required configuration since Rivets.js needs to know how to observe your models for changes as they happen. 37 Use `rivets.configure` to configure Rivets.js for your app. There are a few handy configuration options, such as setting the data attribute prefix and adding formatters that you can pipe binding values to, but setting the adapter is the only required configuration since Rivets.js needs to know how to observe your models for changes as they happen.
38 38
...@@ -75,9 +75,9 @@ To prevent data attribute collision, you can set the `prefix` option to somethin ...@@ -75,9 +75,9 @@ To prevent data attribute collision, you can set the `prefix` option to somethin
75 75
76 Set the `preloadData` option to `true` or `false` depending on if you want the binding routines to run immediately after the initial binding or not — if set to false, the binding routines will only run when the attribute value is updated. 76 Set the `preloadData` option to `true` or `false` depending on if you want the binding routines to run immediately after the initial binding or not — if set to false, the binding routines will only run when the attribute value is updated.
77 77
78 ## Extending 78 ## Extend
79 79
80 You can extend Rivets.js by adding your own custom data bindings (routines). Just pass `rivets.register` an identifier for the routine and routine function. Routine functions take two arguments, `el` which is the DOM element and `value` which is the incoming value of the attribute being bound to. 80 You can extend Rivets.js by adding your own custom data bindings (routines). Just pass `rivets.register` an identifier and a routine function. Routine functions take two arguments, `el` which is the DOM element and `value` which is the incoming value of the attribute being bound to.
81 81
82 So let's say we wanted a `data-color` binding that sets the element's colour. Here's what that might look like: 82 So let's say we wanted a `data-color` binding that sets the element's colour. Here's what that might look like:
83 83
...@@ -97,4 +97,5 @@ So let's say we wanted a `data-color` binding that sets the element's colour. He ...@@ -97,4 +97,5 @@ So let's say we wanted a `data-color` binding that sets the element's colour. He
97 - data-checked 97 - data-checked
98 - data-unchecked 98 - data-unchecked
99 - data-selected 99 - data-selected
100 - data-[attribute]
...\ No newline at end of file ...\ No newline at end of file
100 - data-*[attribute]*
101 - data-on-*[event]*
...\ No newline at end of file ...\ No newline at end of file
......
1 (function(){var m,c,d,g,i,h,n,f,j=function(b,a){return function(){return b.apply(a,arguments)}},o=[].indexOf||function(b){for(var a=0,p=this.length;a<p;a++)if(a in this&&this[a]===b)return a;return-1};g=d=c=m=void 0;m=function(){function b(a,b,e,l,k){this.el=a;this.type=b;this.model=e;this.keypath=l;this.formatters=null!=k?k:[];this.unbind=j(this.unbind,this);this.publish=j(this.publish,this);this.bind=j(this.bind,this);this.set=j(this.set,this);this.routine=c[this.type]||i(this.type)}b.prototype.set= 1 (function(){var p,h,c,k,r,s,l,t,u,j,v,e=function(a,b){return function(){return a.apply(b,arguments)}},x=[].indexOf||function(a){for(var b=0,w=this.length;b<w;b++)if(b in this&&this[b]===a)return b;return-1};k=c=h=p=void 0;String.prototype.trim||(String.prototype.trim=function(){return this.replace(/^\s+|\s+$/g,"")});p=function(){function a(b,a,d,i,f,g){this.el=b;this.type=a;this.bindType=d;this.model=i;this.keypath=f;this.formatters=g!=null?g:[];this.publish=e(this.publish,this);this.bind=e(this.bind,
2 function(a){var b,e,l,k;k=this.formatters;e=0;for(l=k.length;e<l;e++)b=k[e],a=d.formatters[b](a);return this.routine(this.el,a)};b.prototype.bind=function(){var a;d.adapter.subscribe(this.model,this.keypath,this.set);d.preloadData&&this.set(d.adapter.read(this.model,this.keypath));if(a=this.type,0<=o.call(h,a))return this.el.addEventListener("change",this.publish)};b.prototype.publish=function(a){return d.adapter.publish(this.model,this.keypath,n(a.target||a.srcElement))};b.prototype.unbind=function(){var a; 2 this);this.set=e(this.set,this);this.routine=this.bindType==="event"?t(this.type):h[this.type]||r(this.type)}a.prototype.set=function(b){var a,d,i,f;f=this.formatters;d=0;for(i=f.length;d<i;d++){a=f[d];b=c.formatters[a](b)}if(this.bindType==="event"){this.routine(this.el,b,this.currentListener);return this.currentListener=b}return this.routine(this.el,b)};a.prototype.bind=function(){c.adapter.subscribe(this.model,this.keypath,this.set);c.preloadData&&this.set(c.adapter.read(this.model,this.keypath));
3 d.adapter.unsubscribe(this.model,this.keypath,this.set);if(a=this.type,0<=o.call(h,a))return this.el.removeEventListener("change",this.publish)};return b}();g=function(){function b(a,b){this.el=a;this.models=b;this.bind=j(this.bind,this);this.build=j(this.build,this);this.bindingRegExp=j(this.bindingRegExp,this);this.build()}b.prototype.bindingRegExp=function(){var a;return(a=d.prefix)?RegExp("^data-"+a+"-"):/^data-/};b.prototype.build=function(){var a,b,e,l,k,d,f,j,c,g,i,h;this.bindings=[];b=this.bindingRegExp(); 3 if(this.bindType==="bidirectional")return l(this.el,"change",this.publish)};a.prototype.publish=function(b){return c.adapter.publish(this.model,this.keypath,u(b.target||b.srcElement))};return a}();k=function(){function a(b,a){this.el=b;this.models=a;this.bind=e(this.bind,this);this.build=e(this.build,this);this.bindingRegExp=e(this.bindingRegExp,this);if(this.el.jquery)this.el=this.el.get(0);this.build()}a.prototype.bindingRegExp=function(){var b;return(b=c.prefix)?RegExp("^data-"+b+"-"):/^data-/};
4 i=this.el.getElementsByTagName("*");h=[];c=0;for(g=i.length;c<g;c++)k=i[c],h.push(function(){var c,i,h,g;h=k.attributes;g=[];c=0;for(i=h.length;c<i;c++)a=h[c],b.test(a.name)?(j=a.name.replace(b,""),f=a.value.split("|").map(function(a){return a.trim()}),d=f.shift().split("."),l=this.models[d.shift()],e=d.join("."),g.push(this.bindings.push(new m(k,j,l,e,f)))):g.push(void 0);return g}.call(this));return h};b.prototype.bind=function(){var a,b,e,c,d;c=this.bindings;d=[];b=0;for(e=c.length;b<e;b++)a=c[b], 4 a.prototype.build=function(){var b,a,d,i,f,g,c,e,m,n,h,o,k,l,j;this.bindings=[];d=this.bindingRegExp();f=/^on-/;i=[this.el];i.concat(Array.prototype.slice.call(this.el.getElementsByTagName("*")));h=0;for(k=i.length;h<k;h++){e=i[h];j=e.attributes;o=0;for(l=j.length;o<l;o++){b=j[o];if(d.test(b.name)){a="attribute";n=b.name.replace(d,"");var q=g=m=c=void 0;g=b.value.split("|");q=[];c=0;for(m=g.length;c<m;c++){b=g[c];q.push(b.trim())}m=q;g=m.shift().split(".");c=this.models[g.shift()];g=g.join(".");if(f.test(n)){n=
5 d.push(a.bind());return d};return b}();n=function(b){switch(b.type){case "text":case "textarea":case "password":case "select-one":return b.value;case "checkbox":case "radio":return b.checked}};i=function(b){return function(a,c){return c?a.setAttribute(b,c):a.removeAttribute(b)}};f=function(b,a){null==a&&(a=!1);return function(c,d){return i(b)(c,a===!d?b:!1)}};h=["value","checked","unchecked","selected","unselected"];c={checked:f("checked"),selected:f("selected"),disabled:f("disabled"),unchecked:f("checked", 5 n.replace(f,"");a="event"}else x.call(s,n)>=0&&(a="bidirectional");this.bindings.push(new p(e,n,a,c,g,m))}}}};a.prototype.bind=function(){var b,a,d,c,f;c=this.bindings;f=[];a=0;for(d=c.length;a<d;a++){b=c[a];f.push(b.bind())}return f};return a}();l=function(a,b,c){return window.addEventListener?a.addEventListener(b,c):a.attachEvent(b,c)};v=function(a,b,c){return window.removeEventListener?a.removeEventListener(b,c):a.detachEvent(b,c)};u=function(a){switch(a.type){case "text":case "textarea":case "password":case "select-one":return a.value;
6 !0),unselected:f("selected",!0),enabled:f("disabled",!0),text:function(b,a){return null!=b.innerText?b.innerText=a||"":b.textContent=a||""},html:function(b,a){return b.innerHTML=a||""},value:function(b,a){return b.value=a},show:function(b,a){return b.style.display=a?"":"none"},hide:function(b,a){return b.style.display=a?"none":""}};d={preloadData:!0};f={configure:function(b){var a,c,e;null==b&&(b={});e=[];for(a in b)c=b[a],e.push(d[a]=c);return e},register:function(b,a){return c[b]=a},bind:function(b, 6 case "checkbox":case "radio":return a.checked}};t=function(a){return function(b,c,d){c&&l(b,a,c);if(d)return v(b,a,d)}};r=function(a){return function(b,c){return c?b.setAttribute(a,c):b.removeAttribute(a)}};s=["value","checked","unchecked","selected","unselected"];h={enabled:function(a,b){return a.disabled=!b},disabled:function(a,b){return a.disabled=!!b},checked:function(a,b){return a.checked=!!b},unchecked:function(a,b){return a.checked=!b},selected:function(a,b){return a.selected=!!b},unselected:function(a,
7 a){var c;null==a&&(a={});c=new g(b,a);c.bind();return c}};"undefined"!==typeof module&&null!==module?module.exports=f:this.rivets=f}).call(this); 7 b){return a.selected=!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||""}};c={preloadData:!0};j={configure:function(a){var b,e,d;a==null&&(a={});d=[];for(b in a){e=a[b];d.push(c[b]=e)}return d},register:function(a,b){return h[a]=b},bind:function(a,b){var c;b==null&&(b={});c=
8 new k(a,b);c.bind();return c}};"undefined"!==typeof module&&null!==module?module.exports=j:this.rivets=j}).call(this);
......
1 (function() { 1 (function() {
2 var Rivets, attributeBinding, bidirectionals, getInputValue, rivets, stateBinding, 2 var Rivets, attributeBinding, bidirectionals, bindEvent, eventBinding, getInputValue, rivets, unbindEvent,
3 __bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; }, 3 __bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; },
4 __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; }; 4 __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; };
5 5
6 Rivets = {}; 6 Rivets = {};
7 7
8 if (!String.prototype.trim) {
9 String.prototype.trim = function() {
10 return this.replace(/^\s+|\s+$/g, "");
11 };
12 }
13
8 Rivets.Binding = (function() { 14 Rivets.Binding = (function() {
9 15
10 function Binding(el, type, model, keypath, formatters) { 16 function Binding(el, type, bindType, model, keypath, formatters) {
11 this.el = el; 17 this.el = el;
12 this.type = type; 18 this.type = type;
19 this.bindType = bindType;
13 this.model = model; 20 this.model = model;
14 this.keypath = keypath; 21 this.keypath = keypath;
15 this.formatters = formatters != null ? formatters : []; 22 this.formatters = formatters != null ? formatters : [];
16 this.unbind = __bind(this.unbind, this);
17
18 this.publish = __bind(this.publish, this); 23 this.publish = __bind(this.publish, this);
19 24
20 this.bind = __bind(this.bind, this); 25 this.bind = __bind(this.bind, this);
21 26
22 this.set = __bind(this.set, this); 27 this.set = __bind(this.set, this);
23 28
24 this.routine = Rivets.routines[this.type] || attributeBinding(this.type); 29 if (this.bindType === "event") {
30 this.routine = eventBinding(this.type);
31 } else {
32 this.routine = Rivets.routines[this.type] || attributeBinding(this.type);
33 }
25 } 34 }
26 35
27 Binding.prototype.set = function(value) { 36 Binding.prototype.set = function(value) {
...@@ -31,17 +40,21 @@ ...@@ -31,17 +40,21 @@
31 formatter = _ref[_i]; 40 formatter = _ref[_i];
32 value = Rivets.config.formatters[formatter](value); 41 value = Rivets.config.formatters[formatter](value);
33 } 42 }
34 return this.routine(this.el, value); 43 if (this.bindType === "event") {
44 this.routine(this.el, value, this.currentListener);
45 return this.currentListener = value;
46 } else {
47 return this.routine(this.el, value);
48 }
35 }; 49 };
36 50
37 Binding.prototype.bind = function() { 51 Binding.prototype.bind = function() {
38 var _ref;
39 Rivets.config.adapter.subscribe(this.model, this.keypath, this.set); 52 Rivets.config.adapter.subscribe(this.model, this.keypath, this.set);
40 if (Rivets.config.preloadData) { 53 if (Rivets.config.preloadData) {
41 this.set(Rivets.config.adapter.read(this.model, this.keypath)); 54 this.set(Rivets.config.adapter.read(this.model, this.keypath));
42 } 55 }
43 if (_ref = this.type, __indexOf.call(bidirectionals, _ref) >= 0) { 56 if (this.bindType === "bidirectional") {
44 return this.el.addEventListener('change', this.publish); 57 return bindEvent(this.el, 'change', this.publish);
45 } 58 }
46 }; 59 };
47 60
...@@ -51,14 +64,6 @@ ...@@ -51,14 +64,6 @@
51 return Rivets.config.adapter.publish(this.model, this.keypath, getInputValue(el)); 64 return Rivets.config.adapter.publish(this.model, this.keypath, getInputValue(el));
52 }; 65 };
53 66
54 Binding.prototype.unbind = function() {
55 var _ref;
56 Rivets.config.adapter.unsubscribe(this.model, this.keypath, this.set);
57 if (_ref = this.type, __indexOf.call(bidirectionals, _ref) >= 0) {
58 return this.el.removeEventListener('change', this.publish);
59 }
60 };
61
62 return Binding; 67 return Binding;
63 68
64 })(); 69 })();
...@@ -74,6 +79,9 @@ ...@@ -74,6 +79,9 @@
74 79
75 this.bindingRegExp = __bind(this.bindingRegExp, this); 80 this.bindingRegExp = __bind(this.bindingRegExp, this);
76 81
82 if (this.el.jquery) {
83 this.el = this.el.get(0);
84 }
77 this.build(); 85 this.build();
78 } 86 }
79 87
...@@ -88,9 +96,10 @@ ...@@ -88,9 +96,10 @@
88 }; 96 };
89 97
90 View.prototype.build = function() { 98 View.prototype.build = function() {
91 var attribute, bindingRegExp, elements, keypath, model, node, path, pipes, type, _i, _j, _len, _len1, _ref; 99 var attribute, bindType, bindingRegExp, elements, eventRegExp, keypath, model, node, path, pipe, pipes, type, _i, _j, _len, _len1, _ref;
92 this.bindings = []; 100 this.bindings = [];
93 bindingRegExp = this.bindingRegExp(); 101 bindingRegExp = this.bindingRegExp();
102 eventRegExp = /^on-/;
94 elements = [this.el]; 103 elements = [this.el];
95 elements.concat(Array.prototype.slice.call(this.el.getElementsByTagName('*'))); 104 elements.concat(Array.prototype.slice.call(this.el.getElementsByTagName('*')));
96 for (_i = 0, _len = elements.length; _i < _len; _i++) { 105 for (_i = 0, _len = elements.length; _i < _len; _i++) {
...@@ -99,14 +108,28 @@ ...@@ -99,14 +108,28 @@
99 for (_j = 0, _len1 = _ref.length; _j < _len1; _j++) { 108 for (_j = 0, _len1 = _ref.length; _j < _len1; _j++) {
100 attribute = _ref[_j]; 109 attribute = _ref[_j];
101 if (bindingRegExp.test(attribute.name)) { 110 if (bindingRegExp.test(attribute.name)) {
111 bindType = "attribute";
102 type = attribute.name.replace(bindingRegExp, ''); 112 type = attribute.name.replace(bindingRegExp, '');
103 pipes = attribute.value.split('|').map(function(pipe) { 113 pipes = (function() {
104 return pipe.trim(); 114 var _k, _len2, _ref1, _results;
105 }); 115 _ref1 = attribute.value.split('|');
116 _results = [];
117 for (_k = 0, _len2 = _ref1.length; _k < _len2; _k++) {
118 pipe = _ref1[_k];
119 _results.push(pipe.trim());
120 }
121 return _results;
122 })();
106 path = pipes.shift().split('.'); 123 path = pipes.shift().split('.');
107 model = this.models[path.shift()]; 124 model = this.models[path.shift()];
108 keypath = path.join('.'); 125 keypath = path.join('.');
109 this.bindings.push(new Rivets.Binding(node, type, model, keypath, pipes)); 126 if (eventRegExp.test(type)) {
127 type = type.replace(eventRegExp, '');
128 bindType = "event";
129 } else if (__indexOf.call(bidirectionals, type) >= 0) {
130 bindType = "bidirectional";
131 }
132 this.bindings.push(new Rivets.Binding(node, type, bindType, model, keypath, pipes));
110 } 133 }
111 } 134 }
112 } 135 }
...@@ -127,6 +150,22 @@ ...@@ -127,6 +150,22 @@
127 150
128 })(); 151 })();
129 152
153 bindEvent = function(el, event, fn) {
154 if (window.addEventListener) {
155 return el.addEventListener(event, fn);
156 } else {
157 return el.attachEvent(event, fn);
158 }
159 };
160
161 unbindEvent = function(el, event, fn) {
162 if (window.removeEventListener) {
163 return el.removeEventListener(event, fn);
164 } else {
165 return el.detachEvent(event, fn);
166 }
167 };
168
130 getInputValue = function(el) { 169 getInputValue = function(el) {
131 switch (el.type) { 170 switch (el.type) {
132 case 'text': 171 case 'text':
...@@ -140,6 +179,17 @@ ...@@ -140,6 +179,17 @@
140 } 179 }
141 }; 180 };
142 181
182 eventBinding = function(event) {
183 return function(el, bind, unbind) {
184 if (bind) {
185 bindEvent(el, event, bind);
186 }
187 if (unbind) {
188 return unbindEvent(el, event, unbind);
189 }
190 };
191 };
192
143 attributeBinding = function(attr) { 193 attributeBinding = function(attr) {
144 return function(el, value) { 194 return function(el, value) {
145 if (value) { 195 if (value) {
...@@ -150,44 +200,45 @@ ...@@ -150,44 +200,45 @@
150 }; 200 };
151 }; 201 };
152 202
153 stateBinding = function(attr, inverse) {
154 if (inverse == null) {
155 inverse = false;
156 }
157 return function(el, value) {
158 var binding;
159 binding = attributeBinding(attr);
160 return binding(el, inverse === !value ? attr : false);
161 };
162 };
163
164 bidirectionals = ['value', 'checked', 'unchecked', 'selected', 'unselected']; 203 bidirectionals = ['value', 'checked', 'unchecked', 'selected', 'unselected'];
165 204
166 Rivets.routines = { 205 Rivets.routines = {
167 checked: stateBinding('checked'), 206 enabled: function(el, value) {
168 selected: stateBinding('selected'), 207 return el.disabled = !value;
169 disabled: stateBinding('disabled'),
170 unchecked: stateBinding('checked', true),
171 unselected: stateBinding('selected', true),
172 enabled: stateBinding('disabled', true),
173 text: function(el, value) {
174 if (el.innerText != null) {
175 return el.innerText = value || '';
176 } else {
177 return el.textContent = value || '';
178 }
179 }, 208 },
180 html: function(el, value) { 209 disabled: function(el, value) {
181 return el.innerHTML = value || ''; 210 return el.disabled = !!value;
182 }, 211 },
183 value: function(el, value) { 212 checked: function(el, value) {
184 return el.value = value; 213 return el.checked = !!value;
214 },
215 unchecked: function(el, value) {
216 return el.checked = !value;
217 },
218 selected: function(el, value) {
219 return el.selected = !!value;
220 },
221 unselected: function(el, value) {
222 return el.selected = !value;
185 }, 223 },
186 show: function(el, value) { 224 show: function(el, value) {
187 return el.style.display = value ? '' : 'none'; 225 return el.style.display = value ? '' : 'none';
188 }, 226 },
189 hide: function(el, value) { 227 hide: function(el, value) {
190 return el.style.display = value ? 'none' : ''; 228 return el.style.display = value ? 'none' : '';
229 },
230 html: function(el, value) {
231 return el.innerHTML = value || '';
232 },
233 value: function(el, value) {
234 return el.value = value;
235 },
236 text: function(el, value) {
237 if (el.innerText != null) {
238 return el.innerText = value || '';
239 } else {
240 return el.textContent = value || '';
241 }
191 } 242 }
192 }; 243 };
193 244
......
1 { 1 {
2 "name" : "rivets", 2 "name" : "rivets",
3 "description" : "Declarative DOM-binding facility.", 3 "description" : "Declarative data binding facility.",
4 "version" : "0.1.9", 4 "version" : "0.1.12",
5 "author" : "Michael Richards", 5 "author" : "Michael Richards",
6 "main" : "./lib/rivets.js", 6 "main" : "./lib/rivets.js",
7 "licenses" : [{ 7 "licenses" : [{
...@@ -11,5 +11,8 @@ ...@@ -11,5 +11,8 @@
11 "repository" : { 11 "repository" : {
12 "type" : "git", 12 "type" : "git",
13 "url" : "https://github.com/mikeric/rivets.git" 13 "url" : "https://github.com/mikeric/rivets.git"
14 },
15 "scripts" : {
16 "build" : "coffee -o lib -c src"
14 } 17 }
15 } 18 }
......
1 # rivets.js 1 # rivets.js
2 # version : 0.1.9 2 # version : 0.1.12
3 # author : Michael Richards 3 # author : Michael Richards
4 # license : MIT 4 # license : MIT
5 5
6 # The Rivets namespace. 6 # The Rivets namespace.
7 Rivets = {} 7 Rivets = {}
8 8
9 # Polyfill For String::trim.
10 unless String::trim then String::trim = -> @replace /^\s+|\s+$/g, ""
11
9 # A single binding between a model attribute and a DOM element. 12 # A single binding between a model attribute and a DOM element.
10 class Rivets.Binding 13 class Rivets.Binding
11 # All information about the binding is passed into the constructor; the DOM 14 # All information about the binding is passed into the constructor; the DOM
12 # element, the routine identifier, the model object and the keypath at which 15 # element, the routine identifier, the model object and the keypath at which
13 # to listen for changes. 16 # to listen for changes.
14 constructor: (@el, @type, @model, @keypath, @formatters = []) -> 17 constructor: (@el, @type, @bindType, @model, @keypath, @formatters = []) ->
15 @routine = Rivets.routines[@type] || attributeBinding @type 18 if @bindType is "event"
19 @routine = eventBinding @type
20 else
21 @routine = Rivets.routines[@type] || attributeBinding @type
16 22
17 # Sets the value for the binding. This Basically just runs the binding routine 23 # Sets the value for the binding. This Basically just runs the binding routine
18 # with the suplied value and applies any formatters. 24 # with the suplied value and applies any formatters.
...@@ -20,7 +26,11 @@ class Rivets.Binding ...@@ -20,7 +26,11 @@ class Rivets.Binding
20 for formatter in @formatters 26 for formatter in @formatters
21 value = Rivets.config.formatters[formatter] value 27 value = Rivets.config.formatters[formatter] value
22 28
23 @routine @el, value 29 if @bindType is "event"
30 @routine @el, value, @currentListener
31 @currentListener = value
32 else
33 @routine @el, value
24 34
25 # Subscribes to the model for changes at the specified keypath. Bi-directional 35 # Subscribes to the model for changes at the specified keypath. Bi-directional
26 # routines will also listen for changes on the element to propagate them back 36 # routines will also listen for changes on the element to propagate them back
...@@ -31,25 +41,20 @@ class Rivets.Binding ...@@ -31,25 +41,20 @@ class Rivets.Binding
31 if Rivets.config.preloadData 41 if Rivets.config.preloadData
32 @set Rivets.config.adapter.read @model, @keypath 42 @set Rivets.config.adapter.read @model, @keypath
33 43
34 if @type in bidirectionals 44 if @bindType is "bidirectional"
35 @el.addEventListener 'change', @publish 45 bindEvent @el, 'change', @publish
36 46
47 # Publishes the value currently set on the input element back to the model.
37 publish: (e) => 48 publish: (e) =>
38 el = e.target or e.srcElement 49 el = e.target or e.srcElement
39 Rivets.config.adapter.publish @model, @keypath, getInputValue el 50 Rivets.config.adapter.publish @model, @keypath, getInputValue el
40 51
41 # Unsubscribes from the model and the element
42 unbind: =>
43 Rivets.config.adapter.unsubscribe @model, @keypath, @set
44
45 if @type in bidirectionals
46 @el.removeEventListener 'change', @publish
47
48 # A collection of bindings for a parent element. 52 # A collection of bindings for a parent element.
49 class Rivets.View 53 class Rivets.View
50 # The parent DOM element and the model objects for binding are passed into the 54 # The parent DOM element and the model objects for binding are passed into the
51 # constructor. 55 # constructor.
52 constructor: (@el, @models) -> 56 constructor: (@el, @models) ->
57 @el = @el.get(0) if @el.jquery
53 @build() 58 @build()
54 59
55 # Regular expression used to match binding attributes. 60 # Regular expression used to match binding attributes.
...@@ -61,19 +66,26 @@ class Rivets.View ...@@ -61,19 +66,26 @@ class Rivets.View
61 build: => 66 build: =>
62 @bindings = [] 67 @bindings = []
63 bindingRegExp = @bindingRegExp() 68 bindingRegExp = @bindingRegExp()
69 eventRegExp = /^on-/
64 elements = [@el] 70 elements = [@el]
65 elements.concat Array::slice.call @el.getElementsByTagName '*' 71 elements.concat Array::slice.call @el.getElementsByTagName '*'
66
67 for node in elements 72 for node in elements
68 for attribute in node.attributes 73 for attribute in node.attributes
69 if bindingRegExp.test attribute.name 74 if bindingRegExp.test attribute.name
75 bindType = "attribute"
70 type = attribute.name.replace bindingRegExp, '' 76 type = attribute.name.replace bindingRegExp, ''
71 pipes = attribute.value.split('|').map (pipe) -> pipe.trim() 77 pipes = (pipe.trim() for pipe in attribute.value.split '|')
72 path = pipes.shift().split '.' 78 path = pipes.shift().split '.'
73 model = @models[path.shift()] 79 model = @models[path.shift()]
74 keypath = path.join '.' 80 keypath = path.join '.'
75 81
76 @bindings.push new Rivets.Binding node, type, model, keypath, pipes 82 if eventRegExp.test type
83 type = type.replace eventRegExp, ''
84 bindType = "event"
85 else if type in bidirectionals
86 bindType = "bidirectional"
87
88 @bindings.push new Rivets.Binding node, type, bindType, model, keypath, pipes
77 89
78 # To avoid returning the result of the loop 90 # To avoid returning the result of the loop
79 return 91 return
...@@ -82,55 +94,70 @@ class Rivets.View ...@@ -82,55 +94,70 @@ class Rivets.View
82 bind: => 94 bind: =>
83 binding.bind() for binding in @bindings 95 binding.bind() for binding in @bindings
84 96
97 # Cross-browser event binding
98 bindEvent = (el, event, fn) ->
99 # Check to see if addEventListener is available.
100 if window.addEventListener
101 el.addEventListener event, fn
102 else
103 # Assume we are in IE and use attachEvent.
104 el.attachEvent event, fn
105
106 unbindEvent = (el, event, fn) ->
107 # Check to see if addEventListener is available.
108 if window.removeEventListener
109 el.removeEventListener event, fn
110 else
111 # Assume we are in IE and use attachEvent.
112 el.detachEvent event, fn
113
85 # Returns the current input value for the specified element. 114 # Returns the current input value for the specified element.
86 getInputValue = (el) -> 115 getInputValue = (el) ->
87 switch el.type 116 switch el.type
88 when 'text', 'textarea', 'password', 'select-one' then el.value 117 when 'text', 'textarea', 'password', 'select-one' then el.value
89 when 'checkbox', 'radio' then el.checked 118 when 'checkbox', 'radio' then el.checked
90 119
120 # Returns an element binding routine for the specified attribute.
121 eventBinding = (event) -> (el, bind, unbind) ->
122 bindEvent el, event, bind if bind
123 unbindEvent el, event, unbind if unbind
124
91 # Returns an attribute binding routine for the specified attribute. This is what 125 # Returns an attribute binding routine for the specified attribute. This is what
92 # `registerBinding` falls back to when there is no routine for the binding type. 126 # `registerBinding` falls back to when there is no routine for the binding type.
93 attributeBinding = (attr) -> (el, value) -> 127 attributeBinding = (attr) -> (el, value) ->
94 if value then el.setAttribute attr, value else el.removeAttribute attr 128 if value then el.setAttribute attr, value else el.removeAttribute attr
95 129
96 # Returns a state binding routine for the specified attribute. Can optionally be
97 # negatively evaluated. This is used to build a lot of the core state binding
98 # routines.
99 stateBinding = (attr, inverse = false) -> (el, value) ->
100 binding = attributeBinding(attr)
101 binding el, if inverse is !value then attr else false
102
103 # Bindings that should also be observed for changes on the DOM element in order 130 # Bindings that should also be observed for changes on the DOM element in order
104 # to propagate those changes back to the model object. 131 # to propagate those changes back to the model object.
105 bidirectionals = ['value', 'checked', 'unchecked', 'selected', 'unselected'] 132 bidirectionals = ['value', 'checked', 'unchecked', 'selected', 'unselected']
106 133
107 # Core binding routines. 134 # Core binding routines.
108 Rivets.routines = 135 Rivets.routines =
109 checked: 136 enabled: (el, value) ->
110 stateBinding 'checked' 137 el.disabled = !value
111 selected: 138 disabled: (el, value) ->
112 stateBinding 'selected' 139 el.disabled = !!value
113 disabled: 140 checked: (el, value) ->
114 stateBinding 'disabled' 141 el.checked = !!value
115 unchecked: 142 unchecked: (el, value) ->
116 stateBinding 'checked', true 143 el.checked = !value
117 unselected: 144 selected: (el, value) ->
118 stateBinding 'selected', true 145 el.selected = !!value
119 enabled: 146 unselected: (el, value) ->
120 stateBinding 'disabled', true 147 el.selected = !value
148 show: (el, value) ->
149 el.style.display = if value then '' else 'none'
150 hide: (el, value) ->
151 el.style.display = if value then 'none' else ''
152 html: (el, value) ->
153 el.innerHTML = value or ''
154 value: (el, value) ->
155 el.value = value
121 text: (el, value) -> 156 text: (el, value) ->
122 if el.innerText? 157 if el.innerText?
123 el.innerText = value or '' 158 el.innerText = value or ''
124 else 159 else
125 el.textContent = value or '' 160 el.textContent = value or ''
126 html: (el, value) ->
127 el.innerHTML = value or ''
128 value: (el, value) ->
129 el.value = value
130 show: (el, value) ->
131 el.style.display = if value then '' else 'none'
132 hide: (el, value) ->
133 el.style.display = if value then 'none' else ''
134 161
135 # Default configuration. 162 # Default configuration.
136 Rivets.config = 163 Rivets.config =
......