@danielkalen/simplybind
Version:
Magically simple, framework-less one-way/two-way data binding for frontend/backend in ~5kb.
449 lines (266 loc) • 12 kB
text/coffeescript
Binding:: =
## ==========================================================================
## Instance-mutating methods
## ==========================================================================
convertToLive: (force)->
_ = @
switch @type
when 'ObjectProp'
propertyDescriptor = Object.getOwnPropertyDescriptor(@object, @property) or dummyPropertyDescriptor
shouldWriteLiveProp = force or propertyDescriptor.configurable
import [browserOnly] prototype.convertToLive-StylingConstructorCheck.coffee
if shouldWriteLiveProp
@isLiveProp = true
Object.defineProperty @object, @property,
configurable: true
enumerable: propertyDescriptor.enumerable
get: ()-> _.value
set: if propertyDescriptor.set
(newValue)-> propertyDescriptor.set(newValue); _.setValue(newValue); return
else
(newValue)-> _.setValue(newValue); return
when 'Array'
arrayMutatorMethods.forEach (method)->
Object.defineProperty _.value, method,
configurable: true
value: ()->
result = Array::[method].apply _.value, arguments
_.updateAllSubs(_)
return result
if settings.trackArrayChildren and not @trackedChildren
@trackedChildren = [] # Required so we can add new children that are bound to a specific index of this array
@updateSelf = ()-> _.updateAllSubs(_) # Saved to 'this' so it can be reused when adding new children in .setObject
@value.forEach (item, index)->
_.trackedChildren.push(''+index)
SimplyBind(index, OPTS_NOUPDATE).of(_.value)
.to _.updateSelf
when 'Proxy'
originalFn = @original = @value
context = @object
@value = result:null, args:null
if checkIf.isFunction(originalFn)
slice = [].slice
@object[@property] = ()->
_.value.args = args = slice.call(arguments)
_.value.result = result = originalFn.apply(context, args)
_.updateAllSubs(_)
return result
return
addSub: (sub, options=@optionsDefault, updateOnce)->
if sub.isMulti
@addSub(subItem, options, updateOnce) for subItem in sub.bindings
else
if @subOpts[sub.ID]
alreadyHadSub = !@myPholders[sub.ID]
else
@subs.unshift(sub)
@subOpts[sub.ID] = cloneObject(options)
@subOpts[sub.ID].updateEvenIfSame = true if sub.type is 'Func' or sub.type is 'Event'
sub.pubsMap[@ID] = @
if updateOnce
SimplyBind('value', OPTS_NOUPDATE).of(sub).to ()=> @removeSub(sub)
if @placeholder
@myPholders[sub.ID] = @placeholder
else if @myPholders[sub.ID]
delete @myPholders[sub.ID]
if sub.placeholder
@subsPholders[sub.ID] = sub.placeholder
return alreadyHadSub
removeSub: (sub, bothWays)->
if sub.isMulti
@removeSub(subItem, bothWays) for subItem in sub.bindings
else
if @subOpts[sub.ID]
@subs.splice(@subs.indexOf(sub), 1)
delete @subsPholders[sub.ID]
delete @subOpts[sub.ID]
delete sub.pubsMap[@ID]
if bothWays
sub.removeSub(@)
delete @pubsMap[sub.ID]
if @subs.length is 0 and Object.keys(@pubsMap).length is 0
@destroy() # Since it's no longer a subscriber or has any subscribers
return
removeAllSubs: (bothWays)->
@removeSub(sub, bothWays) for sub in @subs.slice()
return
destroy: ()-> # Resets object to initial state (pre-binding state)
delete boundInstances[@ID]
@removePollInterval()
if @type is 'Event'
@unRegisterEvent(event, @customEventMethod.remove) for event in @attachedEvents
delete @object._sb_map
else if @type is 'Array'
delete @object._sb_ID
delete @object[method] for method in arrayMutatorMethods
else if @type is 'Func'
delete @object._sb_ID
else
if @type is 'ObjectProp'
Object.defineProperty @object, @property, {'value':@value, 'writable':true}
delete @object._sb_map
delete @object._sb_ID
return
## ==========================================================================
## Value-related methods
## ==========================================================================
fetchDirectValue: ()->
type = @type
switch
when type is 'Func' then @object()
when type is 'Array' then @object
import [browserOnly] prototype.fetchDirectValue-DOMTypes.coffee
else @object[@property]
setValue: (newValue, specificPlaceholder, publisher, fromSelf)->
publisher ||= @
newValue = @selfTransform(newValue) if @selfTransform
if specificPlaceholder
prevValue = @pholderValues[specificPlaceholder]
@pholderValues[specificPlaceholder] = newValue
import [browserOnly] prototype.setValue-DOMText-placeholders.coffee
newValue = applyPlaceholders(@pholderContexts, @pholderValues, @pholderIndexMap)
switch @type
when 'ObjectProp'
@object[@property] = newValue if not @isLiveProp and newValue isnt @value import [browserOnly] prototype.setValue-DOMText.coffee
when 'Func'
prevValue = @valuePassed
newValue = newValue.slice() if publisher.type is 'Array' and newValue is publisher.value
@valuePassed = newValue
newValue = @object(newValue, prevValue)
when 'Event'
if not fromSelf
@isEmitter = true
@emitEvent(newValue)
@isEmitter = false
import [browserOnly] prototype.setValue-DOMTypes.coffee
@value = newValue
@updateAllSubs(publisher)
return
updateAllSubs: (publisher)-> if @subs.length
if @throttleRate
currentTime = +(new Date)
timePassed = currentTime - @lastUpdate
if timePassed < @throttleRate
clearTimeout(@throttleTimeout)
return @throttleTimeout = setTimeout (()=> @updateAllSubs(publisher)), @throttleRate-timePassed
else
@lastUpdate = currentTime
i = (arr=@subs).length
@updateSub(arr[i], publisher) while i--
return
updateSub: (sub, publisher)->
return if (publisher is sub) or (publisher isnt @ and publisher.subOpts[sub.ID]) # indicates this is an infinite loop
subOptions = @subOpts[sub.ID]
myPlaceholder = @placeholder and @myPholders[sub.ID] # Check if this binding has a placeholder before performing a placeholder lookup
subPlaceholder = sub.placeholder and @subsPholders[sub.ID] # Check if the subscriber has a placeholder before performing a placeholder lookup
currentValue = if myPlaceholder then @pholderValues[myPlaceholder] else @value
subValue = if subPlaceholder then sub.pholderValues[subPlaceholder] else sub.value
newValue = if @transforms and @transforms[sub.ID] then @transforms[sub.ID](currentValue, subValue, sub.object) else currentValue
return if newValue is subValue and not subOptions.updateEvenIfSame or
@conditions and @conditions[sub.ID] and not @conditions[sub.ID](newValue, subValue, sub.object)
# Why do we need the 'promiseTransforms' option when we can just check for the existance of .then method?
# Because tests show that when searching for the .then prop on the object results in a performance slowdown of up to 30%!
# Checking if the promiseTransforms option is enabled first eliminates unnecessary lookups & slowdowns.
if subOptions.promiseTransforms and newValue and checkIf.isFunction(newValue.then)
newValue.then (newValue)-> sub.setValue(newValue, subPlaceholder, publisher); return
else
sub.setValue(newValue, subPlaceholder, publisher)
return
## ==========================================================================
## Transforms & Conditions
## ==========================================================================
addModifierFn: (targetCollection, subInterfaces, targetFn, updateOnBind)->
if not checkIf.isFunction(targetFn)
throwWarning('fnOnly',2)
else
for subInterface in subInterfaces
subscriber = subInterface._ or subInterface # Second is chosen when the passed subscriber interfaces multi-binding (is a recursive call of this method)
if subscriber.isMulti
@addModifierFn(targetCollection, subscriber.bindings, targetFn)
else
@setModifierFn(subscriber.ID, targetCollection, targetFn)
if @pubsMap[subscriber.ID]
subscriber.setModifierFn(@ID, targetCollection, targetFn)
@updateSub(subscriber, @) if (updateOnBind or @type is 'Func') and targetCollection is 'transforms'
return true
setModifierFn: (targetID, targetCollection, targetFn)->
@[targetCollection] ?= genObj()
@[targetCollection][targetID] = targetFn
return
setSelfTransform: (transformFn, updateOnBind)-> if @type isnt 'Array'
@selfTransform = transformFn
@setValue(@value) if updateOnBind
return
## ==========================================================================
## Placeholders
## ==========================================================================
scanForPholders: ()->
@pholderValues = genObj()
@pholderIndexMap = genObj()
@pholderContexts = []
if checkIf.isString(@valueOriginal)
@pholderContexts = @valueOriginal.split pholderRegExSplit
index = 0
@value = @valueOriginal.replace pholderRegEx, (e, pholder)=>
@pholderIndexMap[index++] = pholder
@pholderValues[pholder] = pholder
import [browserOnly] prototype.scanForPholders-DOMText.coffee
return
## ==========================================================================
## Polling
## ==========================================================================
addPollInterval: (time)-> if @type isnt 'Event'
@removePollInterval()
@pollInterval = setInterval ()=>
polledValue = @fetchDirectValue()
@setValue polledValue
, time
removePollInterval: ()->
clearInterval(@pollInterval)
@pollInterval = null
## ==========================================================================
## Events
## ==========================================================================
import [browserOnly] prototype.addUpdateListener.coffee
attachEvents: ()->
if @eventName
@registerEvent @eventName, @customEventMethod.in
import [browserOnly] prototype.attachEvents-DOM.coffee
return
registerEvent: (eventName, customInMethod)-> if not targetIncludes(@attachedEvents, eventName)
import [browserOnly] prototype.registerEvent.defaultInMethod-browser.coffee
import [nodeOnly] prototype.registerEvent.defaultInMethod-node.coffee
@attachedEvents.push(eventName)
attachmentMethod = customInMethod or defaultInMethod
@invokeEventMethod(eventName, attachmentMethod, defaultInMethod)
return
unRegisterEvent: (eventName, customMethod)->
indexOfEvent = @attachedEvents.indexOf eventName
return if indexOfEvent is -1
import [browserOnly] prototype.unRegisterEvent.defaultRemoveMethod-browser.coffee
import [nodeOnly] prototype.unRegisterEvent.defaultRemoveMethod-node.coffee
@attachedEvents.splice(indexOfEvent, 1)
removalMethod = customMethod or defaultRemoveMethod
@invokeEventMethod(eventName, removalMethod, defaultRemoveMethod)
return
invokeEventMethod: (eventName, eventMethod, backupMethod)->
subject = @object
import [browserOnly] prototype.invokeEventMethod-jQuery.coffee
eventMethod = backupMethod unless subject[eventMethod]
@eventHandler = handleUpdateFromEvent.bind(@) unless @eventHandler#exists
subject[eventMethod]? eventName, @eventHandler
return
emitEvent: (extraData)->
subject = @object
import [browserOnly] prototype.emitEvent.defaultOutMethod-browser.coffee
import [nodeOnly] prototype.emitEvent.defaultOutMethod-node.coffee
emitMethod = @customEventMethod.out or defaultOutMethod
import [browserOnly] prototype.emitEvent-jQuery.coffee
emitMethod = defaultOutMethod unless subject[emitMethod]#exists
import [browserOnly] prototype.emitEvent-dispatchEventObject.coffee
subject[emitMethod](@eventName, extraData)
return
handleUpdateFromEvent = ()-> unless @isEmitter
fetchedValue = if @type is 'Event' then arguments[@property] else @fetchDirectValue()
@setValue(fetchedValue, null, null, true)
return