295 lines
9.4 KiB
CoffeeScript
295 lines
9.4 KiB
CoffeeScript
# Rivets.Binding
|
|
# --------------
|
|
|
|
# A single binding between a model attribute and a DOM element.
|
|
class Rivets.Binding
|
|
# All information about the binding is passed into the constructor; the
|
|
# containing view, the DOM node, the type of binding, the model object and the
|
|
# keypath at which to listen for changes.
|
|
constructor: (@view, @el, @type, @keypath, @options = {}) ->
|
|
@formatters = @options.formatters or []
|
|
@dependencies = []
|
|
@formatterObservers = {}
|
|
@model = undefined
|
|
@setBinder()
|
|
|
|
# Sets the binder to use when binding and syncing.
|
|
setBinder: =>
|
|
unless @binder = @view.binders[@type]
|
|
for identifier, value of @view.binders
|
|
if identifier isnt '*' and identifier.indexOf('*') isnt -1
|
|
regexp = new RegExp "^#{identifier.replace(/\*/g, '.+')}$"
|
|
if regexp.test @type
|
|
@binder = value
|
|
@args = new RegExp("^#{identifier.replace(/\*/g, '(.+)')}$").exec @type
|
|
@args.shift()
|
|
|
|
@binder or= @view.binders['*']
|
|
@binder = {routine: @binder} if @binder instanceof Function
|
|
|
|
observe: (obj, keypath, callback) =>
|
|
Rivets.sightglass obj, keypath, callback,
|
|
root: @view.rootInterface
|
|
adapters: @view.adapters
|
|
|
|
parseTarget: =>
|
|
token = Rivets.TypeParser.parse @keypath
|
|
|
|
if token.type is Rivets.TypeParser.types.primitive
|
|
@value = token.value
|
|
else
|
|
@observer = @observe @view.models, @keypath, @sync
|
|
@model = @observer.target
|
|
|
|
parseFormatterArguments: (args, formatterIndex) =>
|
|
args = (Rivets.TypeParser.parse(arg) for arg in args)
|
|
processedArgs = []
|
|
|
|
for arg, ai in args
|
|
processedArgs.push if arg.type is Rivets.TypeParser.types.primitive
|
|
arg.value
|
|
else
|
|
@formatterObservers[formatterIndex] or= {}
|
|
|
|
unless observer = @formatterObservers[formatterIndex][ai]
|
|
observer = @observe @view.models, arg.value, @sync
|
|
@formatterObservers[formatterIndex][ai] = observer
|
|
|
|
observer.value()
|
|
|
|
processedArgs
|
|
|
|
# Applies all the current formatters to the supplied value and returns the
|
|
# formatted value.
|
|
formattedValue: (value) =>
|
|
for formatter, fi in @formatters
|
|
args = formatter.match /[^\s']+|'([^']|'[^\s])*'|"([^"]|"[^\s])*"/g
|
|
id = args.shift()
|
|
formatter = @view.formatters[id]
|
|
|
|
processedArgs = @parseFormatterArguments args, fi
|
|
|
|
if formatter?.read instanceof Function
|
|
value = formatter.read.call @model, value, processedArgs...
|
|
else if formatter instanceof Function
|
|
value = formatter.call @model, value, processedArgs...
|
|
|
|
value
|
|
|
|
# Returns an event handler for the binding around the supplied function.
|
|
eventHandler: (fn) =>
|
|
handler = (binding = @).view.handler
|
|
(ev) -> handler.call fn, @, ev, binding
|
|
|
|
# Sets the value for the binding. This Basically just runs the binding routine
|
|
# with the suplied value formatted.
|
|
set: (value) =>
|
|
# Since 0.9 : doesn't execute function unless backward compatibility is active
|
|
value = if (value instanceof Function and !@binder.function and Rivets.public.executeFunctions)
|
|
@formattedValue value.call @model
|
|
else
|
|
@formattedValue value
|
|
|
|
@binder.routine?.call @, @el, value
|
|
|
|
# Syncs up the view binding with the model.
|
|
sync: =>
|
|
@set if @observer
|
|
if @model isnt @observer.target
|
|
observer.unobserve() for observer in @dependencies
|
|
@dependencies = []
|
|
|
|
if (@model = @observer.target)? and @options.dependencies?.length
|
|
for dependency in @options.dependencies
|
|
observer = @observe @model, dependency, @sync
|
|
@dependencies.push observer
|
|
|
|
@observer.value()
|
|
else
|
|
@value
|
|
|
|
# Publishes the value currently set on the input element back to the model.
|
|
publish: =>
|
|
if @observer
|
|
value = @getValue @el
|
|
lastformatterIndex = @formatters.length - 1
|
|
|
|
for formatter, fiReversed in @formatters.slice(0).reverse()
|
|
fi = lastformatterIndex - fiReversed
|
|
args = formatter.split /\s+/
|
|
id = args.shift()
|
|
|
|
processedArgs = @parseFormatterArguments args, fi
|
|
|
|
if @view.formatters[id]?.publish
|
|
value = @view.formatters[id].publish value, processedArgs...
|
|
|
|
@observer.setValue value
|
|
|
|
# Subscribes to the model for changes at the specified keypath. Bi-directional
|
|
# routines will also listen for changes on the element to propagate them back
|
|
# to the model.
|
|
bind: =>
|
|
@parseTarget()
|
|
@binder.bind?.call @, @el
|
|
|
|
if @model? and @options.dependencies?.length
|
|
for dependency in @options.dependencies
|
|
observer = @observe @model, dependency, @sync
|
|
@dependencies.push observer
|
|
|
|
@sync() if @view.preloadData
|
|
|
|
# Unsubscribes from the model and the element.
|
|
unbind: =>
|
|
@binder.unbind?.call @, @el
|
|
@observer?.unobserve()
|
|
|
|
observer.unobserve() for observer in @dependencies
|
|
@dependencies = []
|
|
|
|
for fi, args of @formatterObservers
|
|
observer.unobserve() for ai, observer of args
|
|
|
|
@formatterObservers = {}
|
|
|
|
# Updates the binding's model from what is currently set on the view. Unbinds
|
|
# the old model first and then re-binds with the new model.
|
|
update: (models = {}) =>
|
|
@model = @observer?.target
|
|
@binder.update?.call @, models
|
|
|
|
# Returns elements value
|
|
getValue: (el) =>
|
|
if @binder and @binder.getValue?
|
|
@binder.getValue.call @, el
|
|
else
|
|
Rivets.Util.getInputValue el
|
|
|
|
# Rivets.ComponentBinding
|
|
# -----------------------
|
|
|
|
# A component view encapsulated as a binding within it's parent view.
|
|
class Rivets.ComponentBinding extends Rivets.Binding
|
|
# Initializes a component binding for the specified view. The raw component
|
|
# element is passed in along with the component type. Attributes and scope
|
|
# inflections are determined based on the components defined attributes.
|
|
constructor: (@view, @el, @type) ->
|
|
@component = @view.components[@type]
|
|
@static = {}
|
|
@observers = {}
|
|
@upstreamObservers = {}
|
|
|
|
bindingRegExp = view.bindingRegExp()
|
|
|
|
for attribute in @el.attributes or []
|
|
unless bindingRegExp.test attribute.name
|
|
propertyName = @camelCase attribute.name
|
|
|
|
token = Rivets.TypeParser.parse(attribute.value)
|
|
|
|
if propertyName in (@component.static ? [])
|
|
@static[propertyName] = attribute.value
|
|
else if token.type is Rivets.TypeParser.types.primitive
|
|
@static[propertyName] = token.value
|
|
else
|
|
@observers[propertyName] = attribute.value
|
|
|
|
# Intercepts `Rivets.Binding::sync` since component bindings are not bound to
|
|
# a particular model to update it's value.
|
|
sync: ->
|
|
|
|
# Intercepts `Rivets.Binding::update` since component bindings are not bound
|
|
# to a particular model to update it's value.
|
|
update: ->
|
|
|
|
# Intercepts `Rivets.Binding::publish` since component bindings are not bound
|
|
# to a particular model to update it's value.
|
|
publish: ->
|
|
|
|
# Returns an object map using the component's scope inflections.
|
|
locals: =>
|
|
result = {}
|
|
|
|
for key, value of @static
|
|
result[key] = value
|
|
|
|
for key, observer of @observers
|
|
result[key] = observer.value()
|
|
|
|
result
|
|
|
|
# Returns a camel-cased version of the string. Used when translating an
|
|
# element's attribute name into a property name for the component's scope.
|
|
camelCase: (string) ->
|
|
string.replace /-([a-z])/g, (grouped) ->
|
|
grouped[1].toUpperCase()
|
|
|
|
# Intercepts `Rivets.Binding::bind` to build `@componentView` with a localized
|
|
# map of models from the root view. Bind `@componentView` on subsequent calls.
|
|
bind: =>
|
|
unless @bound
|
|
for key, keypath of @observers
|
|
@observers[key] = @observe @view.models, keypath, ((key) => =>
|
|
@componentView.models[key] = @observers[key].value()
|
|
).call(@, key)
|
|
|
|
@bound = true
|
|
|
|
if @componentView?
|
|
@componentView.bind()
|
|
else
|
|
@el.innerHTML = @component.template.call this
|
|
scope = @component.initialize.call @, @el, @locals()
|
|
@el._bound = true
|
|
|
|
options = {}
|
|
|
|
for option in Rivets.extensions
|
|
options[option] = {}
|
|
options[option][k] = v for k, v of @component[option] if @component[option]
|
|
options[option][k] ?= v for k, v of @view[option]
|
|
|
|
for option in Rivets.options
|
|
options[option] = @component[option] ? @view[option]
|
|
|
|
@componentView = new Rivets.View(Array.prototype.slice.call(@el.childNodes), scope, options)
|
|
@componentView.bind()
|
|
|
|
for key, observer of @observers
|
|
@upstreamObservers[key] = @observe @componentView.models, key, ((key, observer) => =>
|
|
observer.setValue @componentView.models[key]
|
|
).call(@, key, observer)
|
|
return
|
|
|
|
# Intercept `Rivets.Binding::unbind` to be called on `@componentView`.
|
|
unbind: =>
|
|
for key, observer of @upstreamObservers
|
|
observer.unobserve()
|
|
|
|
for key, observer of @observers
|
|
observer.unobserve()
|
|
|
|
@componentView?.unbind.call @
|
|
|
|
# Rivets.TextBinding
|
|
# -----------------------
|
|
|
|
# A text node binding, defined internally to deal with text and element node
|
|
# differences while avoiding it being overwritten.
|
|
class Rivets.TextBinding extends Rivets.Binding
|
|
# Initializes a text binding for the specified view and text node.
|
|
constructor: (@view, @el, @type, @keypath, @options = {}) ->
|
|
@formatters = @options.formatters or []
|
|
@dependencies = []
|
|
@formatterObservers = {}
|
|
|
|
# A standard routine binder used for text node bindings.
|
|
binder:
|
|
routine: (node, value) ->
|
|
node.data = value ? ''
|
|
|
|
# Wrap the call to `sync` in fat-arrow to avoid function context issues.
|
|
sync: =>
|
|
super
|