UNPKG

@danielkalen/simplybind

Version:

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

473 lines (275 loc) 12 kB
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