UNPKG

@danielkalen/simplybind

Version:

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

449 lines (266 loc) 12 kB
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