@danielkalen/simplybind
Version:
Magically simple, framework-less one-way/two-way data binding for frontend/backend in ~5kb.
473 lines (275 loc) • 12 kB
text/coffeescript
Binding:: =
## ==========================================================================
## Instance-mutating methods
## ==========================================================================
makePropertyLive: (force)-> if @options.liveProps
_ = @
if @type is 'ObjectProp'
propertyDescriptor = Object.getOwnPropertyDescriptor(@object, @property) or dummyPropertyDescriptor
shouldWriteLiveProp = force or not @isLiveProp and (propertyDescriptor.configurable or @options.mutateInherited)
import [browserOnly] Binding;;makePropertyLive.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
else if @type is 'Array'
if not @isLiveProp
@isLiveProp = true
arrayMutatorMethods.forEach (method)->
Object.defineProperty _.value, method,
configurable: true
value: ()->
result = Array::[method].apply _.value, arguments
_.updateAllDeps(_)
return result
if @options.trackArrayChildren and not @trackedChildren
@trackedChildren = [] # Required so we can add new children that are bound to a specific index of this array
@updateSelf = ()-> _.updateAllDeps(_) # Saved to 'this' so it can be reused when adding new children in .setObject
opts = updateOnBind:false
@value.forEach (item, index)->
_.trackedChildren.push(''+index)
SimplyBind(index, opts).of(_.value)
.to _.updateSelf
return
addDep: (dep, bothWays)->
if dep.isMulti
@addDep(depItem) for depItem in dep.bindings
else
unless @depsMap[1][dep.ID]
@depsMap[1][dep.ID] = dep
@deps.push(dep)
if @placeholder
@myPholders[dep.ID] = @placeholder
else if @myPholders[dep.ID]
delete @myPholders[dep.ID]
if dep.placeholder
@depsPholders[dep.ID] = dep.placeholder
if bothWays
@depsMap[2][dep.ID] = dep
else if dep.depsMap[1][@ID] # Check if the passed Object ID has this Object's ID in its dependents and invoke .bothWays()
dep.addDep(@, true)
@addDep(dep, true)
return @
removeDep: (dep, bothWays)->
if dep.isMulti
@removeDep(depItem, bothWays) for depItem in dep.bindings
else
if @depsMap[1][dep.ID]
@deps.splice(@deps.indexOf(dep), 1)
delete @depsMap[1][dep.ID]
delete @depsPholders[dep.ID]
if bothWays
dep.removeDep(@)
delete @depsMap[2][dep.ID]
return
removeAllDeps: (bothWays)->
@removeDep(dep, bothWays) for dep in @deps.slice()
@destroy() if bothWays or Object.keys(@depsMap[2]).length is 0 # Resets object to initial state (Since it's no longer a dependent or has any dependents)
return
destroy: ()->
delete boundInstances[@ID]
if @type is 'ObjectProp'
Object.defineProperty @object, @property, {'value':@value, 'writable':true}
delete @object._sb_map
delete @object._sb_ID
else 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
return
## ==========================================================================
## Value-related methods
## ==========================================================================
fetchDirectValue: ()->
type = @type
switch
when type is 'Func' then @object()
when type is 'Array' then @object
import [browserOnly] Binding;;fetchDirectValue.DOMTypes.coffee
else @object[@property]
setValue: (newValue, specificPlaceholder, updater=@, fromSelf)->
prevValue = if specificPlaceholder then @pholderValues[specificPlaceholder] else @value
newValue = @selfTransform(newValue) if @selfTransform
isNewValue = newValue isnt prevValue or @options.updateEvenIfSame
if isNewValue and @type isnt 'Array'
if specificPlaceholder
@pholderValues[specificPlaceholder] = newValue
import [browserOnly] Binding;;setValue.DOMText-placeholders.coffee
newValue = applyPlaceholders(@pholderContexts, @pholderValues, @pholderIndexMap)
switch @type
when 'ObjectProp'
@object[@property] = newValue unless @isLiveProp import [browserOnly] Binding;;setValue.DOMText.coffee
when 'Func'
prevValue = @valuePassed
newValue = newValue.slice() if updater.type is 'Array' and newValue is updater.value
@valuePassed = newValue
newValue = @object(newValue, prevValue)
when 'Event'
if not fromSelf
@isEmitter = true
@emitEvent(newValue)
@isEmitter = false
import [browserOnly] Binding;;setValue.DOMTypes.coffee
@value = newValue
@updateAllDeps(updater)
return
updateAllDeps: (updater)-> if @deps.length
if @throttleRate
currentTime = +(new Date)
timePassed = currentTime - @lastUpdate
if timePassed < @throttleRate
clearTimeout(@throttleTimeout)
return @throttleTimeout = setTimeout (()=> @updateAllDeps(updater)), @throttleRate-timePassed
else
@lastUpdate = currentTime
@updateDep(dep, updater) for dep in @deps
return
updateDep: (dep, updater)->
return if (updater is dep) or (updater isnt @ and updater.depsMap[1][dep.ID]) # indicates this is an infinite loop
myPlaceholder = @myPholders[dep.ID]
depPlaceholder = @depsPholders[dep.ID]
currentValue = if myPlaceholder then @pholderValues[myPlaceholder] else @value
depValue = if depPlaceholder then dep.pholderValues[depPlaceholder] else dep.value
newValue = if not @hasTransforms then currentValue else @applyTransform(dep, depPlaceholder, currentValue, depValue)
return if @hasConditions and not @checkCondition(dep, depPlaceholder, currentValue, depValue)
# 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 @options.promiseTransforms and newValue and checkIf.isFunction(newValue.then)
newValue.then (newValue)-> dep.setValue(newValue, depPlaceholder, updater); return
else
dep.setValue(newValue, depPlaceholder, updater)
return
## ==========================================================================
## Transforms
## ==========================================================================
processTransform: (transformFn, subjects)->
if not checkIf.isFunction(transformFn)
throwWarning('fnOnly',2)
else
for prox in subjects
prox = prox._ or prox # Second is chosen when the passed proxied multi-binding (is a recursive call of this method)
if prox.isMulti
@processTransform(transformFn, prox.bindings)
else
@addTransform(prox.ID, transformFn)
if @depsMap[2][prox.ID]
prox.addTransform(@ID, transformFn)
@updateDep(prox, @) if @options.updateOnBind or @type is 'Func'
return true
applyTransform: (dep, placeholder, value, depValue)->
if @transforms[dep.ID]
return @transforms[dep.ID](value, depValue)
else return value
addTransform: (ID, transformFn)->
@hasTransforms = true
@transforms[ID] = transformFn
return
## ==========================================================================
## Conditions
## ==========================================================================
processCondition: (conditionFn, subjects)->
if not checkIf.isFunction(conditionFn)
throwWarning('fnOnly',2)
else
for prox in subjects
prox = prox._ or prox # Second is chosen when the passed proxied multi-binding (is a recursive call of this method)
if prox.isMulti
@processCondition(conditionFn, prox.bindings)
else
@addCondition(prox.ID, conditionFn)
if @depsMap[2][prox.ID]
prox.addCondition(@ID, conditionFn)
return true
checkCondition: (dep, placeholder, value, depValue)->
if @conditions[dep.ID]
return @conditions[dep.ID](value, depValue)
else return true
addCondition: (ID, conditionFn)->
@hasConditions = true
@conditions[ID] = conditionFn
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] Binding;;scanForPholders.DOMText.coffee
return
## ==========================================================================
## Polling
## ==========================================================================
addPollInterval: (time)->
@removePollInterval()
@pollInterval = setInterval ()=>
polledValue = @fetchDirectValue()
@setValue polledValue
, time
removePollInterval: ()->
clearInterval(@pollInterval)
@pollInterval = null
## ==========================================================================
## Events
## ==========================================================================
import [browserOnly] Binding;;addUpdateListener.coffee
import [browserOnly] Binding;;emitChangeEvent.coffee
attachEvents: ()->
if @eventName
@registerEvent @eventName, @customEventMethod.in
import [browserOnly] Binding;;attachEvents.DOM.coffee
return
registerEvent: (eventName, customInMethod)-> if not arrayIncludes(@attachedEvents, eventName)
import [browserOnly] Binding;;registerEvent.defaultInMethod-browser.coffee
import [nodeOnly] Binding;;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] Binding;;unRegisterEvent.defaultRemoveMethod-browser.coffee
import [nodeOnly] Binding;;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] Binding;;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] Binding;;emitEvent.defaultOutMethod-browser.coffee
import [nodeOnly] Binding;;emitEvent.defaultOutMethod-node.coffee
emitMethod = @customEventMethod.out or defaultOutMethod
import [browserOnly] Binding;;emitEvent.jQuery.coffee
emitMethod = defaultOutMethod unless subject[emitMethod]#exists
import [browserOnly] Binding;;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