Implement a more flexible custom binder/routine interface such that all special …
…binding logic can be self-contained within their own binder. Binders/routines can now be defined as an object where additional properties can be defined (setup and teardown callbacks that get called on bind() and unbind(), traversal blocking (for iteration-type bindings), whether the binding published or not (so that view.publish() will catch your custom binding), etc.).
Showing
1 changed file
with
121 additions
and
113 deletions
... | @@ -15,19 +15,21 @@ class Rivets.Binding | ... | @@ -15,19 +15,21 @@ class Rivets.Binding |
15 | # element, the type of binding, the model object and the keypath at which | 15 | # element, the type of binding, the model object and the keypath at which |
16 | # to listen for changes. | 16 | # to listen for changes. |
17 | constructor: (@el, @type, @model, @keypath, @options = {}) -> | 17 | constructor: (@el, @type, @model, @keypath, @options = {}) -> |
18 | @routine = switch @options.special | 18 | unless @binder = Rivets.routines[type] |
19 | when 'event' then eventBinding @type | 19 | for identifier, value of Rivets.routines |
20 | when 'class' then classBinding @type | 20 | if identifier isnt '*' and identifier.indexOf('*') isnt -1 |
21 | when 'iteration' then iterationBinding @type | 21 | regexp = new RegExp "^#{identifier.replace('*', '.+')}$" |
22 | else Rivets.routines[@type] || attributeBinding @type | 22 | if regexp.test type |
23 | @binder = value | ||
24 | @args = new RegExp("^#{identifier.replace('*', '(.+)')}$").exec type | ||
25 | @args.shift() | ||
23 | 26 | ||
24 | @formatters = @options.formatters || [] | 27 | @binder or= Rivets.routines['*'] |
28 | |||
29 | if @binder instanceof Function | ||
30 | @binder = {routine: @binder} | ||
25 | 31 | ||
26 | # Returns true|false depending on whether or not the binding should also | 32 | @formatters = @options.formatters || [] |
27 | # observe the DOM element for changes in order to propagate those changes | ||
28 | # back to the model object. | ||
29 | isBidirectional: => | ||
30 | @type in ['value', 'checked', 'unchecked'] | ||
31 | 33 | ||
32 | # Applies all the current formatters to the supplied value and returns the | 34 | # Applies all the current formatters to the supplied value and returns the |
33 | # formatted value. | 35 | # formatted value. |
... | @@ -45,17 +47,12 @@ class Rivets.Binding | ... | @@ -45,17 +47,12 @@ class Rivets.Binding |
45 | # Sets the value for the binding. This Basically just runs the binding routine | 47 | # Sets the value for the binding. This Basically just runs the binding routine |
46 | # with the suplied value formatted. | 48 | # with the suplied value formatted. |
47 | set: (value) => | 49 | set: (value) => |
48 | value = if value instanceof Function and @options.special isnt 'event' | 50 | value = if value instanceof Function and !@binder.function |
49 | @formattedValue value.call @model | 51 | @formattedValue value.call @model |
50 | else | 52 | else |
51 | @formattedValue value | 53 | @formattedValue value |
52 | 54 | ||
53 | if @options.special is 'event' | 55 | @binder.routine?.call @, @el, value |
54 | @currentListener = @routine @el, @model, value, @currentListener | ||
55 | else if @options.special is 'iteration' | ||
56 | @routine @el, value, @ | ||
57 | else | ||
58 | @routine @el, value | ||
59 | 56 | ||
60 | # Syncs up the view binding with the model. | 57 | # Syncs up the view binding with the model. |
61 | sync: => | 58 | sync: => |
... | @@ -75,12 +72,10 @@ class Rivets.Binding | ... | @@ -75,12 +72,10 @@ class Rivets.Binding |
75 | if @options.bypass | 72 | if @options.bypass |
76 | @sync() | 73 | @sync() |
77 | else | 74 | else |
75 | @binder.bind?.call @, @el | ||
78 | Rivets.config.adapter.subscribe @model, @keypath, @sync | 76 | Rivets.config.adapter.subscribe @model, @keypath, @sync |
79 | @sync() if Rivets.config.preloadData | 77 | @sync() if Rivets.config.preloadData |
80 | 78 | ||
81 | if @isBidirectional() | ||
82 | bindEvent @el, 'change', @publish | ||
83 | |||
84 | if @options.dependencies?.length | 79 | if @options.dependencies?.length |
85 | for dependency in @options.dependencies | 80 | for dependency in @options.dependencies |
86 | if /^\./.test dependency | 81 | if /^\./.test dependency |
... | @@ -93,15 +88,12 @@ class Rivets.Binding | ... | @@ -93,15 +88,12 @@ class Rivets.Binding |
93 | 88 | ||
94 | Rivets.config.adapter.subscribe model, keypath, @sync | 89 | Rivets.config.adapter.subscribe model, keypath, @sync |
95 | 90 | ||
96 | |||
97 | # Unsubscribes from the model and the element. | 91 | # Unsubscribes from the model and the element. |
98 | unbind: => | 92 | unbind: => |
99 | unless @options.bypass | 93 | unless @options.bypass |
94 | @binder.unbind?.call @, @el | ||
100 | Rivets.config.adapter.unsubscribe @model, @keypath, @sync | 95 | Rivets.config.adapter.unsubscribe @model, @keypath, @sync |
101 | 96 | ||
102 | if @isBidirectional() | ||
103 | unbindEvent @el 'change', @publish | ||
104 | |||
105 | if @options.dependencies?.length | 97 | if @options.dependencies?.length |
106 | for keypath in @options.dependencies | 98 | for keypath in @options.dependencies |
107 | Rivets.config.adapter.unsubscribe @model, keypath, @sync | 99 | Rivets.config.adapter.unsubscribe @model, keypath, @sync |
... | @@ -119,28 +111,30 @@ class Rivets.View | ... | @@ -119,28 +111,30 @@ class Rivets.View |
119 | prefix = Rivets.config.prefix | 111 | prefix = Rivets.config.prefix |
120 | if prefix then new RegExp("^data-#{prefix}-") else /^data-/ | 112 | if prefix then new RegExp("^data-#{prefix}-") else /^data-/ |
121 | 113 | ||
122 | # Builds the Rivets.Binding instances for the view. | ||
123 | build: => | 114 | build: => |
124 | @bindings = [] | 115 | @bindings = [] |
125 | skipNodes = [] | 116 | skipNodes = [] |
126 | iterator = null | ||
127 | bindingRegExp = @bindingRegExp() | 117 | bindingRegExp = @bindingRegExp() |
128 | eventRegExp = /^on-/ | ||
129 | classRegExp = /^class-/ | ||
130 | iterationRegExp = /^each-/ | ||
131 | 118 | ||
132 | parseNode = (node) => | 119 | parseNode = (node) => |
133 | unless node in skipNodes | 120 | unless node in skipNodes |
134 | for attribute in node.attributes | 121 | for attribute in node.attributes |
135 | if bindingRegExp.test attribute.name | 122 | if bindingRegExp.test attribute.name |
136 | type = attribute.name.replace bindingRegExp, '' | 123 | type = attribute.name.replace bindingRegExp, '' |
124 | unless binder = Rivets.routines[type] | ||
125 | for identifier, value of Rivets.routines | ||
126 | if identifier isnt '*' and identifier.indexOf('*') isnt -1 | ||
127 | regexp = new RegExp "^#{identifier.replace('*', '.+')}$" | ||
128 | if regexp.test type | ||
129 | binder = value | ||
137 | 130 | ||
138 | if iterationRegExp.test type | 131 | binder or= Rivets.routines['*'] |
139 | unless @models[type.replace iterationRegExp, ''] | ||
140 | skipNodes.push n for n in node.getElementsByTagName '*' | ||
141 | iterator = [attribute] | ||
142 | 132 | ||
143 | for attribute in iterator or node.attributes | 133 | if binder.block |
134 | skipNodes.push n for n in node.getElementsByTagName '*' | ||
135 | attributes = [attribute] | ||
136 | |||
137 | for attribute in attributes or node.attributes | ||
144 | if bindingRegExp.test attribute.name | 138 | if bindingRegExp.test attribute.name |
145 | options = {} | 139 | options = {} |
146 | 140 | ||
... | @@ -162,31 +156,19 @@ class Rivets.View | ... | @@ -162,31 +156,19 @@ class Rivets.View |
162 | if dependencies = context.shift() | 156 | if dependencies = context.shift() |
163 | options.dependencies = dependencies.split /\s+/ | 157 | options.dependencies = dependencies.split /\s+/ |
164 | 158 | ||
165 | if eventRegExp.test type | ||
166 | type = type.replace eventRegExp, '' | ||
167 | options.special = 'event' | ||
168 | |||
169 | if classRegExp.test type | ||
170 | type = type.replace classRegExp, '' | ||
171 | options.special = 'class' | ||
172 | |||
173 | if iterationRegExp.test type | ||
174 | type = type.replace iterationRegExp, '' | ||
175 | options.special = 'iteration' | ||
176 | |||
177 | binding = new Rivets.Binding node, type, model, keypath, options | 159 | binding = new Rivets.Binding node, type, model, keypath, options |
178 | binding.view = @ | 160 | binding.view = @ |
179 | 161 | ||
180 | @bindings.push binding | 162 | @bindings.push binding |
181 | 163 | ||
182 | if iterator | 164 | attributes = null if attributes |
183 | node.removeAttribute(a.name) for a in iterator | 165 | |
184 | iterator = null | ||
185 | return | 166 | return |
186 | 167 | ||
187 | for el in @els | 168 | for el in @els |
188 | parseNode el | 169 | parseNode el |
189 | parseNode node for node in el.getElementsByTagName '*' | 170 | parseNode node for node in el.getElementsByTagName '*' |
171 | |||
190 | return | 172 | return |
191 | 173 | ||
192 | # Returns an array of bindings where the supplied function evaluates to true. | 174 | # Returns an array of bindings where the supplied function evaluates to true. |
... | @@ -207,7 +189,7 @@ class Rivets.View | ... | @@ -207,7 +189,7 @@ class Rivets.View |
207 | 189 | ||
208 | # Publishes the input values from the view back to the model (reverse sync). | 190 | # Publishes the input values from the view back to the model (reverse sync). |
209 | publish: => | 191 | publish: => |
210 | binding.publish() for binding in @select (b) -> b.isBidirectional() | 192 | binding.publish() for binding in @select (b) -> b.binder.publishes |
211 | 193 | ||
212 | # Cross-browser event binding. | 194 | # Cross-browser event binding. |
213 | bindEvent = (el, event, handler, context) -> | 195 | bindEvent = (el, event, handler, context) -> |
... | @@ -248,85 +230,111 @@ getInputValue = (el) -> | ... | @@ -248,85 +230,111 @@ getInputValue = (el) -> |
248 | when 'select-multiple' then o.value for o in el when o.selected | 230 | when 'select-multiple' then o.value for o in el when o.selected |
249 | else el.value | 231 | else el.value |
250 | 232 | ||
251 | # Returns an event binding routine for the specified event. | ||
252 | eventBinding = (event) -> (el, context, bind, unbind) -> | ||
253 | unbindEvent el, event, unbind if unbind | ||
254 | bindEvent el, event, bind, context | ||
255 | |||
256 | # Returns a class binding routine for the specified class name. | ||
257 | classBinding = (name) -> (el, value) -> | ||
258 | elClass = " #{el.className} " | ||
259 | hasClass = elClass.indexOf(" #{name} ") != -1 | ||
260 | |||
261 | if !value is hasClass | ||
262 | el.className = if value | ||
263 | "#{el.className} #{name}" | ||
264 | else | ||
265 | elClass.replace(" #{name} ", ' ').trim() | ||
266 | |||
267 | # Returns an iteration binding routine for the specified collection. | ||
268 | iterationBinding = (name) -> (el, collection, binding) -> | ||
269 | if binding.iterated? | ||
270 | for iteration in binding.iterated | ||
271 | iteration.view.unbind() | ||
272 | iteration.el.parentNode.removeChild iteration.el | ||
273 | else | ||
274 | binding.marker = document.createComment " rivets: each-#{name} " | ||
275 | el.parentNode.insertBefore binding.marker, el | ||
276 | el.parentNode.removeChild el | ||
277 | |||
278 | binding.iterated = [] | ||
279 | |||
280 | for item in collection | ||
281 | data = {} | ||
282 | data[n] = m for n, m of binding.view.models | ||
283 | data[name] = item | ||
284 | itemEl = el.cloneNode true | ||
285 | previous = binding.iterated[binding.iterated.length - 1] or binding.marker | ||
286 | binding.marker.parentNode.insertBefore itemEl, previous.nextSibling ? null | ||
287 | |||
288 | binding.iterated.push | ||
289 | el: itemEl | ||
290 | view: rivets.bind itemEl, data | ||
291 | |||
292 | # Returns an attribute binding routine for the specified attribute. This is what | ||
293 | # is used when there are no matching routines for an identifier. | ||
294 | attributeBinding = (attr) -> (el, value) -> | ||
295 | if value then el.setAttribute attr, value else el.removeAttribute attr | ||
296 | |||
297 | # Core binding routines. | 233 | # Core binding routines. |
298 | Rivets.routines = | 234 | Rivets.routines = |
299 | enabled: (el, value) -> | 235 | enabled: (el, value) -> |
300 | el.disabled = !value | 236 | el.disabled = !value |
237 | |||
301 | disabled: (el, value) -> | 238 | disabled: (el, value) -> |
302 | el.disabled = !!value | 239 | el.disabled = !!value |
303 | checked: (el, value) -> | 240 | |
304 | if el.type is 'radio' | 241 | checked: |
305 | el.checked = el.value is value | 242 | publishes: true |
306 | else | 243 | bind: (el) -> |
307 | el.checked = !!value | 244 | bindEvent el, 'change', @publish |
308 | unchecked: (el, value) -> | 245 | unbind: (el) -> |
309 | if el.type is 'radio' | 246 | unbindEvent el, 'change', @publish |
310 | el.checked = el.value isnt value | 247 | routine: (el, value) -> |
311 | else | 248 | if el.type is 'radio' |
312 | el.checked = !value | 249 | el.checked = el.value is value |
250 | else | ||
251 | el.checked = !!value | ||
252 | |||
253 | unchecked: | ||
254 | publishes: true | ||
255 | bind: (el) -> | ||
256 | bindEvent el, 'change', @publish | ||
257 | unbind: (el) -> | ||
258 | unbindEvent el, 'change', @publish | ||
259 | routine: (el, value) -> | ||
260 | if el.type is 'radio' | ||
261 | el.checked = el.value isnt value | ||
262 | else | ||
263 | el.checked = !value | ||
264 | |||
313 | show: (el, value) -> | 265 | show: (el, value) -> |
314 | el.style.display = if value then '' else 'none' | 266 | el.style.display = if value then '' else 'none' |
267 | |||
315 | hide: (el, value) -> | 268 | hide: (el, value) -> |
316 | el.style.display = if value then 'none' else '' | 269 | el.style.display = if value then 'none' else '' |
317 | html: (el, value) -> | 270 | |
271 | html: (el, value) -> | ||
318 | el.innerHTML = if value? then value else '' | 272 | el.innerHTML = if value? then value else '' |
319 | value: (el, value) -> | 273 | |
320 | if el.type is 'select-multiple' | 274 | value: |
321 | o.selected = o.value in value for o in el if value? | 275 | publishes: true |
322 | else | 276 | bind: (el) -> |
323 | el.value = if value? then value else '' | 277 | bindEvent el, 'change', @publish |
278 | unbind: (el) -> | ||
279 | unbindEvent el, 'change', @publish | ||
280 | routine: (el, value) -> | ||
281 | if el.type is 'select-multiple' | ||
282 | o.selected = o.value in value for o in el if value? | ||
283 | else | ||
284 | el.value = if value? then value else '' | ||
285 | |||
324 | text: (el, value) -> | 286 | text: (el, value) -> |
325 | if el.innerText? | 287 | if el.innerText? |
326 | el.innerText = if value? then value else '' | 288 | el.innerText = if value? then value else '' |
327 | else | 289 | else |
328 | el.textContent = if value? then value else '' | 290 | el.textContent = if value? then value else '' |
329 | 291 | ||
292 | "on-*": | ||
293 | function: true | ||
294 | routine: (el, value) -> | ||
295 | unbindEvent el, @args[0], @currentListener if @currentListener | ||
296 | @currentListener = bindEvent el, @args[0], value, @model | ||
297 | |||
298 | "each-*": | ||
299 | block: true | ||
300 | bind: (el, collection) -> | ||
301 | el.removeAttribute ['data', rivets.config.prefix, @type].join('-').replace '--', '-' | ||
302 | routine: (el, collection) -> | ||
303 | if @iterated? | ||
304 | for view in @iterated | ||
305 | view.unbind() | ||
306 | e.parentNode.removeChild e for e in view.els | ||
307 | else | ||
308 | @marker = document.createComment " rivets: #{@type} " | ||
309 | el.parentNode.insertBefore @marker, el | ||
310 | el.parentNode.removeChild el | ||
311 | |||
312 | @iterated = [] | ||
313 | |||
314 | for item in collection | ||
315 | data = {} | ||
316 | data[n] = m for n, m of @view.models | ||
317 | data[@args[0]] = item | ||
318 | itemEl = el.cloneNode true | ||
319 | previous = @iterated[@iterated.length - 1] or @marker | ||
320 | @marker.parentNode.insertBefore itemEl, previous.nextSibling ? null | ||
321 | @iterated.push rivets.bind itemEl, data | ||
322 | |||
323 | "class-*": (el, value) -> | ||
324 | elClass = " #{el.className} " | ||
325 | |||
326 | if !value is (elClass.indexOf(" #{@args[0]} ") isnt -1) | ||
327 | el.className = if value | ||
328 | "#{el.className} #{@args[0]}" | ||
329 | else | ||
330 | elClass.replace(" #{@args[0]} ", ' ').trim() | ||
331 | |||
332 | "*": (el, value) -> | ||
333 | if value | ||
334 | el.setAttribute @type, value | ||
335 | else | ||
336 | el.removeAttribute @type | ||
337 | |||
330 | # Default configuration. | 338 | # Default configuration. |
331 | Rivets.config = | 339 | Rivets.config = |
332 | preloadData: true | 340 | preloadData: true | ... | ... |
-
Please register or sign in to post a comment