Merge remote-tracking branch 'upstream/master' into feature/tests
Conflicts: build/rivets.js src/rivets.coffee
Showing
5 changed files
with
192 additions
and
109 deletions
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 = | ... | ... |
-
Please register or sign in to post a comment