@danielkalen/simplybind
Version:
Magically simple, framework-less one-way/two-way data binding for frontend/backend in ~5kb.
432 lines (261 loc) • 11 kB
text/coffeescript
Binding:: =
## ==========================================================================
## Subscriber Management
## ==========================================================================
addSub: (sub, options, updateOnce, updateEvenIfSame)->
if sub.isMulti
@addSub(subItem, options, updateOnce, updateEvenIfSame) for subItem in sub.bindings
else
if metaData=@subsMeta[sub.ID]
alreadyHadSub = true
else
sub.pubsMap[@ID] = @
@subs.unshift(sub)
metaData = @subsMeta[sub.ID] = genObj()
metaData.updateOnce = updateOnce
metaData.opts = cloneObject(options)
metaData.opts.updateEvenIfSame = true if updateEvenIfSame or @type is 'Event' or @type is 'Proxy' or @type is 'Array'
metaData.valueRef = if sub.type is 'Func' then 'valuePassed' else 'value'
return alreadyHadSub
removeSub: (sub, bothWays)->
if sub.isMulti
@removeSub(subItem, bothWays) for subItem in sub.bindings
else
if @subsMeta[sub.ID]
@subs.splice(@subs.indexOf(sub), 1)
delete @subsMeta[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) for event in @attachedEvents
else if @type is 'Func'
delete @object._sb_ID
### istanbul ignore next ###
convertToReg(@, @object) if @isLiveProp and @origDescriptor
convertToReg(@, @value, true) if @type is 'Array'
if @object._sb_map
delete @object._sb_map[@selector]
delete @object._sb_map if Object.keys(@object._sb_map).length is 0
return
## ==========================================================================
## Value set/get
## ==========================================================================
fetchDirectValue: ()->
type = @type
switch
when type is 'Func' then @object()
# simplyimport:if BUNDLE_TARGET = 'browser'
when type is 'DOMAttr' then @object.getAttribute(@property) or ''
when @isMultiChoice
results = []
for choiceName,choiceEl of @choices
if choiceEl.object.checked
if type is 'DOMRadio'
return choiceName
else
results.push choiceName
return results
# simplyimport:end
else @object[@property]
setValue: (newValue, publisher, fromSelf, fromChangeEvent)-> # fromSelf===true when called from eventUpdateHandler or property descriptor setter (unless it's an Array binding)
publisher ||= @
newValue = @selfTransform(newValue) if @selfTransform
unless fromSelf then switch @type
when 'ObjectProp'
if not @isLiveProp
@object[@property] = newValue if newValue isnt @value
# simplyimport:if BUNDLE_TARGET = 'browser'
importInline './prototype.setValue-ObjectProp-DOMValue'
# simplyimport:end
else if @origSetter
@origSetter(newValue)
when 'Pholder'
parent = @parentBinding
parent.pholderValues[@pholder] = newValue
entireValue = applyPlaceholders(parent.pholderContexts, parent.pholderValues, parent.pholderIndexMap)
# simplyimport:if BUNDLE_TARGET = 'browser'
if @textNodes and newValue isnt @value
for textNode in @textNodes
textNode[textContent] = newValue
# simplyimport:end
# simplyimport:if BUNDLE_TARGET = 'browser'
parent.setValue(entireValue, publisher) unless @property is textContent
# simplyimport:end
# simplyimport:if BUNDLE_TARGET = 'node'
parent.setValue(entireValue, publisher)
# simplyimport:end
when 'Array'
if newValue isnt @value
newValue = Array::concat(newValue) if not checkIf.isArray(newValue)
convertToReg(@, @value, true)
convertToLive(@, newValue=newValue.slice(), true)
@origSetter(newValue) if @origSetter # Will update any other previous non-Array bindings to the same object property
when 'Func'
prevValue = @valuePassed
@valuePassed = newValue
newValue = @object(newValue, prevValue)
when 'Event'
@isEmitter = true
@emitEvent(newValue)
@isEmitter = false
# simplyimport:if BUNDLE_TARGET = 'browser'
importInline './prototype.setValue-DOMTypes'
# simplyimport:end
@value = newValue
@updateAllSubs(publisher)
return
updateAllSubs: (publisher)-> if i=(arr=@subs).length # Ugly shortcut for index definition in order to limit logic repitiion
@updateSub(arr[i], publisher) while i--
return
updateSub: (sub, publisher, isDelayedUpdate)->
return if (publisher is sub) or (publisher isnt @ and publisher.subsMeta[sub.ID]) # indicates this is an infinite loop
meta = @subsMeta[sub.ID]
if meta.disallowList and meta.disallowList[publisher.ID]
return
if meta.opts.throttle
currentTime = +(new Date)
timePassed = currentTime - meta.lastUpdate
if timePassed < meta.opts.throttle
clearTimeout(meta.updateTimer)
return meta.updateTimer =
setTimeout ()=>
@updateSub(sub, publisher) if @subsMeta[sub.ID]
, meta.opts.throttle-timePassed
else
meta.lastUpdate = currentTime
else if meta.opts.delay and not isDelayedUpdate
return setTimeout ()=>
@updateSub(sub, publisher, true) if @subsMeta[sub.ID]
, meta.opts.delay
newValue = if @type is 'Array' and meta.opts.sendArrayCopies then @value.slice() else @value
subValue = sub[meta.valueRef]
newValue = if transform=meta.transformFn then transform(newValue, subValue, sub.object) else newValue
return if newValue is subValue and not meta.opts.updateEvenIfSame or
meta.conditionFn and not meta.conditionFn(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 meta.opts.promiseTransforms and newValue and checkIf.isFunction(newValue.then)
newValue.then (newValue)-> sub.setValue(newValue, publisher); return
else
sub.setValue(newValue, publisher)
@removeSub(sub) if meta.updateOnce
return
## ==========================================================================
## Transforms & Conditions
## ==========================================================================
addModifierFn: (target, subInterfaces, subjectFn, updateOnBind)->
if not checkIf.isFunction(subjectFn)
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(target, subscriber.bindings, subjectFn, updateOnBind)
else
subMetaData = @subsMeta[subscriber.ID]
subMetaData[target] = subjectFn
updateOnBind = updateOnBind and not subMetaData.updateOnce
if @pubsMap[subscriber.ID]
subscriber.subsMeta[@ID][target] ||= subjectFn # Will not replace existing modifier function if exists
@updateSub(subscriber, @) if (updateOnBind or @type is 'Func') and target is 'transformFn'
return true
setSelfTransform: (transformFn, updateOnBind)->
@selfTransform = transformFn
@setValue(@value) if updateOnBind
return
## ==========================================================================
## Allow/Disallow rules
## ==========================================================================
addDisallowRule: (targetSub, targetDisallow)->
disallowList = @subsMeta[targetSub.ID].disallowList ?= genObj()
disallowList[targetDisallow.ID] = 1
return
## ==========================================================================
## Placeholders
## ==========================================================================
scanForPholders: ()-> unless @pholderValues
@pholderValues = genObj()
@pholderIndexMap = genObj()
@pholderContexts = []
if checkIf.isString(@value)
@pholderContexts = @value.split pholderRegExSplit
index = 0
@value = @value.replace pholderRegEx, (e, pholder)=>
@pholderIndexMap[index++] = pholder
@pholderValues[pholder] = pholder
# simplyimport:if BUNDLE_TARGET = 'browser'
scanTextNodesPlaceholders(@object, @textNodes=genObj()) if @isDom and @property is textContent
# simplyimport:end
return
## ==========================================================================
## Polling
## ==========================================================================
addPollInterval: (time)-> if @type isnt 'Event'
@removePollInterval()
@pollInterval = setInterval ()=>
polledValue = @fetchDirectValue()
@setValue polledValue, @, true
, time
removePollInterval: ()->
clearInterval(@pollInterval)
@pollInterval = null
## ==========================================================================
## Events
## ==========================================================================
# simplyimport:if BUNDLE_TARGET = 'browser'
addUpdateListener: (eventName, targetProperty)->
@object.addEventListener eventName, (event)=>
unless event._sb
shouldRedefineValue = @selfTransform and @isDomInput
@setValue(@object[targetProperty], null, !shouldRedefineValue, true)
return
, false
return
# simplyimport:end
attachEvents: ()->
if @eventName
@registerEvent(@eventName)
# simplyimport:if BUNDLE_TARGET = 'browser'
else if @isDomInput
@addUpdateListener('input', 'value')
@addUpdateListener('change', 'value')
else if not @isMultiChoice and (@type is 'DOMRadio' or @type is 'DOMCheckbox')
@addUpdateListener('change', 'checked')
# simplyimport:end
return
registerEvent: (eventName)->
@attachedEvents.push(eventName)
@eventHandler = eventUpdateHandler.bind(@) unless @eventHandler
@object[@eventMethods.listen](eventName, @eventHandler)
return
unRegisterEvent: (eventName)->
@attachedEvents.splice @attachedEvents.indexOf(eventName), 1
@object[@eventMethods.remove](eventName, @eventHandler)
return
emitEvent: (extraData)->
eventObject = @eventName
# simplyimport:if BUNDLE_TARGET = 'browser'
if @eventMethods.emit is 'dispatchEvent'
unless @eventObject
@eventObject = document.createEvent('Event')
@eventObject.initEvent(@eventName, true, true)
@eventObject.bindingData = extraData
eventObject = @eventObject
# simplyimport:end
@object[@eventMethods.emit](eventObject, extraData)
return
eventUpdateHandler = ()-> unless @isEmitter
@setValue(arguments[@property], null, true)
return