Merge pull request #209 from mikeric/adapters
0.6.0 WIP
Showing
13 changed files
with
918 additions
and
855 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,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') | ... | ... |
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 |
1 | # Rivets.js | ||
2 | # ========= | ||
3 | |||
4 | # > version: 0.5.13 | ||
5 | # > author: Michael Richards | ||
6 | # > license: MIT | ||
7 | # > | ||
8 | # > http://rivetsjs.com/ | ||
9 | |||
10 | # --- | ||
11 | |||
12 | # The Rivets namespace. | 1 | # The Rivets namespace. |
13 | Rivets = {} | 2 | Rivets = |
14 | 3 | # Binder definitions, publicly accessible on `module.binders`. Can be | |
15 | # jQuery || Zepto | 4 | # overridden globally or local to a `Rivets.View` instance. |
16 | jQuery = window.jQuery or window.Zepto | 5 | binders: {} |
17 | 6 | ||
18 | # Polyfill For `String::trim`. | 7 | # Component definitions, publicly accessible on `module.components`. Can be |
19 | unless String::trim then String::trim = -> @replace /^\s+|\s+$/g, '' | 8 | # overridden globally or local to a `Rivets.View` instance. |
20 | 9 | components: {} | |
21 | # Rivets.Binding | 10 | |
22 | # -------------- | 11 | # Formatter definitions, publicly accessible on `module.formatters`. Can be |
23 | 12 | # overridden globally or local to a `Rivets.View` instance. | |
24 | # A single binding between a model attribute and a DOM element. | 13 | formatters: {} |
25 | class Rivets.Binding | 14 | |
26 | # All information about the binding is passed into the constructor; the | 15 | # Adapter definitions, publicly accessible on `module.adapters`. Can be |
27 | # containing view, the DOM node, the type of binding, the model object and the | 16 | # overridden globally or local to a `Rivets.View` instance. |
28 | # keypath at which to listen for changes. | 17 | adapters: {} |
29 | constructor: (@view, @el, @type, @key, @keypath, @options = {}) -> | 18 | |
30 | unless @binder = @view.binders[type] | 19 | # The default configuration, publicly accessible on `module.config`. Can be |
31 | for identifier, value of @view.binders | 20 | # overridden globally or local to a `Rivets.View` instance. |
32 | if identifier isnt '*' and identifier.indexOf('*') isnt -1 | 21 | config: |
33 | regexp = new RegExp "^#{identifier.replace('*', '.+')}$" | 22 | prefix: 'rv' |
34 | if regexp.test type | 23 | templateDelimiters: ['{', '}'] |
35 | @binder = value | 24 | rootInterface: '.' |
36 | @args = new RegExp("^#{identifier.replace('*', '(.+)')}$").exec type | 25 | preloadData: true |
37 | @args.shift() | 26 | |
38 | 27 | handler: (context, ev, binding) -> | |
39 | @binder or= @view.binders['*'] | 28 | @call context, ev, binding.view.models |
40 | @binder = {routine: @binder} if @binder instanceof Function | ||
41 | @formatters = @options.formatters || [] | ||
42 | @model = if @key then @view.models[@key] else @view.models | ||
43 | |||
44 | # Applies all the current formatters to the supplied value and returns the | ||
45 | # formatted value. | ||
46 | formattedValue: (value) => | ||
47 | for formatter in @formatters | ||
48 | args = formatter.split /\s+/ | ||
49 | id = args.shift() | ||
50 | |||
51 | formatter = if @model[id] instanceof Function | ||
52 | @model[id] | ||
53 | else | ||
54 | @view.formatters[id] | ||
55 | |||
56 | if formatter?.read instanceof Function | ||
57 | value = formatter.read value, args... | ||
58 | else if formatter instanceof Function | ||
59 | value = formatter value, args... | ||
60 | |||
61 | value | ||
62 | |||
63 | # Returns an event handler for the binding around the supplied function. | ||
64 | eventHandler: (fn) => | ||
65 | handler = (binding = @).view.config.handler | ||
66 | (ev) -> handler.call fn, @, ev, binding | ||
67 | |||
68 | # Sets the value for the binding. This Basically just runs the binding routine | ||
69 | # with the suplied value formatted. | ||
70 | set: (value) => | ||
71 | value = if value instanceof Function and !@binder.function | ||
72 | @formattedValue value.call @model | ||
73 | else | ||
74 | @formattedValue value | ||
75 | |||
76 | @binder.routine?.call @, @el, value | ||
77 | |||
78 | # Syncs up the view binding with the model. | ||
79 | sync: => | ||
80 | @set if @options.bypass | ||
81 | @model[@keypath] | ||
82 | else | ||
83 | @view.config.adapter.read @model, @keypath | ||
84 | |||
85 | # Publishes the value currently set on the input element back to the model. | ||
86 | publish: => | ||
87 | value = Rivets.Util.getInputValue @el | ||
88 | |||
89 | for formatter in @formatters.slice(0).reverse() | ||
90 | args = formatter.split /\s+/ | ||
91 | id = args.shift() | ||
92 | |||
93 | if @view.formatters[id]?.publish | ||
94 | value = @view.formatters[id].publish value, args... | ||
95 | |||
96 | @view.config.adapter.publish @model, @keypath, value | ||
97 | |||
98 | # Subscribes to the model for changes at the specified keypath. Bi-directional | ||
99 | # routines will also listen for changes on the element to propagate them back | ||
100 | # to the model. | ||
101 | bind: => | ||
102 | @binder.bind?.call @, @el | ||
103 | |||
104 | if @options.bypass | ||
105 | @sync() | ||
106 | else | ||
107 | @view.config.adapter.subscribe @model, @keypath, @sync | ||
108 | @sync() if @view.config.preloadData | ||
109 | |||
110 | if @options.dependencies?.length | ||
111 | for dependency in @options.dependencies | ||
112 | if /^\./.test dependency | ||
113 | model = @model | ||
114 | keypath = dependency.substr 1 | ||
115 | else | ||
116 | dependency = dependency.split '.' | ||
117 | model = @view.models[dependency.shift()] | ||
118 | keypath = dependency.join '.' | ||
119 | |||
120 | @view.config.adapter.subscribe model, keypath, @sync | ||
121 | |||
122 | # Unsubscribes from the model and the element. | ||
123 | unbind: => | ||
124 | @binder.unbind?.call @, @el | ||
125 | |||
126 | unless @options.bypass | ||
127 | @view.config.adapter.unsubscribe @model, @keypath, @sync | ||
128 | |||
129 | if @options.dependencies?.length | ||
130 | for dependency in @options.dependencies | ||
131 | if /^\./.test dependency | ||
132 | model = @model | ||
133 | keypath = dependency.substr 1 | ||
134 | else | ||
135 | dependency = dependency.split '.' | ||
136 | model = @view.models[dependency.shift()] | ||
137 | keypath = dependency.join '.' | ||
138 | |||
139 | @view.config.adapter.unsubscribe model, keypath, @sync | ||
140 | |||
141 | # Updates the binding's model from what is currently set on the view. Unbinds | ||
142 | # the old model first and then re-binds with the new model. | ||
143 | update: (models = {}) => | ||
144 | if @key | ||
145 | if models[@key] | ||
146 | unless @options.bypass | ||
147 | @view.config.adapter.unsubscribe @model, @keypath, @sync | ||
148 | |||
149 | @model = models[@key] | ||
150 | |||
151 | if @options.bypass | ||
152 | @sync() | ||
153 | else | ||
154 | @view.config.adapter.subscribe @model, @keypath, @sync | ||
155 | @sync() if @view.config.preloadData | ||
156 | else | ||
157 | @sync() | ||
158 | |||
159 | @binder.update?.call @, models | ||
160 | |||
161 | # Rivets.ComponentBinding | ||
162 | # ----------------------- | ||
163 | |||
164 | # A component view encapsulated as a binding within it's parent view. | ||
165 | class Rivets.ComponentBinding extends Rivets.Binding | ||
166 | # Initializes a component binding for the specified view. The raw component | ||
167 | # element is passed in along with the component type. Attributes and scope | ||
168 | # inflections are determined based on the components defined attributes. | ||
169 | constructor: (@view, @el, @type) -> | ||
170 | @component = Rivets.components[@type] | ||
171 | @attributes = {} | ||
172 | @inflections = {} | ||
173 | |||
174 | for attribute in @el.attributes or [] | ||
175 | if attribute.name in @component.attributes | ||
176 | @attributes[attribute.name] = attribute.value | ||
177 | else | ||
178 | @inflections[attribute.name] = attribute.value | ||
179 | |||
180 | # Intercepts `Rivets.Binding::sync` since component bindings are not bound to | ||
181 | # a particular model to update it's value. | ||
182 | sync: -> | ||
183 | |||
184 | # Returns an object map using the component's scope inflections. | ||
185 | locals: (models = @view.models) => | ||
186 | result = {} | ||
187 | |||
188 | for key, inverse of @inflections | ||
189 | result[key] = (result[key] or models)[path] for path in inverse.split '.' | ||
190 | |||
191 | result[key] ?= model for key, model of models | ||
192 | result | ||
193 | |||
194 | # Intercepts `Rivets.Binding::update` to be called on `@componentView` with a | ||
195 | # localized map of the models. | ||
196 | update: (models) => | ||
197 | @componentView?.update @locals models | ||
198 | |||
199 | # Intercepts `Rivets.Binding::bind` to build `@componentView` with a localized | ||
200 | # map of models from the root view. Bind `@componentView` on subsequent calls. | ||
201 | bind: => | ||
202 | if @componentView? | ||
203 | @componentView?.bind() | ||
204 | else | ||
205 | el = @component.build.call @attributes | ||
206 | (@componentView = new Rivets.View(el, @locals(), @view.options)).bind() | ||
207 | @el.parentNode.replaceChild el, @el | ||
208 | |||
209 | # Intercept `Rivets.Binding::unbind` to be called on `@componentView`. | ||
210 | unbind: => | ||
211 | @componentView?.unbind() | ||
212 | |||
213 | # Rivets.TextBinding | ||
214 | # ----------------------- | ||
215 | |||
216 | # A text node binding, defined internally to deal with text and element node | ||
217 | # differences while avoiding it being overwritten. | ||
218 | class Rivets.TextBinding extends Rivets.Binding | ||
219 | # Initializes a text binding for the specified view and text node. | ||
220 | constructor: (@view, @el, @type, @key, @keypath, @options = {}) -> | ||
221 | @formatters = @options.formatters || [] | ||
222 | @model = if @key then @view.models[@key] else @view.models | ||
223 | |||
224 | # A standard routine binder used for text node bindings. | ||
225 | binder: | ||
226 | routine: (node, value) -> | ||
227 | node.data = value ? '' | ||
228 | |||
229 | # Wrap the call to `sync` in fat-arrow to avoid function context issues. | ||
230 | sync: => | ||
231 | super | ||
232 | |||
233 | # Rivets.View | ||
234 | # ----------- | ||
235 | |||
236 | # A collection of bindings built from a set of parent nodes. | ||
237 | class Rivets.View | ||
238 | # The DOM elements and the model objects for binding are passed into the | ||
239 | # constructor along with any local options that should be used throughout the | ||
240 | # context of the view and it's bindings. | ||
241 | constructor: (@els, @models, @options = {}) -> | ||
242 | @els = [@els] if typeof @els.length is 'undefined' | ||
243 | |||
244 | for option in ['config', 'binders', 'formatters'] | ||
245 | @[option] = {} | ||
246 | @[option][k] = v for k, v of @options[option] if @options[option] | ||
247 | @[option][k] ?= v for k, v of Rivets[option] | ||
248 | |||
249 | @build() | ||
250 | |||
251 | # Regular expression used to match binding attributes. | ||
252 | bindingRegExp: => | ||
253 | prefix = @config.prefix | ||
254 | if prefix then new RegExp("^data-#{prefix}-") else /^data-/ | ||
255 | |||
256 | # Regular expression used to match component nodes. | ||
257 | componentRegExp: => | ||
258 | new RegExp "^#{@config.prefix?.toUpperCase() ? 'RV'}-" | ||
259 | |||
260 | # Parses the DOM tree and builds `Rivets.Binding` instances for every matched | ||
261 | # binding declaration. | ||
262 | build: => | ||
263 | @bindings = [] | ||
264 | skipNodes = [] | ||
265 | bindingRegExp = @bindingRegExp() | ||
266 | componentRegExp = @componentRegExp() | ||
267 | |||
268 | |||
269 | buildBinding = (binding, node, type, declaration) => | ||
270 | options = {} | ||
271 | |||
272 | pipes = (pipe.trim() for pipe in declaration.split '|') | ||
273 | context = (ctx.trim() for ctx in pipes.shift().split '<') | ||
274 | path = context.shift() | ||
275 | splitPath = path.split /\.|:/ | ||
276 | options.formatters = pipes | ||
277 | options.bypass = path.indexOf(':') != -1 | ||
278 | |||
279 | if splitPath[0] | ||
280 | key = splitPath.shift() | ||
281 | else | ||
282 | key = null | ||
283 | splitPath.shift() | ||
284 | |||
285 | keypath = splitPath.join '.' | ||
286 | |||
287 | if dependencies = context.shift() | ||
288 | options.dependencies = dependencies.split /\s+/ | ||
289 | |||
290 | @bindings.push new Rivets[binding] @, node, type, key, keypath, options | ||
291 | |||
292 | parse = (node) => | ||
293 | unless node in skipNodes | ||
294 | if node.nodeType is Node.TEXT_NODE | ||
295 | parser = Rivets.TextTemplateParser | ||
296 | |||
297 | if delimiters = @config.templateDelimiters | ||
298 | if (tokens = parser.parse(node.data, delimiters)).length | ||
299 | unless tokens.length is 1 and tokens[0].type is parser.types.text | ||
300 | [startToken, restTokens...] = tokens | ||
301 | node.data = startToken.value | ||
302 | |||
303 | if startToken.type is 0 | ||
304 | node.data = startToken.value | ||
305 | else | ||
306 | buildBinding 'TextBinding', node, null, startToken.value | ||
307 | |||
308 | for token in restTokens | ||
309 | text = document.createTextNode token.value | ||
310 | node.parentNode.appendChild text | ||
311 | |||
312 | if token.type is 1 | ||
313 | buildBinding 'TextBinding', text, null, token.value | ||
314 | else if componentRegExp.test node.tagName | ||
315 | type = node.tagName.replace(componentRegExp, '').toLowerCase() | ||
316 | @bindings.push new Rivets.ComponentBinding @, node, type | ||
317 | |||
318 | else if node.attributes? | ||
319 | for attribute in node.attributes | ||
320 | if bindingRegExp.test attribute.name | ||
321 | type = attribute.name.replace bindingRegExp, '' | ||
322 | unless binder = @binders[type] | ||
323 | for identifier, value of @binders | ||
324 | if identifier isnt '*' and identifier.indexOf('*') isnt -1 | ||
325 | regexp = new RegExp "^#{identifier.replace('*', '.+')}$" | ||
326 | if regexp.test type | ||
327 | binder = value | ||
328 | |||
329 | binder or= @binders['*'] | ||
330 | |||
331 | if binder.block | ||
332 | skipNodes.push n for n in node.childNodes | ||
333 | attributes = [attribute] | ||
334 | |||
335 | for attribute in attributes or node.attributes | ||
336 | if bindingRegExp.test attribute.name | ||
337 | type = attribute.name.replace bindingRegExp, '' | ||
338 | buildBinding 'Binding', node, type, attribute.value | ||
339 | |||
340 | parse childNode for childNode in node.childNodes | ||
341 | |||
342 | parse el for el in @els | ||
343 | |||
344 | return | ||
345 | |||
346 | # Returns an array of bindings where the supplied function evaluates to true. | ||
347 | select: (fn) => | ||
348 | binding for binding in @bindings when fn binding | ||
349 | |||
350 | # Binds all of the current bindings for this view. | ||
351 | bind: => | ||
352 | binding.bind() for binding in @bindings | ||
353 | |||
354 | # Unbinds all of the current bindings for this view. | ||
355 | unbind: => | ||
356 | binding.unbind() for binding in @bindings | ||
357 | |||
358 | # Syncs up the view with the model by running the routines on all bindings. | ||
359 | sync: => | ||
360 | binding.sync() for binding in @bindings | ||
361 | |||
362 | # Publishes the input values from the view back to the model (reverse sync). | ||
363 | publish: => | ||
364 | binding.publish() for binding in @select (b) -> b.binder.publishes | ||
365 | |||
366 | # Updates the view's models along with any affected bindings. | ||
367 | update: (models = {}) => | ||
368 | @models[key] = model for key, model of models | ||
369 | binding.update models for binding in @bindings | ||
370 | |||
371 | # Rivets.TextTemplateParser | ||
372 | # ------------------------- | ||
373 | |||
374 | # Rivets.js text template parser and tokenizer for mustache-style text content | ||
375 | # binding declarations. | ||
376 | class Rivets.TextTemplateParser | ||
377 | @types: | ||
378 | text: 0 | ||
379 | binding: 1 | ||
380 | |||
381 | # Parses the template and returns a set of tokens, separating static portions | ||
382 | # of text from binding declarations. | ||
383 | @parse: (template, delimiters) -> | ||
384 | tokens = [] | ||
385 | length = template.length | ||
386 | index = 0 | ||
387 | lastIndex = 0 | ||
388 | |||
389 | while lastIndex < length | ||
390 | index = template.indexOf delimiters[0], lastIndex | ||
391 | |||
392 | if index < 0 | ||
393 | tokens.push type: @types.text, value: template.slice lastIndex | ||
394 | break | ||
395 | else | ||
396 | if index > 0 and lastIndex < index | ||
397 | tokens.push type: @types.text, value: template.slice lastIndex, index | ||
398 | |||
399 | lastIndex = index + 2 | ||
400 | index = template.indexOf delimiters[1], lastIndex | ||
401 | |||
402 | if index < 0 | ||
403 | substring = template.slice lastIndex - 2 | ||
404 | lastToken = tokens[tokens.length - 1] | ||
405 | |||
406 | if lastToken?.type is @types.text | ||
407 | lastToken.value += substring | ||
408 | else | ||
409 | tokens.push type: @types.text, value: substring | ||
410 | |||
411 | break | ||
412 | |||
413 | value = template.slice(lastIndex, index).trim() | ||
414 | tokens.push type: @types.binding, value: value | ||
415 | lastIndex = index + 2 | ||
416 | |||
417 | tokens | ||
418 | |||
419 | # Rivets.Util | ||
420 | # ----------- | ||
421 | |||
422 | # Houses common utility functions used internally by Rivets.js. | ||
423 | Rivets.Util = | ||
424 | # Create a single DOM event binding. | ||
425 | bindEvent: (el, event, handler) -> | ||
426 | if window.jQuery? | ||
427 | el = jQuery el | ||
428 | if el.on? then el.on event, handler else el.bind event, handler | ||
429 | else if window.addEventListener? | ||
430 | el.addEventListener event, handler, false | ||
431 | else | ||
432 | event = 'on' + event | ||
433 | el.attachEvent event, handler | ||
434 | |||
435 | # Remove a single DOM event binding. | ||
436 | unbindEvent: (el, event, handler) -> | ||
437 | if window.jQuery? | ||
438 | el = jQuery el | ||
439 | if el.off? then el.off event, handler else el.unbind event, handler | ||
440 | else if window.removeEventListener? | ||
441 | el.removeEventListener event, handler, false | ||
442 | else | ||
443 | event = 'on' + event | ||
444 | el.detachEvent event, handler | ||
445 | |||
446 | # Get the current value of an input node. | ||
447 | getInputValue: (el) -> | ||
448 | if window.jQuery? | ||
449 | el = jQuery el | ||
450 | |||
451 | switch el[0].type | ||
452 | when 'checkbox' then el.is ':checked' | ||
453 | else el.val() | ||
454 | else | ||
455 | switch el.type | ||
456 | when 'checkbox' then el.checked | ||
457 | when 'select-multiple' then o.value for o in el when o.selected | ||
458 | else el.value | ||
459 | |||
460 | # Rivets.binders | ||
461 | # -------------- | ||
462 | |||
463 | # Core binders that are included with Rivets.js, publicly available on | ||
464 | # `module.binders`. Can be overridden globally or local to a `Rivets.View` | ||
465 | # instance. | ||
466 | Rivets.binders = | ||
467 | enabled: (el, value) -> | ||
468 | el.disabled = !value | ||
469 | |||
470 | disabled: (el, value) -> | ||
471 | el.disabled = !!value | ||
472 | |||
473 | checked: | ||
474 | publishes: true | ||
475 | bind: (el) -> | ||
476 | Rivets.Util.bindEvent el, 'change', @publish | ||
477 | unbind: (el) -> | ||
478 | Rivets.Util.unbindEvent el, 'change', @publish | ||
479 | routine: (el, value) -> | ||
480 | if el.type is 'radio' | ||
481 | el.checked = el.value?.toString() is value?.toString() | ||
482 | else | ||
483 | el.checked = !!value | ||
484 | |||
485 | unchecked: | ||
486 | publishes: true | ||
487 | bind: (el) -> | ||
488 | Rivets.Util.bindEvent el, 'change', @publish | ||
489 | unbind: (el) -> | ||
490 | Rivets.Util.unbindEvent el, 'change', @publish | ||
491 | routine: (el, value) -> | ||
492 | if el.type is 'radio' | ||
493 | el.checked = el.value?.toString() isnt value?.toString() | ||
494 | else | ||
495 | el.checked = !value | ||
496 | |||
497 | show: (el, value) -> | ||
498 | el.style.display = if value then '' else 'none' | ||
499 | |||
500 | hide: (el, value) -> | ||
501 | el.style.display = if value then 'none' else '' | ||
502 | |||
503 | html: (el, value) -> | ||
504 | el.innerHTML = if value? then value else '' | ||
505 | |||
506 | value: | ||
507 | publishes: true | ||
508 | bind: (el) -> | ||
509 | Rivets.Util.bindEvent el, 'change', @publish | ||
510 | unbind: (el) -> | ||
511 | Rivets.Util.unbindEvent el, 'change', @publish | ||
512 | routine: (el, value) -> | ||
513 | if window.jQuery? | ||
514 | el = jQuery el | ||
515 | |||
516 | if value?.toString() isnt el.val()?.toString() | ||
517 | el.val if value? then value else '' | ||
518 | else | ||
519 | if el.type is 'select-multiple' | ||
520 | o.selected = o.value in value for o in el if value? | ||
521 | else if value?.toString() isnt el.value?.toString() | ||
522 | el.value = if value? then value else '' | ||
523 | |||
524 | text: (el, value) -> | ||
525 | if el.innerText? | ||
526 | el.innerText = if value? then value else '' | ||
527 | else | ||
528 | el.textContent = if value? then value else '' | ||
529 | |||
530 | if: | ||
531 | block: true | ||
532 | |||
533 | bind: (el) -> | ||
534 | unless @marker? | ||
535 | attr = ['data', @view.config.prefix, @type].join('-').replace '--', '-' | ||
536 | declaration = el.getAttribute attr | ||
537 | |||
538 | @marker = document.createComment " rivets: #{@type} #{declaration} " | ||
539 | |||
540 | el.removeAttribute attr | ||
541 | el.parentNode.insertBefore @marker, el | ||
542 | el.parentNode.removeChild el | ||
543 | |||
544 | unbind: -> | ||
545 | @nested?.unbind() | ||
546 | |||
547 | routine: (el, value) -> | ||
548 | if !!value is not @nested? | ||
549 | if value | ||
550 | models = {} | ||
551 | models[key] = model for key, model of @view.models | ||
552 | |||
553 | options = | ||
554 | binders: @view.options.binders | ||
555 | formatters: @view.options.formatters | ||
556 | config: @view.options.config | ||
557 | |||
558 | (@nested = new Rivets.View(el, models, options)).bind() | ||
559 | @marker.parentNode.insertBefore el, @marker.nextSibling | ||
560 | else | ||
561 | el.parentNode.removeChild el | ||
562 | @nested.unbind() | ||
563 | delete @nested | ||
564 | |||
565 | update: (models) -> | ||
566 | @nested?.update models | ||
567 | |||
568 | unless: | ||
569 | block: true | ||
570 | |||
571 | bind: (el) -> | ||
572 | Rivets.binders.if.bind.call @, el | ||
573 | |||
574 | unbind: -> | ||
575 | Rivets.binders.if.unbind.call @ | ||
576 | |||
577 | routine: (el, value) -> | ||
578 | Rivets.binders.if.routine.call @, el, not value | ||
579 | |||
580 | update: (models) -> | ||
581 | Rivets.binders.if.update.call @, models | ||
582 | |||
583 | "on-*": | ||
584 | function: true | ||
585 | |||
586 | unbind: (el) -> | ||
587 | Rivets.Util.unbindEvent el, @args[0], @handler if @handler | ||
588 | |||
589 | routine: (el, value) -> | ||
590 | Rivets.Util.unbindEvent el, @args[0], @handler if @handler | ||
591 | Rivets.Util.bindEvent el, @args[0], @handler = @eventHandler value | ||
592 | |||
593 | "each-*": | ||
594 | block: true | ||
595 | |||
596 | bind: (el) -> | ||
597 | unless @marker? | ||
598 | attr = ['data', @view.config.prefix, @type].join('-').replace '--', '-' | ||
599 | @marker = document.createComment " rivets: #{@type} " | ||
600 | @iterated = [] | ||
601 | |||
602 | el.removeAttribute attr | ||
603 | el.parentNode.insertBefore @marker, el | ||
604 | el.parentNode.removeChild el | ||
605 | |||
606 | unbind: (el) -> | ||
607 | view.unbind() for view in @iterated if @iterated? | ||
608 | |||
609 | routine: (el, collection) -> | ||
610 | modelName = @args[0] | ||
611 | collection = collection or [] | ||
612 | |||
613 | if @iterated.length > collection.length | ||
614 | for i in Array @iterated.length - collection.length | ||
615 | view = @iterated.pop() | ||
616 | view.unbind() | ||
617 | @marker.parentNode.removeChild view.els[0] | ||
618 | |||
619 | for model, index in collection | ||
620 | data = {} | ||
621 | data[modelName] = model | ||
622 | |||
623 | if not @iterated[index]? | ||
624 | for key, model of @view.models | ||
625 | data[key] ?= model | ||
626 | |||
627 | previous = if @iterated.length | ||
628 | @iterated[@iterated.length - 1].els[0] | ||
629 | else | ||
630 | @marker | ||
631 | |||
632 | options = | ||
633 | binders: @view.options.binders | ||
634 | formatters: @view.options.formatters | ||
635 | config: {} | ||
636 | |||
637 | options.config[k] = v for k, v of @view.options.config | ||
638 | options.config.preloadData = true | ||
639 | |||
640 | template = el.cloneNode true | ||
641 | view = new Rivets.View(template, data, options) | ||
642 | view.bind() | ||
643 | @iterated.push view | ||
644 | |||
645 | @marker.parentNode.insertBefore template, previous.nextSibling | ||
646 | else if @iterated[index].models[modelName] isnt model | ||
647 | @iterated[index].update data | ||
648 | |||
649 | update: (models) -> | ||
650 | data = {} | ||
651 | |||
652 | for key, model of models | ||
653 | data[key] = model unless key is @args[0] | ||
654 | |||
655 | view.update data for view in @iterated | ||
656 | |||
657 | "class-*": (el, value) -> | ||
658 | elClass = " #{el.className} " | ||
659 | |||
660 | if !value is (elClass.indexOf(" #{@args[0]} ") isnt -1) | ||
661 | el.className = if value | ||
662 | "#{el.className} #{@args[0]}" | ||
663 | else | ||
664 | elClass.replace(" #{@args[0]} ", ' ').trim() | ||
665 | |||
666 | "*": (el, value) -> | ||
667 | if value | ||
668 | el.setAttribute @type, value | ||
669 | else | ||
670 | el.removeAttribute @type | ||
671 | |||
672 | # Rivets.components | ||
673 | # ----------------- | ||
674 | |||
675 | # Default components (there aren't any), publicly accessible on | ||
676 | # `module.components`. Can be overridden globally or local to a `Rivets.View` | ||
677 | # instance. | ||
678 | Rivets.components = {} | ||
679 | |||
680 | # Rivets.config | ||
681 | # ------------- | ||
682 | |||
683 | # Default configuration, publicly accessible on `module.config`. Can be | ||
684 | # overridden globally or local to a `Rivets.View` instance. | ||
685 | Rivets.config = | ||
686 | preloadData: true | ||
687 | handler: (context, ev, binding) -> | ||
688 | @call context, ev, binding.view.models | ||
689 | |||
690 | # Rivets.formatters | ||
691 | # ----------------- | ||
692 | |||
693 | # Default formatters (there aren't any), publicly accessible on | ||
694 | # `module.formatters`. Can be overridden globally or local to a `Rivets.View` | ||
695 | # instance. | ||
696 | Rivets.formatters = {} | ||
697 | |||
698 | # Rivets.factory | ||
699 | # -------------- | ||
700 | |||
701 | # The Rivets.js module factory. | ||
702 | Rivets.factory = (exports) -> | ||
703 | # Exposes the full Rivets namespace. This is mainly used for isolated testing. | ||
704 | exports._ = Rivets | ||
705 | |||
706 | # Exposes the core binding routines that can be extended or stripped down. | ||
707 | exports.binders = Rivets.binders | ||
708 | |||
709 | # Exposes the components object to be extended. | ||
710 | exports.components = Rivets.components | ||
711 | |||
712 | # Exposes the formatters object to be extended. | ||
713 | exports.formatters = Rivets.formatters | ||
714 | |||
715 | # Exposes the rivets configuration options. These can be set manually or from | ||
716 | # rivets.configure with an object literal. | ||
717 | exports.config = Rivets.config | ||
718 | |||
719 | # Sets configuration options by merging an object literal. | ||
720 | exports.configure = (options={}) -> | ||
721 | for property, value of options | ||
722 | Rivets.config[property] = value | ||
723 | return | ||
724 | |||
725 | # Binds a set of model objects to a parent DOM element and returns a | ||
726 | # `Rivets.View` instance. | ||
727 | exports.bind = (el, models = {}, options = {}) -> | ||
728 | view = new Rivets.View(el, models, options) | ||
729 | view.bind() | ||
730 | view | ||
731 | |||
732 | # Export | ||
733 | # ------ | ||
734 | |||
735 | # Exports Rivets.js for CommonJS, AMD and the browser. | ||
736 | if typeof exports == 'object' | ||
737 | Rivets.factory(exports) | ||
738 | else if typeof define == 'function' && define.amd | ||
739 | define ['exports'], (exports) -> | ||
740 | Rivets.factory(@rivets = exports) | ||
741 | return exports | ||
742 | else | ||
743 | Rivets.factory(@rivets = {}) | ... | ... |
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