cfa4fce3 by Michael Richards

Merge pull request #209 from mikeric/adapters

0.6.0 WIP
2 parents dacf10b9 217e4e54
1 _SpecRunner.html
1 .DS_Store 2 .DS_Store
2 *.swp 3 *.swp
4 .grunt/**/*
3 node_modules/**/* 5 node_modules/**/*
......
...@@ -11,8 +11,20 @@ module.exports = (grunt) -> ...@@ -11,8 +11,20 @@ module.exports = (grunt) ->
11 11
12 coffee: 12 coffee:
13 all: 13 all:
14 options:
15 join: true
14 files: 16 files:
15 'dist/rivets.js': 'src/rivets.coffee' 17 'dist/rivets.js': [
18 'src/rivets.coffee'
19 'src/util.coffee'
20 'src/view.coffee'
21 'src/bindings.coffee'
22 'src/parsers.coffee'
23 'src/keypath_observer.coffee'
24 'src/binders.coffee'
25 'src/adapters.coffee'
26 'src/export.coffee'
27 ]
16 28
17 concat: 29 concat:
18 all: 30 all:
......
...@@ -2,19 +2,13 @@ describe('Rivets.Binding', function() { ...@@ -2,19 +2,13 @@ describe('Rivets.Binding', function() {
2 var model, el, view, binding, opts; 2 var model, el, view, binding, opts;
3 3
4 beforeEach(function() { 4 beforeEach(function() {
5 rivets.configure({ 5 rivets.config.prefix = 'data'
6 adapter: { 6 adapter = rivets.adapters['.']
7 subscribe: function() {},
8 unsubscribe: function() {},
9 read: function() {},
10 publish: function() {}
11 }
12 });
13 7
14 el = document.createElement('div'); 8 el = document.createElement('div');
15 el.setAttribute('data-text', 'obj.name'); 9 el.setAttribute('data-text', 'obj.name');
16 opts = {}; 10
17 view = rivets.bind(el, {obj: {}}, opts); 11 view = rivets.bind(el, {obj: {}});
18 binding = view.bindings[0]; 12 binding = view.bindings[0];
19 model = binding.model; 13 model = binding.model;
20 }); 14 });
...@@ -25,9 +19,9 @@ describe('Rivets.Binding', function() { ...@@ -25,9 +19,9 @@ describe('Rivets.Binding', function() {
25 19
26 describe('bind()', function() { 20 describe('bind()', function() {
27 it('subscribes to the model for changes via the adapter', function() { 21 it('subscribes to the model for changes via the adapter', function() {
28 spyOn(rivets.config.adapter, 'subscribe'); 22 spyOn(adapter, 'subscribe');
29 binding.bind(); 23 binding.bind();
30 expect(rivets.config.adapter.subscribe).toHaveBeenCalledWith(model, 'name', binding.sync); 24 expect(adapter.subscribe).toHaveBeenCalledWith(model, 'name', binding.sync);
31 }); 25 });
32 26
33 it("calls the binder's bind method if one exists", function() { 27 it("calls the binder's bind method if one exists", function() {
...@@ -48,47 +42,23 @@ describe('Rivets.Binding', function() { ...@@ -48,47 +42,23 @@ describe('Rivets.Binding', function() {
48 42
49 it('sets the initial value via the adapter', function() { 43 it('sets the initial value via the adapter', function() {
50 spyOn(binding, 'set'); 44 spyOn(binding, 'set');
51 spyOn(rivets.config.adapter, 'read'); 45 spyOn(adapter, 'read');
52 binding.bind(); 46 binding.bind();
53 expect(rivets.config.adapter.read).toHaveBeenCalledWith(model, 'name'); 47 expect(adapter.read).toHaveBeenCalledWith(model, 'name');
54 expect(binding.set).toHaveBeenCalled(); 48 expect(binding.set).toHaveBeenCalled();
55 }); 49 });
56 }); 50 });
57 51
58 describe('with the bypass option set to true', function() {
59 beforeEach(function() {
60 binding.options.bypass = true;
61 });
62
63 it('sets the initial value from the model directly', function() {
64 spyOn(binding, 'set');
65 binding.model.name = 'espresso';
66 binding.bind();
67 expect(binding.set).toHaveBeenCalledWith('espresso');
68 });
69
70 it("calls the binder's bind method if one exists", function() {
71 expect(function(){
72 binding.bind();
73 }).not.toThrow(new Error());
74
75 binding.binder.bind = function(){};
76 spyOn(binding.binder, 'bind');
77 binding.bind();
78 expect(binding.binder.bind).toHaveBeenCalled();
79 });
80 });
81
82 describe('with dependencies', function() { 52 describe('with dependencies', function() {
83 beforeEach(function() { 53 beforeEach(function() {
84 binding.options.dependencies = ['.fname', '.lname']; 54 binding.options.dependencies = ['.fname', '.lname'];
85 }); 55 });
86 56
87 it('sets up observers on the dependant attributes', function() { 57 it('sets up observers on the dependant attributes', function() {
88 spyOn(rivets.config.adapter, 'subscribe'); 58 spyOn(adapter, 'subscribe');
89 binding.bind(); 59 binding.bind();
90 expect(rivets.config.adapter.subscribe).toHaveBeenCalledWith(model, 'fname', binding.sync); 60 expect(adapter.subscribe).toHaveBeenCalledWith(model, 'fname', binding.sync);
91 expect(rivets.config.adapter.subscribe).toHaveBeenCalledWith(model, 'lname', binding.sync); 61 expect(adapter.subscribe).toHaveBeenCalledWith(model, 'lname', binding.sync);
92 }); 62 });
93 }); 63 });
94 }); 64 });
...@@ -104,23 +74,6 @@ describe('Rivets.Binding', function() { ...@@ -104,23 +74,6 @@ describe('Rivets.Binding', function() {
104 binding.unbind(); 74 binding.unbind();
105 expect(binding.binder.unbind).toHaveBeenCalled(); 75 expect(binding.binder.unbind).toHaveBeenCalled();
106 }); 76 });
107
108 describe('with the bypass option set to true', function() {
109 beforeEach(function() {
110 binding.options.bypass = true;
111 });
112
113 it("calls the binder's unbind method if one exists", function() {
114 expect(function(){
115 binding.unbind();
116 }).not.toThrow(new Error());
117
118 binding.binder.unbind = function(){};
119 spyOn(binding.binder, 'unbind');
120 binding.unbind();
121 expect(binding.binder.unbind).toHaveBeenCalled();
122 });
123 });
124 }); 77 });
125 78
126 describe('set()', function() { 79 describe('set()', function() {
...@@ -158,9 +111,9 @@ describe('Rivets.Binding', function() { ...@@ -158,9 +111,9 @@ describe('Rivets.Binding', function() {
158 111
159 numberInput.value = 42; 112 numberInput.value = 42;
160 113
161 spyOn(rivets.config.adapter, 'publish'); 114 spyOn(adapter, 'publish');
162 binding.publish({target: numberInput}); 115 binding.publish({target: numberInput});
163 expect(rivets.config.adapter.publish).toHaveBeenCalledWith(model, 'num', '42'); 116 expect(adapter.publish).toHaveBeenCalledWith(model, 'num', '42');
164 }); 117 });
165 }); 118 });
166 119
...@@ -191,9 +144,9 @@ describe('Rivets.Binding', function() { ...@@ -191,9 +144,9 @@ describe('Rivets.Binding', function() {
191 144
192 numberInput.value = 42; 145 numberInput.value = 42;
193 146
194 spyOn(rivets.config.adapter, 'publish'); 147 spyOn(adapter, 'publish');
195 binding.publish({target: numberInput}); 148 binding.publish({target: numberInput});
196 expect(rivets.config.adapter.publish).toHaveBeenCalledWith(model, 'num', 'awesome 42'); 149 expect(adapter.publish).toHaveBeenCalledWith(model, 'num', 'awesome 42');
197 }); 150 });
198 151
199 it("should format a value in both directions", function() { 152 it("should format a value in both directions", function() {
...@@ -209,10 +162,10 @@ describe('Rivets.Binding', function() { ...@@ -209,10 +162,10 @@ describe('Rivets.Binding', function() {
209 binding = view.bindings[0]; 162 binding = view.bindings[0];
210 model = binding.model; 163 model = binding.model;
211 164
212 spyOn(rivets.config.adapter, 'publish'); 165 spyOn(adapter, 'publish');
213 valueInput.value = 'charles'; 166 valueInput.value = 'charles';
214 binding.publish({target: valueInput}); 167 binding.publish({target: valueInput});
215 expect(rivets.config.adapter.publish).toHaveBeenCalledWith(model, 'name', 'awesome charles'); 168 expect(adapter.publish).toHaveBeenCalledWith(model, 'name', 'awesome charles');
216 169
217 spyOn(binding.binder, 'routine'); 170 spyOn(binding.binder, 'routine');
218 binding.set('fred'); 171 binding.set('fred');
...@@ -229,10 +182,10 @@ describe('Rivets.Binding', function() { ...@@ -229,10 +182,10 @@ describe('Rivets.Binding', function() {
229 binding = view.bindings[0]; 182 binding = view.bindings[0];
230 model = binding.model; 183 model = binding.model;
231 184
232 spyOn(rivets.config.adapter, 'publish'); 185 spyOn(adapter, 'publish');
233 valueInput.value = 'charles'; 186 valueInput.value = 'charles';
234 binding.publish({target: valueInput}); 187 binding.publish({target: valueInput});
235 expect(rivets.config.adapter.publish).toHaveBeenCalledWith(model, 'name', 'charles'); 188 expect(adapter.publish).toHaveBeenCalledWith(model, 'name', 'charles');
236 189
237 spyOn(binding.binder, 'routine'); 190 spyOn(binding.binder, 'routine');
238 binding.set('fred'); 191 binding.set('fred');
...@@ -262,10 +215,10 @@ describe('Rivets.Binding', function() { ...@@ -262,10 +215,10 @@ describe('Rivets.Binding', function() {
262 binding.set('fred'); 215 binding.set('fred');
263 expect(binding.binder.routine).toHaveBeenCalledWith(valueInput, 'fred is awesome totally'); 216 expect(binding.binder.routine).toHaveBeenCalledWith(valueInput, 'fred is awesome totally');
264 217
265 spyOn(rivets.config.adapter, 'publish'); 218 spyOn(adapter, 'publish');
266 valueInput.value = 'fred'; 219 valueInput.value = 'fred';
267 binding.publish({target: valueInput}); 220 binding.publish({target: valueInput});
268 expect(rivets.config.adapter.publish).toHaveBeenCalledWith(model, 'name', 'fred totally is awesome'); 221 expect(adapter.publish).toHaveBeenCalledWith(model, 'name', 'fred totally is awesome');
269 }); 222 });
270 223
271 it("binders in a chain should be skipped if they're not there", function() { 224 it("binders in a chain should be skipped if they're not there", function() {
...@@ -290,10 +243,10 @@ describe('Rivets.Binding', function() { ...@@ -290,10 +243,10 @@ describe('Rivets.Binding', function() {
290 binding.set('fred'); 243 binding.set('fred');
291 expect(binding.binder.routine).toHaveBeenCalledWith(valueInput, 'fred is awesome totally'); 244 expect(binding.binder.routine).toHaveBeenCalledWith(valueInput, 'fred is awesome totally');
292 245
293 spyOn(rivets.config.adapter, 'publish'); 246 spyOn(adapter, 'publish');
294 valueInput.value = 'fred'; 247 valueInput.value = 'fred';
295 binding.publish({target: valueInput}); 248 binding.publish({target: valueInput});
296 expect(rivets.config.adapter.publish).toHaveBeenCalledWith(model, 'name', 'fred totally is radical'); 249 expect(adapter.publish).toHaveBeenCalledWith(model, 'name', 'fred totally is radical');
297 }); 250 });
298 251
299 }); 252 });
...@@ -305,12 +258,6 @@ describe('Rivets.Binding', function() { ...@@ -305,12 +258,6 @@ describe('Rivets.Binding', function() {
305 expect(binding.formattedValue('hat')).toBe('awesome hat'); 258 expect(binding.formattedValue('hat')).toBe('awesome hat');
306 }); 259 });
307 260
308 it('uses formatters on the model', function() {
309 model.modelAwesome = function(value) { return 'model awesome ' + value; };
310 binding.formatters.push('modelAwesome');
311 expect(binding.formattedValue('hat')).toBe('model awesome hat');
312 });
313
314 describe('with a multi-argument formatter string', function() { 261 describe('with a multi-argument formatter string', function() {
315 beforeEach(function() { 262 beforeEach(function() {
316 view.formatters.awesome = function(value, prefix) { 263 view.formatters.awesome = function(value, prefix) {
......
...@@ -2,34 +2,42 @@ describe('Functional', function() { ...@@ -2,34 +2,42 @@ describe('Functional', function() {
2 var data, bindData, el, input; 2 var data, bindData, el, input;
3 3
4 beforeEach(function() { 4 beforeEach(function() {
5 data = new Data({foo: 'bar', items: [{name: 'a'}, {name: 'b'}]}); 5 adapter = {
6 subscribe: function(obj, keypath, callback) {
7 obj.on(keypath, callback);
8 },
9 unsubscribe: function(obj, keypath, callback) {
10 obj.off(keypath, callback);
11 },
12 read: function(obj, keypath) {
13 return obj.get(keypath);
14 },
15 publish: function(obj, keypath, value) {
16 attributes = {};
17 attributes[keypath] = value;
18 obj.set(attributes);
19 }
20 };
21
22 rivets.adapters[':'] = adapter;
23 rivets.configure({preloadData: true});
24
25 data = new Data({
26 foo: 'bar',
27 items: [{name: 'a'}, {name: 'b'}]
28 });
29
6 bindData = {data: data}; 30 bindData = {data: data};
31
7 el = document.createElement('div'); 32 el = document.createElement('div');
8 input = document.createElement('input'); 33 input = document.createElement('input');
9 input.setAttribute('type', 'text'); 34 input.setAttribute('type', 'text');
10
11 rivets.configure({
12 preloadData: true,
13 adapter: {
14 subscribe: function(obj, keypath, callback) {
15 obj.on(keypath, callback);
16 },
17 read: function(obj, keypath) {
18 return obj.get(keypath);
19 },
20 publish: function(obj, keypath, value) {
21 attributes = {};
22 attributes[keypath] = value;
23 obj.set(attributes);
24 }
25 }
26 });
27 }); 35 });
28 36
29 describe('Adapter', function() { 37 describe('Adapter', function() {
30 it('should read the initial value', function() { 38 it('should read the initial value', function() {
31 spyOn(data, 'get'); 39 spyOn(data, 'get');
32 el.setAttribute('data-text', 'data.foo'); 40 el.setAttribute('data-text', 'data:foo');
33 rivets.bind(el, bindData); 41 rivets.bind(el, bindData);
34 expect(data.get).toHaveBeenCalledWith('foo'); 42 expect(data.get).toHaveBeenCalledWith('foo');
35 }); 43 });
...@@ -37,14 +45,14 @@ describe('Functional', function() { ...@@ -37,14 +45,14 @@ describe('Functional', function() {
37 it('should read the initial value unless preloadData is false', function() { 45 it('should read the initial value unless preloadData is false', function() {
38 rivets.configure({preloadData: false}); 46 rivets.configure({preloadData: false});
39 spyOn(data, 'get'); 47 spyOn(data, 'get');
40 el.setAttribute('data-value', 'data.foo'); 48 el.setAttribute('data-value', 'data:foo');
41 rivets.bind(el, bindData); 49 rivets.bind(el, bindData);
42 expect(data.get).not.toHaveBeenCalled(); 50 expect(data.get).not.toHaveBeenCalled();
43 }); 51 });
44 52
45 it('should subscribe to updates', function() { 53 it('should subscribe to updates', function() {
46 spyOn(data, 'on'); 54 spyOn(data, 'on');
47 el.setAttribute('data-value', 'data.foo'); 55 el.setAttribute('data-value', 'data:foo');
48 rivets.bind(el, bindData); 56 rivets.bind(el, bindData);
49 expect(data.on).toHaveBeenCalled(); 57 expect(data.on).toHaveBeenCalled();
50 }); 58 });
...@@ -53,13 +61,14 @@ describe('Functional', function() { ...@@ -53,13 +61,14 @@ describe('Functional', function() {
53 describe('Binds', function() { 61 describe('Binds', function() {
54 describe('Text', function() { 62 describe('Text', function() {
55 it('should set the text content of the element', function() { 63 it('should set the text content of the element', function() {
56 el.setAttribute('data-text', 'data.foo'); 64 el.setAttribute('data-text', 'data:foo');
57 rivets.bind(el, bindData); 65 rivets.bind(el, bindData);
66 debugger
58 expect(el.textContent || el.innerText).toBe(data.get('foo')); 67 expect(el.textContent || el.innerText).toBe(data.get('foo'));
59 }); 68 });
60 69
61 it('should correctly handle HTML in the content', function() { 70 it('should correctly handle HTML in the content', function() {
62 el.setAttribute('data-text', 'data.foo'); 71 el.setAttribute('data-text', 'data:foo');
63 value = '<b>Fail</b>'; 72 value = '<b>Fail</b>';
64 data.set({foo: value}); 73 data.set({foo: value});
65 rivets.bind(el, bindData); 74 rivets.bind(el, bindData);
...@@ -69,13 +78,13 @@ describe('Functional', function() { ...@@ -69,13 +78,13 @@ describe('Functional', function() {
69 78
70 describe('HTML', function() { 79 describe('HTML', function() {
71 it('should set the html content of the element', function() { 80 it('should set the html content of the element', function() {
72 el.setAttribute('data-html', 'data.foo'); 81 el.setAttribute('data-html', 'data:foo');
73 rivets.bind(el, bindData); 82 rivets.bind(el, bindData);
74 expect(el).toHaveTheTextContent(data.get('foo')); 83 expect(el).toHaveTheTextContent(data.get('foo'));
75 }); 84 });
76 85
77 it('should correctly handle HTML in the content', function() { 86 it('should correctly handle HTML in the content', function() {
78 el.setAttribute('data-html', 'data.foo'); 87 el.setAttribute('data-html', 'data:foo');
79 value = '<b>Fail</b>'; 88 value = '<b>Fail</b>';
80 data.set({foo: value}); 89 data.set({foo: value});
81 rivets.bind(el, bindData); 90 rivets.bind(el, bindData);
...@@ -85,7 +94,7 @@ describe('Functional', function() { ...@@ -85,7 +94,7 @@ describe('Functional', function() {
85 94
86 describe('Value', function() { 95 describe('Value', function() {
87 it('should set the value of the element', function() { 96 it('should set the value of the element', function() {
88 input.setAttribute('data-value', 'data.foo'); 97 input.setAttribute('data-value', 'data:foo');
89 rivets.bind(input, bindData); 98 rivets.bind(input, bindData);
90 expect(input.value).toBe(data.get('foo')); 99 expect(input.value).toBe(data.get('foo'));
91 }); 100 });
...@@ -93,8 +102,8 @@ describe('Functional', function() { ...@@ -93,8 +102,8 @@ describe('Functional', function() {
93 102
94 describe('Multiple', function() { 103 describe('Multiple', function() {
95 it('should bind a list of multiple elements', function() { 104 it('should bind a list of multiple elements', function() {
96 el.setAttribute('data-html', 'data.foo'); 105 el.setAttribute('data-html', 'data:foo');
97 input.setAttribute('data-value', 'data.foo'); 106 input.setAttribute('data-value', 'data:foo');
98 rivets.bind([el, input], bindData); 107 rivets.bind([el, input], bindData);
99 expect(el).toHaveTheTextContent(data.get('foo')); 108 expect(el).toHaveTheTextContent(data.get('foo'));
100 expect(input.value).toBe(data.get('foo')); 109 expect(input.value).toBe(data.get('foo'));
...@@ -106,7 +115,7 @@ describe('Functional', function() { ...@@ -106,7 +115,7 @@ describe('Functional', function() {
106 list = document.createElement('ul'); 115 list = document.createElement('ul');
107 el.appendChild(list); 116 el.appendChild(list);
108 listItem = document.createElement('li'); 117 listItem = document.createElement('li');
109 listItem.setAttribute('data-each-item', 'data.items'); 118 listItem.setAttribute('data-each-item', 'data:items');
110 list.appendChild(listItem); 119 list.appendChild(listItem);
111 }); 120 });
112 121
...@@ -133,9 +142,9 @@ describe('Functional', function() { ...@@ -133,9 +142,9 @@ describe('Functional', function() {
133 142
134 it('should allow binding to the iterated item as well as any parent contexts', function() { 143 it('should allow binding to the iterated item as well as any parent contexts', function() {
135 span1 = document.createElement('span'); 144 span1 = document.createElement('span');
136 span1.setAttribute('data-text', 'item:name') 145 span1.setAttribute('data-text', 'item.name')
137 span2 = document.createElement('span'); 146 span2 = document.createElement('span');
138 span2.setAttribute('data-text', 'data.foo') 147 span2.setAttribute('data-text', 'data:foo')
139 listItem.appendChild(span1); 148 listItem.appendChild(span1);
140 listItem.appendChild(span2); 149 listItem.appendChild(span2);
141 150
...@@ -145,8 +154,8 @@ describe('Functional', function() { ...@@ -145,8 +154,8 @@ describe('Functional', function() {
145 }); 154 });
146 155
147 it('should allow binding to the iterated element directly', function() { 156 it('should allow binding to the iterated element directly', function() {
148 listItem.setAttribute('data-text', 'item:name'); 157 listItem.setAttribute('data-text', 'item.name');
149 listItem.setAttribute('data-class', 'data.foo'); 158 listItem.setAttribute('data-class', 'data:foo');
150 rivets.bind(el, bindData); 159 rivets.bind(el, bindData);
151 expect(el.getElementsByTagName('li')[0]).toHaveTheTextContent('a'); 160 expect(el.getElementsByTagName('li')[0]).toHaveTheTextContent('a');
152 expect(el.getElementsByTagName('li')[0].className).toBe('bar'); 161 expect(el.getElementsByTagName('li')[0].className).toBe('bar');
...@@ -160,7 +169,7 @@ describe('Functional', function() { ...@@ -160,7 +169,7 @@ describe('Functional', function() {
160 list.appendChild(lastItem); 169 list.appendChild(lastItem);
161 list.insertBefore(firstItem, listItem); 170 list.insertBefore(firstItem, listItem);
162 171
163 listItem.setAttribute('data-text', 'item:name'); 172 listItem.setAttribute('data-text', 'item.name');
164 173
165 rivets.bind(el, bindData); 174 rivets.bind(el, bindData);
166 expect(el.getElementsByTagName('li')[0]).toHaveTheTextContent('first'); 175 expect(el.getElementsByTagName('li')[0]).toHaveTheTextContent('first');
...@@ -174,7 +183,7 @@ describe('Functional', function() { ...@@ -174,7 +183,7 @@ describe('Functional', function() {
174 183
175 describe('Updates', function() { 184 describe('Updates', function() {
176 it('should change the value', function() { 185 it('should change the value', function() {
177 el.setAttribute('data-text', 'data.foo'); 186 el.setAttribute('data-text', 'data:foo');
178 rivets.bind(el, bindData); 187 rivets.bind(el, bindData);
179 data.set({foo: 'some new value'}); 188 data.set({foo: 'some new value'});
180 expect(el).toHaveTheTextContent(data.get('foo')); 189 expect(el).toHaveTheTextContent(data.get('foo'));
...@@ -183,7 +192,7 @@ describe('Functional', function() { ...@@ -183,7 +192,7 @@ describe('Functional', function() {
183 192
184 describe('Input', function() { 193 describe('Input', function() {
185 it('should update the model value', function() { 194 it('should update the model value', function() {
186 input.setAttribute('data-value', 'data.foo'); 195 input.setAttribute('data-value', 'data:foo');
187 rivets.bind(input, bindData); 196 rivets.bind(input, bindData);
188 input.value = 'some new value'; 197 input.value = 'some new value';
189 var event = document.createEvent('HTMLEvents') 198 var event = document.createEvent('HTMLEvents')
......
1 # The default `.` adapter thats comes with Rivets.js. Allows subscribing to
2 # properties on POJSOs, implemented in ES5 natives using
3 # `Object.defineProperty`.
4 Rivets.adapters['.'] =
5 id: '_rv'
6 counter: 0
7 weakmap: {}
8
9 weakReference: (obj) ->
10 unless obj[@id]?
11 id = @counter++
12
13 @weakmap[id] =
14 callbacks: {}
15
16 Object.defineProperty obj, @id, value: id
17
18 @weakmap[obj[@id]]
19
20 stubFunction: (obj, fn) ->
21 original = obj[fn]
22 map = @weakReference obj
23 weakmap = @weakmap
24
25 obj[fn] = ->
26 response = original.apply obj, arguments
27
28 for r, k of map.pointers
29 callback() for callback in weakmap[r]?.callbacks[k] ? []
30
31 response
32
33 observeMutations: (obj, ref, keypath) ->
34 if Array.isArray obj
35 map = @weakReference obj
36
37 unless map.pointers?
38 map.pointers = {}
39 functions = ['push', 'pop', 'shift', 'unshift', 'sort', 'reverse', 'splice']
40 @stubFunction obj, fn for fn in functions
41
42 map.pointers[ref] ?= []
43
44 unless keypath in map.pointers[ref]
45 map.pointers[ref].push keypath
46
47 unobserveMutations: (obj, ref, keypath) ->
48 if Array.isArray obj and obj[@id]?
49 if keypaths = @weakReference(obj).pointers?[ref]
50 keypaths.splice keypaths.indexOf(keypath), 1
51
52 subscribe: (obj, keypath, callback) ->
53 callbacks = @weakReference(obj).callbacks
54
55 unless callbacks[keypath]?
56 callbacks[keypath] = []
57 value = obj[keypath]
58
59 Object.defineProperty obj, keypath,
60 get: -> value
61 set: (newValue) =>
62 if newValue isnt value
63 value = newValue
64 callback() for callback in callbacks[keypath]
65 @observeMutations newValue, obj[@id], keypath
66
67 unless callback in callbacks[keypath]
68 callbacks[keypath].push callback
69
70 @observeMutations obj[keypath], obj[@id], keypath
71
72 unsubscribe: (obj, keypath, callback) ->
73 callbacks = @weakmap[obj[@id]].callbacks[keypath]
74 callbacks.splice callbacks.indexOf(callback), 1
75 @unobserveMutations obj[keypath], obj[@id], keypath
76
77 read: (obj, keypath) ->
78 obj[keypath]
79
80 publish: (obj, keypath, value) ->
81 obj[keypath] = value
1 # Core binders that are included with Rivets.js.
2 Rivets.binders.enabled = (el, value) ->
3 el.disabled = !value
4
5 Rivets.binders.disabled = (el, value) ->
6 el.disabled = !!value
7
8 Rivets.binders.checked =
9 publishes: true
10 bind: (el) ->
11 Rivets.Util.bindEvent el, 'change', @publish
12 unbind: (el) ->
13 Rivets.Util.unbindEvent el, 'change', @publish
14 routine: (el, value) ->
15 if el.type is 'radio'
16 el.checked = el.value?.toString() is value?.toString()
17 else
18 el.checked = !!value
19
20 Rivets.binders.unchecked =
21 publishes: true
22 bind: (el) ->
23 Rivets.Util.bindEvent el, 'change', @publish
24 unbind: (el) ->
25 Rivets.Util.unbindEvent el, 'change', @publish
26 routine: (el, value) ->
27 if el.type is 'radio'
28 el.checked = el.value?.toString() isnt value?.toString()
29 else
30 el.checked = !value
31
32 Rivets.binders.show = (el, value) ->
33 el.style.display = if value then '' else 'none'
34
35 Rivets.binders.hide = (el, value) ->
36 el.style.display = if value then 'none' else ''
37
38 Rivets.binders.html = (el, value) ->
39 el.innerHTML = if value? then value else ''
40
41 Rivets.binders.value =
42 publishes: true
43 bind: (el) ->
44 Rivets.Util.bindEvent el, 'change', @publish
45 unbind: (el) ->
46 Rivets.Util.unbindEvent el, 'change', @publish
47 routine: (el, value) ->
48 if window.jQuery?
49 el = jQuery el
50
51 if value?.toString() isnt el.val()?.toString()
52 el.val if value? then value else ''
53 else
54 if el.type is 'select-multiple'
55 o.selected = o.value in value for o in el if value?
56 else if value?.toString() isnt el.value?.toString()
57 el.value = if value? then value else ''
58
59 Rivets.binders.text = (el, value) ->
60 if el.innerText?
61 el.innerText = if value? then value else ''
62 else
63 el.textContent = if value? then value else ''
64
65 Rivets.binders.if =
66 block: true
67
68 bind: (el) ->
69 unless @marker?
70 attr = [@view.config.prefix, @type].join('-').replace '--', '-'
71 declaration = el.getAttribute attr
72
73 @marker = document.createComment " rivets: #{@type} #{declaration} "
74
75 el.removeAttribute attr
76 el.parentNode.insertBefore @marker, el
77 el.parentNode.removeChild el
78
79 unbind: ->
80 @nested?.unbind()
81
82 routine: (el, value) ->
83 if !!value is not @nested?
84 if value
85 models = {}
86 models[key] = model for key, model of @view.models
87
88 options =
89 binders: @view.options.binders
90 formatters: @view.options.formatters
91 adapters: @view.options.adapters
92 config: @view.options.config
93
94 (@nested = new Rivets.View(el, models, options)).bind()
95 @marker.parentNode.insertBefore el, @marker.nextSibling
96 else
97 el.parentNode.removeChild el
98 @nested.unbind()
99 delete @nested
100
101 update: (models) ->
102 @nested?.update models
103
104 Rivets.binders.unless =
105 block: true
106
107 bind: (el) ->
108 Rivets.binders.if.bind.call @, el
109
110 unbind: ->
111 Rivets.binders.if.unbind.call @
112
113 routine: (el, value) ->
114 Rivets.binders.if.routine.call @, el, not value
115
116 update: (models) ->
117 Rivets.binders.if.update.call @, models
118
119 Rivets.binders['on-*'] =
120 function: true
121
122 unbind: (el) ->
123 Rivets.Util.unbindEvent el, @args[0], @handler if @handler
124
125 routine: (el, value) ->
126 Rivets.Util.unbindEvent el, @args[0], @handler if @handler
127 Rivets.Util.bindEvent el, @args[0], @handler = @eventHandler value
128
129 Rivets.binders['each-*'] =
130 block: true
131
132 bind: (el) ->
133 unless @marker?
134 attr = [@view.config.prefix, @type].join('-').replace '--', '-'
135 @marker = document.createComment " rivets: #{@type} "
136 @iterated = []
137
138 el.removeAttribute attr
139 el.parentNode.insertBefore @marker, el
140 el.parentNode.removeChild el
141
142 unbind: (el) ->
143 view.unbind() for view in @iterated if @iterated?
144
145 routine: (el, collection) ->
146 modelName = @args[0]
147 collection = collection or []
148
149 if @iterated.length > collection.length
150 for i in Array @iterated.length - collection.length
151 view = @iterated.pop()
152 view.unbind()
153 @marker.parentNode.removeChild view.els[0]
154
155 for model, index in collection
156 data = {}
157 data[modelName] = model
158
159 if not @iterated[index]?
160 for key, model of @view.models
161 data[key] ?= model
162
163 previous = if @iterated.length
164 @iterated[@iterated.length - 1].els[0]
165 else
166 @marker
167
168 options =
169 binders: @view.options.binders
170 formatters: @view.options.formatters
171 adapters: @view.options.adapters
172 config: {}
173
174 options.config[k] = v for k, v of @view.options.config
175 options.config.preloadData = true
176
177 template = el.cloneNode true
178 view = new Rivets.View(template, data, options)
179 view.bind()
180 @iterated.push view
181
182 @marker.parentNode.insertBefore template, previous.nextSibling
183 else if @iterated[index].models[modelName] isnt model
184 @iterated[index].update data
185
186 if el.nodeName is 'OPTION'
187 for binding in @view.bindings
188 if binding.el is @marker.parentNode and binding.type is 'value'
189 binding.sync()
190
191 update: (models) ->
192 data = {}
193
194 for key, model of models
195 data[key] = model unless key is @args[0]
196
197 view.update data for view in @iterated
198
199 Rivets.binders['class-*'] = (el, value) ->
200 elClass = " #{el.className} "
201
202 if !value is (elClass.indexOf(" #{@args[0]} ") isnt -1)
203 el.className = if value
204 "#{el.className} #{@args[0]}"
205 else
206 elClass.replace(" #{@args[0]} ", ' ').trim()
207
208 Rivets.binders['*'] = (el, value) ->
209 if value
210 el.setAttribute @type, value
211 else
212 el.removeAttribute @type
1 # Rivets.Binding
2 # --------------
3
4 # A single binding between a model attribute and a DOM element.
5 class Rivets.Binding
6 # All information about the binding is passed into the constructor; the
7 # containing view, the DOM node, the type of binding, the model object and the
8 # keypath at which to listen for changes.
9 constructor: (@view, @el, @type, @keypath, @options = {}) ->
10 @formatters = @options.formatters || []
11 @dependencies = []
12 @setBinder()
13 @setObserver()
14
15 setBinder: =>
16 unless @binder = @view.binders[@type]
17 for identifier, value of @view.binders
18 if identifier isnt '*' and identifier.indexOf('*') isnt -1
19 regexp = new RegExp "^#{identifier.replace('*', '.+')}$"
20 if regexp.test @type
21 @binder = value
22 @args = new RegExp("^#{identifier.replace('*', '(.+)')}$").exec @type
23 @args.shift()
24
25 @binder or= @view.binders['*']
26 @binder = {routine: @binder} if @binder instanceof Function
27
28 setObserver: =>
29 @observer = new KeypathObserver @view, @view.models, @keypath, (obs) =>
30 @unbind true if @key
31 @model = obs.target
32 @bind true if @key
33 @sync()
34
35 @key = @observer.key
36 @model = @observer.target
37
38 # Applies all the current formatters to the supplied value and returns the
39 # formatted value.
40 formattedValue: (value) =>
41 for formatter in @formatters
42 args = formatter.split /\s+/
43 id = args.shift()
44 formatter = @view.formatters[id]
45
46 if formatter?.read instanceof Function
47 value = formatter.read value, args...
48 else if formatter instanceof Function
49 value = formatter value, args...
50
51 value
52
53 # Returns an event handler for the binding around the supplied function.
54 eventHandler: (fn) =>
55 handler = (binding = @).view.config.handler
56 (ev) -> handler.call fn, @, ev, binding
57
58 # Sets the value for the binding. This Basically just runs the binding routine
59 # with the suplied value formatted.
60 set: (value) =>
61 value = if value instanceof Function and !@binder.function
62 @formattedValue value.call @model
63 else
64 @formattedValue value
65
66 @binder.routine?.call @, @el, value
67
68 # Syncs up the view binding with the model.
69 sync: =>
70 @set if @key
71 @view.adapters[@key.interface].read @model, @key.path
72 else
73 @model
74
75 # Publishes the value currently set on the input element back to the model.
76 publish: =>
77 value = Rivets.Util.getInputValue @el
78
79 for formatter in @formatters.slice(0).reverse()
80 args = formatter.split /\s+/
81 id = args.shift()
82
83 if @view.formatters[id]?.publish
84 value = @view.formatters[id].publish value, args...
85
86 @view.adapters[@key.interface].publish @model, @key.path, value
87
88 # Subscribes to the model for changes at the specified keypath. Bi-directional
89 # routines will also listen for changes on the element to propagate them back
90 # to the model.
91 bind: (silent = false) =>
92 @binder.bind?.call @, @el unless silent
93 @view.adapters[@key.interface].subscribe(@model, @key.path, @sync) if @key
94 @sync() if @view.config.preloadData unless silent
95
96 if @options.dependencies?.length
97 for dependency in @options.dependencies
98 observer = new KeypathObserver @view, @model, dependency, (obs, prev) =>
99 key = obs.key
100 @view.adapters[key.interface].unsubscribe prev, key.path, @sync
101 @view.adapters[key.interface].subscribe obs.target, key.path, @sync
102 @sync()
103
104 key = observer.key
105 @view.adapters[key.interface].subscribe observer.target, key.path, @sync
106 @dependencies.push observer
107
108 # Unsubscribes from the model and the element.
109 unbind: (silent = false) =>
110 @binder.unbind?.call @, @el unless silent
111 @view.adapters[@key.interface].unsubscribe(@model, @key.path, @sync) if @key
112
113 if @dependencies.length
114 for obs in @dependencies
115 key = obs.key
116 @view.adapters[key.interface].unsubscribe obs.target, key.path, @sync
117
118 @dependencies = []
119
120 # Updates the binding's model from what is currently set on the view. Unbinds
121 # the old model first and then re-binds with the new model.
122 update: (models = {}) =>
123 @binder.update?.call @, models
124
125 # Rivets.ComponentBinding
126 # -----------------------
127
128 # A component view encapsulated as a binding within it's parent view.
129 class Rivets.ComponentBinding extends Rivets.Binding
130 # Initializes a component binding for the specified view. The raw component
131 # element is passed in along with the component type. Attributes and scope
132 # inflections are determined based on the components defined attributes.
133 constructor: (@view, @el, @type) ->
134 @component = Rivets.components[@type]
135 @attributes = {}
136 @inflections = {}
137
138 for attribute in @el.attributes or []
139 if attribute.name in @component.attributes
140 @attributes[attribute.name] = attribute.value
141 else
142 @inflections[attribute.name] = attribute.value
143
144 # Intercepts `Rivets.Binding::sync` since component bindings are not bound to
145 # a particular model to update it's value.
146 sync: ->
147
148 # Returns an object map using the component's scope inflections.
149 locals: (models = @view.models) =>
150 result = {}
151
152 for key, inverse of @inflections
153 result[key] = (result[key] or models)[path] for path in inverse.split '.'
154
155 result[key] ?= model for key, model of models
156 result
157
158 # Intercepts `Rivets.Binding::update` to be called on `@componentView` with a
159 # localized map of the models.
160 update: (models) =>
161 @componentView?.update @locals models
162
163 # Intercepts `Rivets.Binding::bind` to build `@componentView` with a localized
164 # map of models from the root view. Bind `@componentView` on subsequent calls.
165 bind: =>
166 if @componentView?
167 @componentView?.bind()
168 else
169 el = @component.build.call @attributes
170 (@componentView = new Rivets.View(el, @locals(), @view.options)).bind()
171 @el.parentNode.replaceChild el, @el
172
173 # Intercept `Rivets.Binding::unbind` to be called on `@componentView`.
174 unbind: =>
175 @componentView?.unbind()
176
177 # Rivets.TextBinding
178 # -----------------------
179
180 # A text node binding, defined internally to deal with text and element node
181 # differences while avoiding it being overwritten.
182 class Rivets.TextBinding extends Rivets.Binding
183 # Initializes a text binding for the specified view and text node.
184 constructor: (@view, @el, @type, @keypath, @options = {}) ->
185 @formatters = @options.formatters || []
186 @dependencies = []
187 @setObserver()
188
189 # A standard routine binder used for text node bindings.
190 binder:
191 routine: (node, value) ->
192 node.data = value ? ''
193
194 # Wrap the call to `sync` in fat-arrow to avoid function context issues.
195 sync: =>
196 super
1 # Rivets.factory
2 # --------------
3
4 # Rivets.js module factory.
5 Rivets.factory = (exports) ->
6 # Exposes the full Rivets namespace. This is mainly used for isolated testing.
7 exports._ = Rivets
8
9 # Exposes the binders object.
10 exports.binders = Rivets.binders
11
12 # Exposes the components object.
13 exports.components = Rivets.components
14
15 # Exposes the formatters object.
16 exports.formatters = Rivets.formatters
17
18 # Exposes the adapters object.
19 exports.adapters = Rivets.adapters
20
21 # Exposes the config object.
22 exports.config = Rivets.config
23
24 # Merges an object literal onto the config object.
25 exports.configure = (options={}) ->
26 for property, value of options
27 Rivets.config[property] = value
28 return
29
30 # Binds a set of model objects to a parent DOM element and returns a
31 # `Rivets.View` instance.
32 exports.bind = (el, models = {}, options = {}) ->
33 view = new Rivets.View(el, models, options)
34 view.bind()
35 view
36
37 # Exports Rivets.js for CommonJS, AMD and the browser.
38 if typeof exports == 'object'
39 Rivets.factory(exports)
40 else if typeof define == 'function' && define.amd
41 define ['exports'], (exports) ->
42 Rivets.factory(@rivets = exports)
43 return exports
44 else
45 Rivets.factory(@rivets = {})
1 class KeypathObserver
2 constructor: (@view, @model, @keypath, @callback) ->
3 @parse()
4 @objectPath = []
5 @target = @realize()
6
7 parse: =>
8 interfaces = (k for k, v of @view.adapters)
9
10 if @keypath[0] in interfaces
11 root = @keypath[0]
12 path = @keypath.substr 1
13 else
14 root = @view.config.rootInterface
15 path = @keypath
16
17 @tokens = Rivets.KeypathParser.parse path, interfaces, root
18 @key = @tokens.pop()
19
20 update: =>
21 unless (next = @realize()) is @target
22 prev = @target
23 @target = next
24 @callback @, prev
25
26 realize: =>
27 current = @model
28
29 for token, index in @tokens
30 if @objectPath[index]?
31 if current isnt prev = @objectPath[index]
32 @view.adapters[token.interface].unsubscribe prev, token.path, @update
33 @view.adapters[token.interface].subscribe current, token.path, @update
34 @objectPath[index] = current
35 else
36 @view.adapters[token.interface].subscribe current, token.path, @update
37 @objectPath[index] = current
38
39 current = @view.adapters[token.interface].read current, token.path
40
41 current
1 # Rivets.KeypathParser
2 # --------------------
3
4 # Parser and tokenizer for keypaths in binding declarations.
5 class Rivets.KeypathParser
6 # Parses the keypath and returns a set of adapter interface + path tokens.
7 @parse: (keypath, interfaces, root) ->
8 tokens = []
9 current = {interface: root, path: ''}
10
11 for index, char of keypath
12 if char in interfaces
13 tokens.push current
14 current = {interface: char, path: ''}
15 else
16 current.path += char
17
18 tokens.push current
19 tokens
20
21 # Rivets.TextTemplateParser
22 # -------------------------
23
24 # Rivets.js text template parser and tokenizer for mustache-style text content
25 # binding declarations.
26 class Rivets.TextTemplateParser
27 @types:
28 text: 0
29 binding: 1
30
31 # Parses the template and returns a set of tokens, separating static portions
32 # of text from binding declarations.
33 @parse: (template, delimiters) ->
34 tokens = []
35 length = template.length
36 index = 0
37 lastIndex = 0
38
39 while lastIndex < length
40 index = template.indexOf delimiters[0], lastIndex
41
42 if index < 0
43 tokens.push type: @types.text, value: template.slice lastIndex
44 break
45 else
46 if index > 0 and lastIndex < index
47 tokens.push type: @types.text, value: template.slice lastIndex, index
48
49 lastIndex = index + 2
50 index = template.indexOf delimiters[1], lastIndex
51
52 if index < 0
53 substring = template.slice lastIndex - 2
54 lastToken = tokens[tokens.length - 1]
55
56 if lastToken?.type is @types.text
57 lastToken.value += substring
58 else
59 tokens.push type: @types.text, value: substring
60
61 break
62
63 value = template.slice(lastIndex, index).trim()
64 tokens.push type: @types.binding, value: value
65 lastIndex = index + 2
66
67 tokens
1 # Rivets.Util
2 # -----------
3
4 # Houses common utility functions used internally by Rivets.js.
5 Rivets.Util =
6 # Create a single DOM event binding.
7 bindEvent: (el, event, handler) ->
8 if window.jQuery?
9 el = jQuery el
10 if el.on? then el.on event, handler else el.bind event, handler
11 else if window.addEventListener?
12 el.addEventListener event, handler, false
13 else
14 event = 'on' + event
15 el.attachEvent event, handler
16
17 # Remove a single DOM event binding.
18 unbindEvent: (el, event, handler) ->
19 if window.jQuery?
20 el = jQuery el
21 if el.off? then el.off event, handler else el.unbind event, handler
22 else if window.removeEventListener?
23 el.removeEventListener event, handler, false
24 else
25 event = 'on' + event
26 el.detachEvent event, handler
27
28 # Get the current value of an input node.
29 getInputValue: (el) ->
30 if window.jQuery?
31 el = jQuery el
32
33 switch el[0].type
34 when 'checkbox' then el.is ':checked'
35 else el.val()
36 else
37 switch el.type
38 when 'checkbox' then el.checked
39 when 'select-multiple' then o.value for o in el when o.selected
40 else el.value
1 # Rivets.View
2 # -----------
3
4 # A collection of bindings built from a set of parent nodes.
5 class Rivets.View
6 # The DOM elements and the model objects for binding are passed into the
7 # constructor along with any local options that should be used throughout the
8 # context of the view and it's bindings.
9 constructor: (@els, @models, @options = {}) ->
10 @els = [@els] unless (@els.jquery || @els instanceof Array)
11
12 for option in ['config', 'binders', 'formatters', 'adapters']
13 @[option] = {}
14 @[option][k] = v for k, v of @options[option] if @options[option]
15 @[option][k] ?= v for k, v of Rivets[option]
16
17 @build()
18
19 # Regular expression used to match binding attributes.
20 bindingRegExp: =>
21 new RegExp "^#{@config.prefix}-"
22
23 # Regular expression used to match component nodes.
24 componentRegExp: =>
25 new RegExp "^#{@config.prefix.toUpperCase()}-"
26
27 # Parses the DOM tree and builds `Rivets.Binding` instances for every matched
28 # binding declaration.
29 build: =>
30 @bindings = []
31 skipNodes = []
32 bindingRegExp = @bindingRegExp()
33 componentRegExp = @componentRegExp()
34
35 buildBinding = (binding, node, type, declaration) =>
36 options = {}
37
38 pipes = (pipe.trim() for pipe in declaration.split '|')
39 context = (ctx.trim() for ctx in pipes.shift().split '<')
40 keypath = context.shift()
41
42 options.formatters = pipes
43
44 if dependencies = context.shift()
45 options.dependencies = dependencies.split /\s+/
46
47 @bindings.push new Rivets[binding] @, node, type, keypath, options
48
49 parse = (node) =>
50 unless node in skipNodes
51 if node.nodeType is Node.TEXT_NODE
52 parser = Rivets.TextTemplateParser
53
54 if delimiters = @config.templateDelimiters
55 if (tokens = parser.parse(node.data, delimiters)).length
56 unless tokens.length is 1 and tokens[0].type is parser.types.text
57 [startToken, restTokens...] = tokens
58 node.data = startToken.value
59
60 if startToken.type is 0
61 node.data = startToken.value
62 else
63 buildBinding 'TextBinding', node, null, startToken.value
64
65 for token in restTokens
66 text = document.createTextNode token.value
67 node.parentNode.appendChild text
68
69 if token.type is 1
70 buildBinding 'TextBinding', text, null, token.value
71 else if componentRegExp.test node.tagName
72 type = node.tagName.replace(componentRegExp, '').toLowerCase()
73 @bindings.push new Rivets.ComponentBinding @, node, type
74
75 else if node.attributes?
76 for attribute in node.attributes
77 if bindingRegExp.test attribute.name
78 type = attribute.name.replace bindingRegExp, ''
79 unless binder = @binders[type]
80 for identifier, value of @binders
81 if identifier isnt '*' and identifier.indexOf('*') isnt -1
82 regexp = new RegExp "^#{identifier.replace('*', '.+')}$"
83 if regexp.test type
84 binder = value
85
86 binder or= @binders['*']
87
88 if binder.block
89 skipNodes.push n for n in node.childNodes
90 attributes = [attribute]
91
92 for attribute in attributes or node.attributes
93 if bindingRegExp.test attribute.name
94 type = attribute.name.replace bindingRegExp, ''
95 buildBinding 'Binding', node, type, attribute.value
96
97 parse childNode for childNode in node.childNodes
98
99 parse el for el in @els
100
101 return
102
103 # Returns an array of bindings where the supplied function evaluates to true.
104 select: (fn) =>
105 binding for binding in @bindings when fn binding
106
107 # Binds all of the current bindings for this view.
108 bind: =>
109 binding.bind() for binding in @bindings
110
111 # Unbinds all of the current bindings for this view.
112 unbind: =>
113 binding.unbind() for binding in @bindings
114
115 # Syncs up the view with the model by running the routines on all bindings.
116 sync: =>
117 binding.sync() for binding in @bindings
118
119 # Publishes the input values from the view back to the model (reverse sync).
120 publish: =>
121 binding.publish() for binding in @select (b) -> b.binder.publishes
122
123 # Updates the view's models along with any affected bindings.
124 update: (models = {}) =>
125 @models[key] = model for key, model of models
126 binding.update models for binding in @bindings