@danielkalen/simplybind
Version:
Magically simple, framework-less one-way/two-way data binding for frontend/backend in ~5kb.
232 lines (148 loc) • 9.39 kB
text/coffeescript
do ()->
import parts/closure-helpers.coffee
import parts/closure-errorsAndWarnings.coffee
boundInstances = {}
globalOptions =
silent: false
liveProps: true
dispatchEvents: false
updateEvenIfSame: false
updateOnBind: true
mutateInherited: false
trackArrayChildren: false
simpleSelector: false
promiseTransforms: false
placeholder: ['{{', '}}']
setPholderRegEx()
SimplyBind = (subject, options, isProxiedFunc)->
if (!subject and subject isnt 0) or (!checkIf.isString(subject) and !checkIf.isNumber(subject) and !checkIf.isFunction(subject) and subject not instanceof Array)
throwError('invalidParamName') unless checkIf.isBindingInterface(subject)
if checkIf.isObject(subject) and subject not instanceof Array # Indicates it's a Binding instance object due to the above check
return new BindingInterface subject._, 1#, subject.placeholder
else return new BindingInterface null, 0, null, subject, isProxiedFunc, options
Binding = (object, type, state, isProxiedFunc)->
extendState(@, state)
@type = type # ObjectProp | Array | Func | Event | DOMAttr | DOMValue | DOMCheckbox | DOMRadio
@object = object # The subject object of this binding, i.e. function, array, {}, DOM el, etc.
@ID = genID() # Assigned only after passing a valid object to .of()
@deps = [] # Dependents array listing all of the objects that will be updated upon value update
@depsMap = {1:genObj(), 2:genObj()} # Map dependents by ID on the '1' object if is one-way binding and '2' as wel if is two-way (deps that have this object as a dep as well)
@depsPholders = genObj() # DependentID:dep-placeholder mapping
@myPholders = genObj() # DependentID:self-placeholder mapping indicating which deps will receive a value of a specific placeholder from this binding (if applicable)
@transforms = genObj() # DependentID:transformFn mapping
@conditions = genObj() # DependentID:coniditionFn mapping
@attachedEvents = [] # Array listing all of the events currently listened on @object
# ==== Properties declared later or inherited from binding interface =================================================================================
# @options = options
# @valueOriginal = undefined # Value on init
# @value = undefined # Will represent the actual current value of the binding/object
# @property = property # The property name or array index or event callback argument
# @selector = selector # The property name or array index or event callback argument
# @customEventMethod = {} # Names of the event emitter/trigger methods (if applicable)
# @pholderValues = {} # Placeholder value mapping
# @pholderContexts = {} # Placeholder surroundings (original binding value split by the placeholder regEx)
# @pholderIndexMap = {} # Placeholder occurence mapping, i.e. the placeholder name for each placeholder occurence
# @placeholder = "" # The last specified placeholder to bind the value to
# @descriptor = "" # Describes the type of property, i.e. 'attr:data-name' to indicate a DOMAttr type binding
# @isLiveProp = Boolean # Indicates whether or not the Object/Object's propety have been modified to be a live property
# @isDom = Boolean # Indicates whether or not the binding's object is a DOM object
# @pollInterval = ID # The interval ID of the timer that manually polls the object's value at a set interval
# @arrayBinding = Binding # Reference to the parent array binding (if exists) for an index-of-array binding (i.e. SimplyBind(array))
# @trackedChildren = [] # For Array type bindings - indicates which indicies of the array are tracked for changes (applicable when @options.trackArrayChildren) is true
# @eventName = "" # The name of the event this binding is listening to (for Event type bindings)
# @isEmitter = Boolean # Tracker to let us know we shouldn't handle the event update we received as it is the event this binding just emitted
# @eventHandler = Function # The callback that gets triggered upon an event emittance (for Even type bindings)
# @eventObject = Event # The dispatched event object (for Event type bindings)
# @selfTransform = Function # The transform function that new values being set to this binding are being passed through during @setValue (if applicable)
# @throttleRate = milliseconds # The rate in which the binding's dependents will be updated only once in
# @throttleTimeout = ID # The timeout ID of the delay timer to update a throttled binding
# @lastUpdate = epoch timestamp # Last time the deps have been updated; used for throttle functions
# @hasTransforms = Boolean # Indicates whether or not at least 1 dependent has a transform (to refrain from executing useless code)
# @hasConditions = Boolean # Indicates whether or not at least 1 dependent has a condition (to refrain from executing useless code)
# @isAsync = Boolean # Indicates if this is an async binding (currently only used for Event bindings)
### ========================================================================== ###
if @type is 'Event' or @type is 'Func'
@options.updateOnBind = false
@options.updateEvenIfSame = true
import [browserOnly] parts/Binding-DOMChoice_group.coffee
unless @type is 'Event' or isProxiedFunc
@value = @valueOriginal = subjectValue = @fetchDirectValue()
if @type is 'ObjectProp' and not checkIf.isDefined(subjectValue)
@object[@property] = subjectValue # Define the prop on the object if it non-existent
if @placeholder and not @pholderValues
@scanForPholders()
@makePropertyLive()
@attachEvents()
# IF this is a binding to a specific index of an array then we must make this binding update the array on change if the array binding is tracking its children
if @object instanceof Array and @type isnt 'Array'
@arrayBinding = arrayBinding = cache.get(@object, true)
if arrayBinding and arrayBinding.options.trackArrayChildren and not arrayIncludes(arrayBinding.trackedChildren, @property)
arrayBinding.trackedChildren.push(@property)
SimplyBind(@property).of(@object).to arrayBinding.updateSelf
return boundInstances[@ID] = @
###*
* Stage definitions:
*
* 0: Selection: Got selector, awaiting object.
* 1: Indication: Got object, awaiting proxied property / function / Binding-object.
* 2: Binding Selection: Got proxied selector, awaiting proxied object.
* 3: Binding Complete: Complete, awaiting additional (optional) bindings/mutations.
###
BindingInterface = (binding, stage, inheritedState, subject, isProxiedFunc, options)->
extendState(@, inheritedState) if inheritedState
@stage = stage or 0
@proxies ?= []
@state ?= {}
# @state.hasTransform
# @state.hasMultiTransforms
# @state.initialBinding
# @state.hasEventName
switch @stage
when 0
@optionsPassed = options ||= {}
@options = {}
for key of globalOptions
@options[key] = if options[key]? then options[key] else globalOptions[key]
if checkIf.isFunction(subject)
@stage = 1
binding = @createBinding(subject, 'Func', true, isProxiedFunc)
else if subject instanceof Array
@stage = 1
binding = @createBinding(subject, 'Array', true)
else
subject = subject.toString() if checkIf.isNumber(subject)
@selector = @property = subject
unless @options.simpleSelector
if arrayIncludes(@selector, ':')
split = @property.split(':')
@descriptor = split[0] # An addl. string in the selector name defining the scope of the selection (i.e. SimplyBind('attr:class'))
@property = split[1]
if arrayIncludes(@selector, '.') # Placeholder extraction
split = @property.split('.')
@property = split[0]
@placeholder = split.slice(1).join('.')
@selector = @property
when 1
unless binding # is defined
import [browserOnly] parts/BindingInterface-parseDOMObject.coffee
newObjectType = switch
when @state.hasEventName then 'Event'
import [browserOnly] parts/BindingInterface-setType.DOM.coffee
else 'ObjectProp'
if @descriptor is 'multi'
binding = new BindingMulti(subject, newObjectType, @)
else
binding = @createBinding(subject, newObjectType)
@defineMainProps(binding)
# ==== Binding Prototype =================================================================================
import parts/Binding-prototype.coffee
# ==== BindingInterface Prototype =================================================================================
import parts/BindingInterface-private_prototype.coffee
import parts/BindingInterface-prototype.coffee
# ==== BindingInterface Multi Proxy =================================================================================
import parts/BindingMulti.coffee
# ==== SimplyBind props/methods =================================================================================
import parts/SimplyBind-methods.coffee
# ==== Exports =================================================================================
import [browserOnly] parts/SimplyBind-export.window.coffee
import [nodeOnly] parts/SimplyBind-export.module.coffee