-
Notifications
You must be signed in to change notification settings - Fork 1
/
Copy pathbindings.coffee
452 lines (368 loc) · 14.7 KB
/
bindings.coffee
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
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
define (require)->
Cuffs = require './ns'
{Binding, Template, Attribute,
optionize, substitute, getVars,} = require './template'
utils = require './utils'
# Looks up objects on the global classpath, eg:
# lookup('window.document.body')
lookup = (classpath)->
parts = classpath.split '.'
current = this
for part in parts
current = current[part]
if not current?
throw new Error "Not found: #{classpath}"
current
class DataController extends Binding
# Initialise the controller associated with the current node
@__id__: 0
@id: -> ++ DataController.__id__
@bind 'data-controller'
stop: true # Stop recursive descent
constructor: (args...)->
super args...
@Class = lookup @attr
applyContext: (parent)->
app = parent.root().$app
context = parent.new()
@instance = new @Class context: context, app: app
app.addController @Class, @instance
template = new Template @node
template.applyContext context
class DataShow extends Binding
# Make element visible depending if the context's attribute
# is not falsy, eg:
# <div data-show="todo.done"></div>
# <div data-show="todo.done==0"></div>
# <div data-show="todo.done!=0"></div>
#
# Handles simple type coercion such as `true` and `false`:
#
# <div data-show="todo.done==true"></div>
@bind 'data-show'
constructor: (@node)->
super node
notEqual = /.*!=.*/
equal = /.*==.*/
lesser = /.*<.*/
greater = /.*>.*/
booleanWrapper = (attrValue)=>
if attrValue == "false"
return false
if attrValue == "true"
return true
return attrValue
if notEqual.test @attr
[@attrName, @attrValue] = @attr.split("!=")
@test = (value)=>
# We don't want strict comparison - coercion is
# fine - hence the backticks.
return `booleanWrapper(this.attrValue) != value`
else if equal.test @attr
[@attrName, @attrValue] = @attr.split("==")
@test = (value)=>
# We don't want strict comparison - coercion is
# fine - hence the backticks.
return `booleanWrapper(this.attrValue) == value`
else if lesser.test @attr
[@attrName, @attrValue] = @attr.split("<")
@test = (value)=>
return `value < booleanWrapper(this.attrValue)`
else if greater.test @attr
[@attrName, @attrValue] = @attr.split(">")
@test = (value)=>
return `value > booleanWrapper(this.attrValue)`
else
@attrName = @attr
@test = (value)->
return value? and value
toggle: (value)->
if @test value
$(@node).show()
else
$(@node).hide()
applyContext: (context)->
context.watch @attrName, (value)=>
@toggle value
@toggle context.get @attrName
this
class DataAttr extends Binding
# Sets an attribute on the current node, eg:
# <img src="foo.png" data-attr="alt=img.desc" />
@bind 'data-attr'
constructor: (node)->
super node
@values = optionize @attr
setAttr: (name, value)=>
$(@node).attr name, value
applyContext: (context)->
for own key, val of @values
@setAttr key, substitute val, context
for pair in getVars(val)
context.watch pair.name, =>
@setAttr key, substitute val, context
class DataOr extends Binding
# Picks one of two choices to set as the content of
# a node, eg:
# <span data-or="todo.done ? Todo is done : Todo is not done"></span>
@bind 'data-or'
constructor: (node)->
super node
[@predicate, values] = @attr.split('?')
[@true, @false] = values.split(':')
@predicate = @predicate.trim()
setValue: (value)->
if value
@node.innerHTML = @true.trim()
else
@node.innerHTML = @false.trim()
applyContext: (context)->
context.watch @predicate, (value)=> @setValue value
@setValue context.get @predicate
class DataSubmit extends Binding
# Calls a function on the context when a form is submitted,
# eg:
#
# <form data-submit="onSubmit"></form>
@bind 'data-submit'
applyContext: (context)->
[fnName, args] = @attr.split(':')
argNames = args?.split(',') or []
$(@node).bind 'submit', (e)=>
fn = context.get fnName, false
fn.apply @node, [e].concat (context.get(argName) for argName in argNames)
class DataBind extends Binding
# Create two way binding of data between elements and the
# model. It's reasonably smart, so it handles normal content
# and different input types, eg:
#
# <input type="text" data-bind="todo.name" value="" />
# <input type="checkbox" data-bind="todo.done" />
# <span data-bind="todo.name"></span>
@bind 'data-bind'
textTypes: ['text','hidden', 'email']
constructor: (@node)->
@type = @node.type
super @node
setValue: (value)->
if @node.tagName == "TEXTAREA"
$(@node).val(value)
else if not @type or @type == ""?
if value?
$(@node).text value
else if @type in @textTypes
@node.value = value
else if @type == 'password'
@node.value = value or ''
else if @type == 'checkbox'
$(@node).attr 'checked', value
this
getValue: ()->
if @node.tagName == "TEXTAREA"
$(@node).val()
else if not @type or @type == ""?
$(@node).html()
else if @type in @textTypes
@node.value
else if @type == 'password'
@node.value
else if @type == 'checkbox'
$(@node).is ':checked'
else
throw new Error "Unknown type to bind to: #{@type}"
applyContext: (context)->
context.watch @attr, (value)=>
@setValue value if value?
$(@node).change ()=>
try
context.get(@attr, false)(@getValue())
catch err
context.set @attr, @getValue()
val = context.get @attr
@setValue val if val?
this
class DataText extends Binding
# Interpolate a node's text content on the fly.
@bind 'data-text'
update: (context)->
$(@node).text substitute @text, context
applyContext: (context)->
@text = $(@node).text()
vars = getVars @text
for pair in vars
context.watch pair.name, => @update context
@update context
class DataClick extends Binding
# Call a method on the context when the tag is clicked, eg:
# <a data-click="markDone" href="#">Click Me</a>
# <a data-click="markDone:todo.id" href="#">Click Me</a>
@bind 'data-click'
constructor: (node)->
super node
[@funcName, @funcArgs] = @attr.split(':')
@funcArgs or= ''
applyContext: (context)->
$(@node).bind 'click', (e)=>
args = [e].concat (context.get(arg) for arg in @funcArgs.split ',' when arg.trim())
fn = context.get @funcName, false
if not fn?
throw new Error "Couldn't find click handler '#{@funcName}' in context"
fn.apply @node, args
this
class DataHover extends Binding
# Call a method on the context when we hover over an
# element, eg:
# <a data-hover="highlight:todo" href="#">Click Me</a>
# If the mouse enters, the first argument will be `true`,
# if the mouse leaves, the first argument will be `false`.
@bind 'data-hover'
constructor: (node)->
super node
[@funcName, @funcArgs] = @attr.split(':')
@funcArgs or= ''
applyContext: (context)->
getArgs = =>
(context.get(arg) for arg in @funcArgs.split ',' when arg.trim())
fn = context.get @funcName, false
$(@node).bind 'mouseenter', (e)=>
fn.apply this, [e, true].concat getArgs()
$(@node).bind 'mouseleave', (e)=>
fn.apply this, [e, false].concat getArgs()
class DataSet extends Binding
# Sets a certain value on the model. Sometimes useful. Eg:
# <ul data-set="active=one">...</ul>
@bind 'data-set'
applyContext: (context)->
[name, value] = @attr.split '='
context.set(name, value)
class DataActivate extends Binding
# Gives an element an `active` class depending on if the
# context property matches the attribute, eg:
# <ul><li data-activate="active==one,active==two"/></ul>
@bind 'data-activate'
constructor: (node)->
super node
@values = (att.split('==') for att in @attr.split(','))
activate: (value)->
if value in (v[1] for v in @values)
$(@node).addClass 'active'
else
$(@node).removeClass 'active'
applyContext: (context)->
for v in @values
context.watch v[0], (value)=>
@activate value
$(@node).bind 'click', =>
context.set @values[0][0], @values[0][1]
@activate context.get @values[0][0]
class DataStyle extends Binding
# Adds inline styling to a node, eg:
# <a data-style="background:#{todo.color};font-size:#{todo.priority}em;"
@bind 'data-style'
regexp: /#\{(.*?)\}/g
constructor: (node)->
super node
@styles = {}
styles = @attr.split(";")
for style in styles
continue if not style.trim()
[name, raw] = style.split(":")
objects = raw.match @regexp
@styles[name] = raw: raw, objects: objects
setValue: (name, value)->
$(@node).css(name.trim(), value.trim())
doStyle: (context, name, raw, objects)->
for object in objects
raw = raw.replace object, context.get(object.replace(@regexp, '$1'))
@setValue name, raw
applyContext: (context)->
for own name, {raw, objects} of @styles
if not objects?
@setValue name, raw
continue
for object in objects
object = object.replace(@regexp, '$1')
context.watch object, (value)=>
@doStyle context, name, raw, objects
@doStyle context, name, raw, objects
class DataTemplate extends Binding
# Creates a reusable template and stores it in the context
# under it's attribute value, eg:
#
# <div data-template="template1"></div>
#
# To render the template, you can query the context and call
# `render` which returns a new DOM node with the given context
# applied to it:
#
# $(body).append(context.get('template1').render(tplcontext))
@bind 'data-template'
stop: true
constructor: (node)->
@attr = @templateName = node.getAttribute @name
@node = node.parentElement
@template = node.cloneNode true
$(@template).removeAttr 'data-template'
$(node).remove()
applyContext: (context)->
context.root().set @templateName, this
render: (context)->
clone = @template.cloneNode true
tpl = new Template(clone)
{bindings} = Binding.init clone
tpl.push b for b in bindings
tpl.applyContext context
return clone
class DataLoop extends Binding
@bind 'data-loop'
# Stop recursive descent on this node
stop: true
constructor: (node)->
@attr = node.getAttribute @name
@node = @parentElement = node.parentElement
@template = node.cloneNode true
$(@template).removeAttr 'data-loop'
$(node).remove()
[@elementName, _, @iterableName] = @attr.split ' '
@nodes = []
@contexts = []
renderIterable: (context, iterable)->
iterable = iterable or []
$(@nodes).remove()
@nodes = []
@templates = []
@contexts.shift().destroy() while @contexts.length > 0
for element in iterable
ctx = context.new()
ctx[@elementName] = element
@contexts.push ctx
clone = @template.cloneNode true
@nodes.push clone
tpl = new Template(clone)
@templates.push tpl
# Creating a template instance does not take
# into account bindings on the root element
# so we have to do that manually
{bindings} = Binding.init clone
tpl.push binding for binding in bindings
tpl.attrs = tpl.attrs.concat Attribute.init clone
tpl.applyContext ctx
@parentElement.appendChild node for node in @nodes
this
applyContext: (context)->
context.watch @iterableName, (iterable)=>
@renderIterable context, iterable
@renderIterable context, context.get @iterableName
this
return Cuffs.Template.DefaultBindings = {
DataShow: DataShow
DataBind: DataBind
DataClick: DataClick
DataLoop: DataLoop
DataSet: DataSet
DataActivate: DataActivate
DataStyle: DataStyle
DataAttr: DataAttr
DataOr: DataOr
DataTemplate: DataTemplate
}