bindings.coffee
7 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
# 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 || []
@setBinders()
@setModel()
setBinders: =>
unless @binder = @view.binders[@type]
for identifier, value of @view.binders
if identifier isnt '*' and identifier.indexOf('*') isnt -1
regexp = new RegExp "^#{identifier.replace('*', '.+')}$"
if regexp.test @type
@binder = value
@args = new RegExp("^#{identifier.replace('*', '(.+)')}$").exec @type
@args.shift()
@binder or= @view.binders['*']
@binder = {routine: @binder} if @binder instanceof Function
setModel: =>
interfaces = (k for k, v of @view.adapters)
tokens = Rivets.KeypathParser.parse @keypath, interfaces, '.'
@model = @view.models
@rootKey = tokens[0]
@key = tokens.pop()
for token, index in tokens
@model = @view.adapters[token.interface].read(@model, token.path)
# Applies all the current formatters to the supplied value and returns the
# formatted value.
formattedValue: (value) =>
for formatter in @formatters
args = formatter.split /\s+/
id = args.shift()
formatter = if @model[id] instanceof Function
@model[id]
else
@view.formatters[id]
if formatter?.read instanceof Function
value = formatter.read value, args...
else if formatter instanceof Function
value = formatter value, args...
value
# Returns an event handler for the binding around the supplied function.
eventHandler: (fn) =>
handler = (binding = @).view.config.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) =>
value = if value instanceof Function and !@binder.function
@formattedValue value.call @model
else
@formattedValue value
@binder.routine?.call @, @el, value
# Syncs up the view binding with the model.
sync: =>
@set @view.adapters[@key.interface].read @model, @key.path
# Publishes the value currently set on the input element back to the model.
publish: =>
value = Rivets.Util.getInputValue @el
for formatter in @formatters.slice(0).reverse()
args = formatter.split /\s+/
id = args.shift()
if @view.formatters[id]?.publish
value = @view.formatters[id].publish value, args...
@view.adapters[@key.interface].publish @model, @key.path, 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: =>
@binder.bind?.call @, @el
@view.adapters[@key.interface].subscribe @model, @key.path, @sync
@sync() if @view.config.preloadData
if @options.dependencies?.length
for dependency in @options.dependencies
if /^\./.test dependency
model = @model
keypath = dependency.substr 1
else
dependency = dependency.split '.'
model = @view.models[dependency.shift()]
keypath = dependency.join '.'
@view.adapters['.'].subscribe model, keypath, @sync
# Unsubscribes from the model and the element.
unbind: =>
@binder.unbind?.call @, @el
@view.adapters[@key.interface].unsubscribe @model, @key.path, @sync
if @options.dependencies?.length
for dependency in @options.dependencies
if /^\./.test dependency
model = @model
keypath = dependency.substr 1
else
dependency = dependency.split '.'
model = @view.models[dependency.shift()]
keypath = dependency.join '.'
@view.adapters['.'].unsubscribe model, keypath, @sync
# 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 = {}) =>
if models[@rootKey.path]
@view.adapters[@key.interface].unsubscribe @model, @key.path, @sync
@setModel()
@view.adapters[@key.interface].subscribe @model, @key.path, @sync
@sync()
@binder.update?.call @, models
# 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 = Rivets.components[@type]
@attributes = {}
@inflections = {}
for attribute in @el.attributes or []
if attribute.name in @component.attributes
@attributes[attribute.name] = attribute.value
else
@inflections[attribute.name] = attribute.value
# Intercepts `Rivets.Binding::sync` since component bindings are not bound to
# a particular model to update it's value.
sync: ->
# Returns an object map using the component's scope inflections.
locals: (models = @view.models) =>
result = {}
for key, inverse of @inflections
result[key] = (result[key] or models)[path] for path in inverse.split '.'
result[key] ?= model for key, model of models
result
# Intercepts `Rivets.Binding::update` to be called on `@componentView` with a
# localized map of the models.
update: (models) =>
@componentView?.update @locals models
# Intercepts `Rivets.Binding::bind` to build `@componentView` with a localized
# map of models from the root view. Bind `@componentView` on subsequent calls.
bind: =>
if @componentView?
@componentView?.bind()
else
el = @component.build.call @attributes
(@componentView = new Rivets.View(el, @locals(), @view.options)).bind()
@el.parentNode.replaceChild el, @el
# Intercept `Rivets.Binding::unbind` to be called on `@componentView`.
unbind: =>
@componentView?.unbind()
# 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 = {}) ->
interfaces = (k for k, v of @view.adapters)
tokens = Rivets.KeypathParser.parse(@keypath, interfaces, '.')
@formatters = @options.formatters || []
@setModel()
# 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