-
Notifications
You must be signed in to change notification settings - Fork 303
Expand file tree
/
Copy pathbindings.coffee
More file actions
294 lines (230 loc) · 9.44 KB
/
bindings.coffee
File metadata and controls
294 lines (230 loc) · 9.44 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
# Rivets.Binding
# --------------
# A single binding between a model attribute and a DOM element.
class Rivets.Binding
# All information about the binding is passed into the constructor; the
# containing view, the DOM node, the type of binding, the model object and the
# keypath at which to listen for changes.
constructor: (@view, @el, @type, @keypath, @options = {}) ->
@formatters = @options.formatters or []
@dependencies = []
@formatterObservers = {}
@model = undefined
@setBinder()
# Sets the binder to use when binding and syncing.
setBinder: =>
unless @binder = @view.binders[@type]
for identifier, value of @view.binders
if identifier isnt '*' and identifier.indexOf('*') isnt -1
regexp = new RegExp "^#{identifier.replace(/\*/g, '.+')}$"
if regexp.test @type
@binder = value
@args = new RegExp("^#{identifier.replace(/\*/g, '(.+)')}$").exec @type
@args.shift()
@binder or= @view.binders['*']
@binder = {routine: @binder} if @binder instanceof Function
observe: (obj, keypath, callback) =>
Rivets.sightglass obj, keypath, callback,
root: @view.rootInterface
adapters: @view.adapters
parseTarget: =>
token = Rivets.TypeParser.parse @keypath
if token.type is Rivets.TypeParser.types.primitive
@value = token.value
else
@observer = @observe @view.models, @keypath, @sync
@model = @observer.target
parseFormatterArguments: (args, formatterIndex) =>
args = (Rivets.TypeParser.parse(arg) for arg in args)
processedArgs = []
for arg, ai in args
processedArgs.push if arg.type is Rivets.TypeParser.types.primitive
arg.value
else
@formatterObservers[formatterIndex] or= {}
unless observer = @formatterObservers[formatterIndex][ai]
observer = @observe @view.models, arg.value, @sync
@formatterObservers[formatterIndex][ai] = observer
observer.value()
processedArgs
# Applies all the current formatters to the supplied value and returns the
# formatted value.
formattedValue: (value) =>
for formatter, fi in @formatters
args = formatter.match /[^\s']+|'([^']|'[^\s])*'|"([^"]|"[^\s])*"/g
id = args.shift()
formatter = @view.formatters[id]
processedArgs = @parseFormatterArguments args, fi
if formatter?.read instanceof Function
value = formatter.read.call @model, value, processedArgs...
else if formatter instanceof Function
value = formatter.call @model, value, processedArgs...
value
# Returns an event handler for the binding around the supplied function.
eventHandler: (fn) =>
handler = (binding = @).view.handler
(ev) -> handler.call fn, @, ev, binding
# Sets the value for the binding. This Basically just runs the binding routine
# with the suplied value formatted.
set: (value) =>
# Since 0.9 : doesn't execute function unless backward compatibility is active
value = if (value instanceof Function and !@binder.function and Rivets.public.executeFunctions)
@formattedValue value.call @model
else
@formattedValue value
@binder.routine?.call @, @el, value
# Syncs up the view binding with the model.
sync: =>
@set if @observer
if @model isnt @observer.target
observer.unobserve() for observer in @dependencies
@dependencies = []
if (@model = @observer.target)? and @options.dependencies?.length
for dependency in @options.dependencies
observer = @observe @model, dependency, @sync
@dependencies.push observer
@observer.value()
else
@value
# Publishes the value currently set on the input element back to the model.
publish: =>
if @observer
value = @getValue @el
lastformatterIndex = @formatters.length - 1
for formatter, fiReversed in @formatters.slice(0).reverse()
fi = lastformatterIndex - fiReversed
args = formatter.split /\s+/
id = args.shift()
processedArgs = @parseFormatterArguments args, fi
if @view.formatters[id]?.publish
value = @view.formatters[id].publish value, processedArgs...
@observer.setValue value
# Subscribes to the model for changes at the specified keypath. Bi-directional
# routines will also listen for changes on the element to propagate them back
# to the model.
bind: =>
@parseTarget()
@binder.bind?.call @, @el
if @model? and @options.dependencies?.length
for dependency in @options.dependencies
observer = @observe @model, dependency, @sync
@dependencies.push observer
@sync() if @view.preloadData
# Unsubscribes from the model and the element.
unbind: =>
@binder.unbind?.call @, @el
@observer?.unobserve()
observer.unobserve() for observer in @dependencies
@dependencies = []
for fi, args of @formatterObservers
observer.unobserve() for ai, observer of args
@formatterObservers = {}
# Updates the binding's model from what is currently set on the view. Unbinds
# the old model first and then re-binds with the new model.
update: (models = {}) =>
@model = @observer?.target
@binder.update?.call @, models
# Returns elements value
getValue: (el) =>
if @binder and @binder.getValue?
@binder.getValue.call @, el
else
Rivets.Util.getInputValue el
# Rivets.ComponentBinding
# -----------------------
# A component view encapsulated as a binding within it's parent view.
class Rivets.ComponentBinding extends Rivets.Binding
# Initializes a component binding for the specified view. The raw component
# element is passed in along with the component type. Attributes and scope
# inflections are determined based on the components defined attributes.
constructor: (@view, @el, @type) ->
@component = @view.components[@type]
@static = {}
@observers = {}
@upstreamObservers = {}
bindingRegExp = view.bindingRegExp()
for attribute in @el.attributes or []
unless bindingRegExp.test attribute.name
propertyName = @camelCase attribute.name
token = Rivets.TypeParser.parse(attribute.value)
if propertyName in (@component.static ? [])
@static[propertyName] = attribute.value
else if token.type is Rivets.TypeParser.types.primitive
@static[propertyName] = token.value
else
@observers[propertyName] = attribute.value
# Intercepts `Rivets.Binding::sync` since component bindings are not bound to
# a particular model to update it's value.
sync: ->
# Intercepts `Rivets.Binding::update` since component bindings are not bound
# to a particular model to update it's value.
update: ->
# Intercepts `Rivets.Binding::publish` since component bindings are not bound
# to a particular model to update it's value.
publish: ->
# Returns an object map using the component's scope inflections.
locals: =>
result = {}
for key, value of @static
result[key] = value
for key, observer of @observers
result[key] = observer.value()
result
# Returns a camel-cased version of the string. Used when translating an
# element's attribute name into a property name for the component's scope.
camelCase: (string) ->
string.replace /-([a-z])/g, (grouped) ->
grouped[1].toUpperCase()
# Intercepts `Rivets.Binding::bind` to build `@componentView` with a localized
# map of models from the root view. Bind `@componentView` on subsequent calls.
bind: =>
unless @bound
for key, keypath of @observers
@observers[key] = @observe @view.models, keypath, ((key) => =>
@componentView.models[key] = @observers[key].value()
).call(@, key)
@bound = true
if @componentView?
@componentView.bind()
else
@el.innerHTML = @component.template.call this
scope = @component.initialize.call @, @el, @locals()
@el._bound = true
options = {}
for option in Rivets.extensions
options[option] = {}
options[option][k] = v for k, v of @component[option] if @component[option]
options[option][k] ?= v for k, v of @view[option]
for option in Rivets.options
options[option] = @component[option] ? @view[option]
@componentView = new Rivets.View(Array.prototype.slice.call(@el.childNodes), scope, options)
@componentView.bind()
for key, observer of @observers
@upstreamObservers[key] = @observe @componentView.models, key, ((key, observer) => =>
observer.setValue @componentView.models[key]
).call(@, key, observer)
return
# Intercept `Rivets.Binding::unbind` to be called on `@componentView`.
unbind: =>
for key, observer of @upstreamObservers
observer.unobserve()
for key, observer of @observers
observer.unobserve()
@componentView?.unbind.call @
# Rivets.TextBinding
# -----------------------
# A text node binding, defined internally to deal with text and element node
# differences while avoiding it being overwritten.
class Rivets.TextBinding extends Rivets.Binding
# Initializes a text binding for the specified view and text node.
constructor: (@view, @el, @type, @keypath, @options = {}) ->
@formatters = @options.formatters or []
@dependencies = []
@formatterObservers = {}
# A standard routine binder used for text node bindings.
binder:
routine: (node, value) ->
node.data = value ? ''
# Wrap the call to `sync` in fat-arrow to avoid function context issues.
sync: =>
super