Merge pull request #209 from mikeric/adapters
0.6.0 WIP
Showing
13 changed files
with
883 additions
and
105 deletions
... | @@ -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,18 +2,13 @@ describe('Functional', function() { | ... | @@ -2,18 +2,13 @@ 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 | bindData = {data: data}; | ||
7 | el = document.createElement('div'); | ||
8 | input = document.createElement('input'); | ||
9 | input.setAttribute('type', 'text'); | ||
10 | |||
11 | rivets.configure({ | ||
12 | preloadData: true, | ||
13 | adapter: { | ||
14 | subscribe: function(obj, keypath, callback) { | 6 | subscribe: function(obj, keypath, callback) { |
15 | obj.on(keypath, callback); | 7 | obj.on(keypath, callback); |
16 | }, | 8 | }, |
9 | unsubscribe: function(obj, keypath, callback) { | ||
10 | obj.off(keypath, callback); | ||
11 | }, | ||
17 | read: function(obj, keypath) { | 12 | read: function(obj, keypath) { |
18 | return obj.get(keypath); | 13 | return obj.get(keypath); |
19 | }, | 14 | }, |
... | @@ -22,14 +17,27 @@ describe('Functional', function() { | ... | @@ -22,14 +17,27 @@ describe('Functional', function() { |
22 | attributes[keypath] = value; | 17 | attributes[keypath] = value; |
23 | obj.set(attributes); | 18 | obj.set(attributes); |
24 | } | 19 | } |
25 | } | 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'}] | ||
26 | }); | 28 | }); |
29 | |||
30 | bindData = {data: data}; | ||
31 | |||
32 | el = document.createElement('div'); | ||
33 | input = document.createElement('input'); | ||
34 | input.setAttribute('type', 'text'); | ||
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') | ... | ... |
src/adapters.coffee
0 → 100644
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 |
src/binders.coffee
0 → 100644
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 |
src/bindings.coffee
0 → 100644
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 |
src/export.coffee
0 → 100644
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 = {}) |
src/keypath_observer.coffee
0 → 100644
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 |
src/parsers.coffee
0 → 100644
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 |
This diff is collapsed.
Click to expand it.
src/util.coffee
0 → 100644
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 |
src/view.coffee
0 → 100644
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 |
-
Please register or sign in to post a comment