185fb885 by Michael Richards

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.).
1 parent 3a018796
...@@ -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
......