UNPKG

@danielkalen/simplybind

Version:

Magically simple, framework-less one-way/two-way data binding for frontend/backend in ~5kb.

432 lines (261 loc) 11 kB
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