fd7fcbfa by Michael Richards

Separate classes and the adapter / binder definitions into their own files.

1 parent 5988bc88
# The default `.` adapter thats comes with Rivets.js. Allows subscribing to
# properties on POJSOs, implemented in ES5 natives using
# `Object.defineProperty`.
Rivets.adapters['.'] =
id: '_rv'
counter: 0
weakmap: {}
subscribe: (obj, keypath, callback) ->
unless obj[@id]?
obj[@id] = @counter++
@weakmap[obj[@id]] = {}
map = @weakmap[obj[@id]]
unless map[keypath]?
map[keypath] = []
value = obj[keypath]
Object.defineProperty obj, keypath,
get: -> value
set: (newValue) ->
if newValue isnt value
value = newValue
callback() for callback in map[keypath]
unless callback in map[keypath]
map[keypath].push callback
unsubscribe: (obj, keypath, callback) ->
callbacks = @weakmap[obj[@id]][keypath]
callbacks.splice callbacks.indexOf(callback), 1
read: (obj, keypath) ->
obj[keypath]
publish: (obj, keypath, value) ->
obj[keypath] = value
# Core binders that are included with Rivets.js.
Rivets.binders.enabled = (el, value) ->
el.disabled = !value
Rivets.binders.disabled = (el, value) ->
el.disabled = !!value
Rivets.binders.checked =
publishes: true
bind: (el) ->
Rivets.Util.bindEvent el, 'change', @publish
unbind: (el) ->
Rivets.Util.unbindEvent el, 'change', @publish
routine: (el, value) ->
if el.type is 'radio'
el.checked = el.value?.toString() is value?.toString()
else
el.checked = !!value
Rivets.binders.unchecked =
publishes: true
bind: (el) ->
Rivets.Util.bindEvent el, 'change', @publish
unbind: (el) ->
Rivets.Util.unbindEvent el, 'change', @publish
routine: (el, value) ->
if el.type is 'radio'
el.checked = el.value?.toString() isnt value?.toString()
else
el.checked = !value
Rivets.binders.show = (el, value) ->
el.style.display = if value then '' else 'none'
Rivets.binders.hide = (el, value) ->
el.style.display = if value then 'none' else ''
Rivets.binders.html = (el, value) ->
el.innerHTML = if value? then value else ''
Rivets.binders.value =
publishes: true
bind: (el) ->
Rivets.Util.bindEvent el, 'change', @publish
unbind: (el) ->
Rivets.Util.unbindEvent el, 'change', @publish
routine: (el, value) ->
if window.jQuery?
el = jQuery el
if value?.toString() isnt el.val()?.toString()
el.val if value? then value else ''
else
if el.type is 'select-multiple'
o.selected = o.value in value for o in el if value?
else if value?.toString() isnt el.value?.toString()
el.value = if value? then value else ''
Rivets.binders.text = (el, value) ->
if el.innerText?
el.innerText = if value? then value else ''
else
el.textContent = if value? then value else ''
Rivets.binders.if =
block: true
bind: (el) ->
unless @marker?
attr = ['data', @view.config.prefix, @type].join('-').replace '--', '-'
declaration = el.getAttribute attr
@marker = document.createComment " rivets: #{@type} #{declaration} "
el.removeAttribute attr
el.parentNode.insertBefore @marker, el
el.parentNode.removeChild el
unbind: ->
@nested?.unbind()
routine: (el, value) ->
if !!value is not @nested?
if value
models = {}
models[key] = model for key, model of @view.models
options =
binders: @view.options.binders
formatters: @view.options.formatters
adapters: @view.options.adapters
config: @view.options.config
(@nested = new Rivets.View(el, models, options)).bind()
@marker.parentNode.insertBefore el, @marker.nextSibling
else
el.parentNode.removeChild el
@nested.unbind()
delete @nested
update: (models) ->
@nested?.update models
Rivets.binders.unless =
block: true
bind: (el) ->
Rivets.binders.if.bind.call @, el
unbind: ->
Rivets.binders.if.unbind.call @
routine: (el, value) ->
Rivets.binders.if.routine.call @, el, not value
update: (models) ->
Rivets.binders.if.update.call @, models
Rivets.binders['on-*'] =
function: true
unbind: (el) ->
Rivets.Util.unbindEvent el, @args[0], @handler if @handler
routine: (el, value) ->
Rivets.Util.unbindEvent el, @args[0], @handler if @handler
Rivets.Util.bindEvent el, @args[0], @handler = @eventHandler value
Rivets.binders['each-*'] =
block: true
bind: (el) ->
unless @marker?
attr = ['data', @view.config.prefix, @type].join('-').replace '--', '-'
@marker = document.createComment " rivets: #{@type} "
@iterated = []
el.removeAttribute attr
el.parentNode.insertBefore @marker, el
el.parentNode.removeChild el
unbind: (el) ->
view.unbind() for view in @iterated if @iterated?
routine: (el, collection) ->
modelName = @args[0]
collection = collection or []
if @iterated.length > collection.length
for i in Array @iterated.length - collection.length
view = @iterated.pop()
view.unbind()
@marker.parentNode.removeChild view.els[0]
for model, index in collection
data = {}
data[modelName] = model
if not @iterated[index]?
for key, model of @view.models
data[key] ?= model
previous = if @iterated.length
@iterated[@iterated.length - 1].els[0]
else
@marker
options =
binders: @view.options.binders
formatters: @view.options.formatters
adapters: @view.options.adapters
config: {}
options.config[k] = v for k, v of @view.options.config
options.config.preloadData = true
template = el.cloneNode true
view = new Rivets.View(template, data, options)
view.bind()
@iterated.push view
@marker.parentNode.insertBefore template, previous.nextSibling
else if @iterated[index].models[modelName] isnt model
@iterated[index].update data
update: (models) ->
data = {}
for key, model of models
data[key] = model unless key is @args[0]
view.update data for view in @iterated
Rivets.binders['class-*'] = (el, value) ->
elClass = " #{el.className} "
if !value is (elClass.indexOf(" #{@args[0]} ") isnt -1)
el.className = if value
"#{el.className} #{@args[0]}"
else
elClass.replace(" #{@args[0]} ", ' ').trim()
Rivets.binders['*'] = (el, value) ->
if value
el.setAttribute @type, value
else
el.removeAttribute @type
# 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 || []
@setBinders()
@setModel()
setBinders: =>
unless @binder = @view.binders[@type]
for identifier, value of @view.binders
if identifier isnt '*' and identifier.indexOf('*') isnt -1
regexp = new RegExp "^#{identifier.replace('*', '.+')}$"
if regexp.test @type
@binder = value
@args = new RegExp("^#{identifier.replace('*', '(.+)')}$").exec @type
@args.shift()
@binder or= @view.binders['*']
@binder = {routine: @binder} if @binder instanceof Function
setModel: =>
interfaces = (k for k, v of @view.adapters)
tokens = Rivets.KeypathParser.parse @keypath, interfaces, '.'
@model = @view.models
@rootKey = tokens[0]
@key = tokens.pop()
for token, index in tokens
@model = @view.adapters[token.interface].read(@model, token.path)
# Applies all the current formatters to the supplied value and returns the
# formatted value.
formattedValue: (value) =>
for formatter in @formatters
args = formatter.split /\s+/
id = args.shift()
formatter = if @model[id] instanceof Function
@model[id]
else
@view.formatters[id]
if formatter?.read instanceof Function
value = formatter.read value, args...
else if formatter instanceof Function
value = formatter value, args...
value
# Returns an event handler for the binding around the supplied function.
eventHandler: (fn) =>
handler = (binding = @).view.config.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) =>
value = if value instanceof Function and !@binder.function
@formattedValue value.call @model
else
@formattedValue value
@binder.routine?.call @, @el, value
# Syncs up the view binding with the model.
sync: =>
@set @view.adapters[@key.interface].read @model, @key.path
# Publishes the value currently set on the input element back to the model.
publish: =>
value = Rivets.Util.getInputValue @el
for formatter in @formatters.slice(0).reverse()
args = formatter.split /\s+/
id = args.shift()
if @view.formatters[id]?.publish
value = @view.formatters[id].publish value, args...
@view.adapters[@key.interface].publish @model, @key.path, 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: =>
@binder.bind?.call @, @el
@view.adapters[@key.interface].subscribe @model, @key.path, @sync
@sync() if @view.config.preloadData
if @options.dependencies?.length
for dependency in @options.dependencies
if /^\./.test dependency
model = @model
keypath = dependency.substr 1
else
dependency = dependency.split '.'
model = @view.models[dependency.shift()]
keypath = dependency.join '.'
@view.adapters['.'].subscribe model, keypath, @sync
# Unsubscribes from the model and the element.
unbind: =>
@binder.unbind?.call @, @el
@view.adapters[@key.interface].unsubscribe @model, @key.path, @sync
if @options.dependencies?.length
for dependency in @options.dependencies
if /^\./.test dependency
model = @model
keypath = dependency.substr 1
else
dependency = dependency.split '.'
model = @view.models[dependency.shift()]
keypath = dependency.join '.'
@view.adapters['.'].unsubscribe model, keypath, @sync
# 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 = {}) =>
if models[@rootKey.path]
@view.adapters[@key.interface].unsubscribe @model, @key.path, @sync
@setModel()
@view.adapters[@key.interface].subscribe @model, @key.path, @sync
@sync()
@binder.update?.call @, models
# 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 = Rivets.components[@type]
@attributes = {}
@inflections = {}
for attribute in @el.attributes or []
if attribute.name in @component.attributes
@attributes[attribute.name] = attribute.value
else
@inflections[attribute.name] = attribute.value
# Intercepts `Rivets.Binding::sync` since component bindings are not bound to
# a particular model to update it's value.
sync: ->
# Returns an object map using the component's scope inflections.
locals: (models = @view.models) =>
result = {}
for key, inverse of @inflections
result[key] = (result[key] or models)[path] for path in inverse.split '.'
result[key] ?= model for key, model of models
result
# Intercepts `Rivets.Binding::update` to be called on `@componentView` with a
# localized map of the models.
update: (models) =>
@componentView?.update @locals models
# Intercepts `Rivets.Binding::bind` to build `@componentView` with a localized
# map of models from the root view. Bind `@componentView` on subsequent calls.
bind: =>
if @componentView?
@componentView?.bind()
else
el = @component.build.call @attributes
(@componentView = new Rivets.View(el, @locals(), @view.options)).bind()
@el.parentNode.replaceChild el, @el
# Intercept `Rivets.Binding::unbind` to be called on `@componentView`.
unbind: =>
@componentView?.unbind()
# 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 = {}) ->
interfaces = (k for k, v of @view.adapters)
tokens = Rivets.KeypathParser.parse(@keypath, interfaces, '.')
@formatters = @options.formatters || []
@setModel()
# 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
# Rivets.factory
# --------------
# Rivets.js module factory.
Rivets.factory = (exports) ->
# Exposes the full Rivets namespace. This is mainly used for isolated testing.
exports._ = Rivets
# Exposes the binders object.
exports.binders = Rivets.binders
# Exposes the components object.
exports.components = Rivets.components
# Exposes the formatters object.
exports.formatters = Rivets.formatters
# Exposes the adapters object.
exports.adapters = Rivets.adapters
# Exposes the config object.
exports.config = Rivets.config
# Merges an object literal onto the config object.
exports.configure = (options={}) ->
for property, value of options
Rivets.config[property] = value
return
# Binds a set of model objects to a parent DOM element and returns a
# `Rivets.View` instance.
exports.bind = (el, models = {}, options = {}) ->
view = new Rivets.View(el, models, options)
view.bind()
view
# Exports Rivets.js for CommonJS, AMD and the browser.
if typeof exports == 'object'
Rivets.factory(exports)
else if typeof define == 'function' && define.amd
define ['exports'], (exports) ->
Rivets.factory(@rivets = exports)
return exports
else
Rivets.factory(@rivets = {})
# Rivets.KeypathParser
# --------------------
# Parser and tokenizer for keypaths in binding declarations.
class Rivets.KeypathParser
# Parses the keypath and returns a set of adapter interface + path tokens.
@parse: (keypath, interfaces, root) ->
tokens = []
current = {interface: root, path: ''}
for index, char of keypath
if char in interfaces
tokens.push current
current = {interface: char, path: ''}
else
current.path += char
tokens.push current
tokens
# Rivets.TextTemplateParser
# -------------------------
# Rivets.js text template parser and tokenizer for mustache-style text content
# binding declarations.
class Rivets.TextTemplateParser
@types:
text: 0
binding: 1
# Parses the template and returns a set of tokens, separating static portions
# of text from binding declarations.
@parse: (template, delimiters) ->
tokens = []
length = template.length
index = 0
lastIndex = 0
while lastIndex < length
index = template.indexOf delimiters[0], lastIndex
if index < 0
tokens.push type: @types.text, value: template.slice lastIndex
break
else
if index > 0 and lastIndex < index
tokens.push type: @types.text, value: template.slice lastIndex, index
lastIndex = index + 2
index = template.indexOf delimiters[1], lastIndex
if index < 0
substring = template.slice lastIndex - 2
lastToken = tokens[tokens.length - 1]
if lastToken?.type is @types.text
lastToken.value += substring
else
tokens.push type: @types.text, value: substring
break
value = template.slice(lastIndex, index).trim()
tokens.push type: @types.binding, value: value
lastIndex = index + 2
tokens
# Rivets.js
# =========
# > version: 0.5.12
# > author: Michael Richards
# > license: MIT
# >
# > http://rivetsjs.com/
# ---
# The Rivets namespace.
Rivets = {}
# Polyfill For `String::trim`.
unless String::trim then String::trim = -> @replace /^\s+|\s+$/g, ''
# 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 || []
@setBinders()
@setModel()
setBinders: =>
unless @binder = @view.binders[type]
for identifier, value of @view.binders
if identifier isnt '*' and identifier.indexOf('*') isnt -1
regexp = new RegExp "^#{identifier.replace('*', '.+')}$"
if regexp.test type
@binder = value
@args = new RegExp("^#{identifier.replace('*', '(.+)')}$").exec type
@args.shift()
@binder or= @view.binders['*']
@binder = {routine: @binder} if @binder instanceof Function
setModel: =>
interfaces = (k for k, v of @view.adapters)
tokens = Rivets.KeypathParser.parse @keypath, interfaces, '.'
@model = @view.models
@rootKey = tokens[0]
@key = tokens.pop()
for token, index in tokens
@model = @view.adapters[token.interface].read(@model, token.path)
# Applies all the current formatters to the supplied value and returns the
# formatted value.
formattedValue: (value) =>
for formatter in @formatters
args = formatter.split /\s+/
id = args.shift()
formatter = if @model[id] instanceof Function
@model[id]
else
@view.formatters[id]
if formatter?.read instanceof Function
value = formatter.read value, args...
else if formatter instanceof Function
value = formatter value, args...
value
# Returns an event handler for the binding around the supplied function.
eventHandler: (fn) =>
handler = (binding = @).view.config.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) =>
value = if value instanceof Function and !@binder.function
@formattedValue value.call @model
else
@formattedValue value
@binder.routine?.call @, @el, value
# Syncs up the view binding with the model.
sync: =>
@set @view.adapters[@key.interface].read @model, @key.path
# Publishes the value currently set on the input element back to the model.
publish: =>
value = Rivets.Util.getInputValue @el
for formatter in @formatters.slice(0).reverse()
args = formatter.split /\s+/
id = args.shift()
if @view.formatters[id]?.publish
value = @view.formatters[id].publish value, args...
@view.adapters[@key.interface].publish @model, @key.path, 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: =>
@binder.bind?.call @, @el
@view.adapters[@key.interface].subscribe @model, @key.path, @sync
@sync() if @view.config.preloadData
if @options.dependencies?.length
for dependency in @options.dependencies
if /^\./.test dependency
model = @model
keypath = dependency.substr 1
else
dependency = dependency.split '.'
model = @view.models[dependency.shift()]
keypath = dependency.join '.'
@view.adapters['.'].subscribe model, keypath, @sync
# Unsubscribes from the model and the element.
unbind: =>
@binder.unbind?.call @, @el
@view.adapters[@key.interface].unsubscribe @model, @key.path, @sync
if @options.dependencies?.length
for dependency in @options.dependencies
if /^\./.test dependency
model = @model
keypath = dependency.substr 1
else
dependency = dependency.split '.'
model = @view.models[dependency.shift()]
keypath = dependency.join '.'
@view.adapters['.'].unsubscribe model, keypath, @sync
# 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 = {}) =>
if models[@rootKey.path]
@view.adapters[@key.interface].unsubscribe @model, @key.path, @sync
@setModel()
@view.adapters[@key.interface].subscribe @model, @key.path, @sync
@sync()
@binder.update?.call @, models
# 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 = Rivets.components[@type]
@attributes = {}
@inflections = {}
for attribute in @el.attributes or []
if attribute.name in @component.attributes
@attributes[attribute.name] = attribute.value
else
@inflections[attribute.name] = attribute.value
# Intercepts `Rivets.Binding::sync` since component bindings are not bound to
# a particular model to update it's value.
sync: ->
# Returns an object map using the component's scope inflections.
locals: (models = @view.models) =>
result = {}
for key, inverse of @inflections
result[key] = (result[key] or models)[path] for path in inverse.split '.'
result[key] ?= model for key, model of models
result
# Intercepts `Rivets.Binding::update` to be called on `@componentView` with a
# localized map of the models.
update: (models) =>
@componentView?.update @locals models
# Intercepts `Rivets.Binding::bind` to build `@componentView` with a localized
# map of models from the root view. Bind `@componentView` on subsequent calls.
bind: =>
if @componentView?
@componentView?.bind()
else
el = @component.build.call @attributes
(@componentView = new Rivets.View(el, @locals(), @view.options)).bind()
@el.parentNode.replaceChild el, @el
# Intercept `Rivets.Binding::unbind` to be called on `@componentView`.
unbind: =>
@componentView?.unbind()
# 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 = {}) ->
interfaces = (k for k, v of @view.adapters)
tokens = Rivets.KeypathParser.parse(@keypath, interfaces, '.')
@formatters = @options.formatters || []
@setModel()
# 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
# Rivets.View
# -----------
# A collection of bindings built from a set of parent nodes.
class Rivets.View
# The DOM elements and the model objects for binding are passed into the
# constructor along with any local options that should be used throughout the
# context of the view and it's bindings.
constructor: (@els, @models, @options = {}) ->
@els = [@els] unless (@els.jquery || @els instanceof Array)
for option in ['config', 'binders', 'formatters', 'adapters']
@[option] = {}
@[option][k] = v for k, v of @options[option] if @options[option]
@[option][k] ?= v for k, v of Rivets[option]
@build()
# Regular expression used to match binding attributes.
bindingRegExp: =>
prefix = @config.prefix
if prefix then new RegExp("^data-#{prefix}-") else /^data-/
# Regular expression used to match component nodes.
componentRegExp: =>
new RegExp "^#{@config.prefix?.toUpperCase() ? 'RV'}-"
# Parses the DOM tree and builds `Rivets.Binding` instances for every matched
# binding declaration.
build: =>
@bindings = []
skipNodes = []
bindingRegExp = @bindingRegExp()
componentRegExp = @componentRegExp()
buildBinding = (binding, node, type, declaration) =>
options = {}
pipes = (pipe.trim() for pipe in declaration.split '|')
context = (ctx.trim() for ctx in pipes.shift().split '<')
keypath = context.shift()
options.formatters = pipes
if dependencies = context.shift()
options.dependencies = dependencies.split /\s+/
@bindings.push new Rivets[binding] @, node, type, keypath, options
parse = (node) =>
unless node in skipNodes
if node.nodeType is Node.TEXT_NODE
parser = Rivets.TextTemplateParser
if delimiters = @config.templateDelimiters
if (tokens = parser.parse(node.data, delimiters)).length
unless tokens.length is 1 and tokens[0].type is parser.types.text
[startToken, restTokens...] = tokens
node.data = startToken.value
if startToken.type is 0
node.data = startToken.value
else
buildBinding 'TextBinding', node, null, startToken.value
for token in restTokens
text = document.createTextNode token.value
node.parentNode.appendChild text
if token.type is 1
buildBinding 'TextBinding', text, null, token.value
else if componentRegExp.test node.tagName
type = node.tagName.replace(componentRegExp, '').toLowerCase()
@bindings.push new Rivets.ComponentBinding @, node, type
else if node.attributes?
for attribute in node.attributes
if bindingRegExp.test attribute.name
type = attribute.name.replace bindingRegExp, ''
unless binder = @binders[type]
for identifier, value of @binders
if identifier isnt '*' and identifier.indexOf('*') isnt -1
regexp = new RegExp "^#{identifier.replace('*', '.+')}$"
if regexp.test type
binder = value
binder or= @binders['*']
if binder.block
skipNodes.push n for n in node.childNodes
attributes = [attribute]
for attribute in attributes or node.attributes
if bindingRegExp.test attribute.name
type = attribute.name.replace bindingRegExp, ''
buildBinding 'Binding', node, type, attribute.value
parse childNode for childNode in node.childNodes
parse el for el in @els
return
# Returns an array of bindings where the supplied function evaluates to true.
select: (fn) =>
binding for binding in @bindings when fn binding
# Binds all of the current bindings for this view.
bind: =>
binding.bind() for binding in @bindings
# Unbinds all of the current bindings for this view.
unbind: =>
binding.unbind() for binding in @bindings
# Syncs up the view with the model by running the routines on all bindings.
sync: =>
binding.sync() for binding in @bindings
# Publishes the input values from the view back to the model (reverse sync).
publish: =>
binding.publish() for binding in @select (b) -> b.binder.publishes
# Updates the view's models along with any affected bindings.
update: (models = {}) =>
@models[key] = model for key, model of models
binding.update models for binding in @bindings
# Rivets.KeypathParser
# --------------------
# Parser and tokenizer for keypaths in binding declarations.
class Rivets.KeypathParser
# Parses the keypath and returns a set of adapter interface + path tokens.
@parse: (keypath, interfaces, root) ->
tokens = []
current = {interface: root, path: ''}
for index, char of keypath
if char in interfaces
tokens.push current
current = {interface: char, path: ''}
else
current.path += char
tokens.push current
tokens
# Rivets.TextTemplateParser
# -------------------------
# Rivets.js text template parser and tokenizer for mustache-style text content
# binding declarations.
class Rivets.TextTemplateParser
@types:
text: 0
binding: 1
# Parses the template and returns a set of tokens, separating static portions
# of text from binding declarations.
@parse: (template, delimiters) ->
tokens = []
length = template.length
index = 0
lastIndex = 0
while lastIndex < length
index = template.indexOf delimiters[0], lastIndex
if index < 0
tokens.push type: @types.text, value: template.slice lastIndex
break
else
if index > 0 and lastIndex < index
tokens.push type: @types.text, value: template.slice lastIndex, index
lastIndex = index + 2
index = template.indexOf delimiters[1], lastIndex
if index < 0
substring = template.slice lastIndex - 2
lastToken = tokens[tokens.length - 1]
if lastToken?.type is @types.text
lastToken.value += substring
else
tokens.push type: @types.text, value: substring
break
value = template.slice(lastIndex, index).trim()
tokens.push type: @types.binding, value: value
lastIndex = index + 2
tokens
# Rivets.Util
# -----------
# Houses common utility functions used internally by Rivets.js.
Rivets.Util =
# Create a single DOM event binding.
bindEvent: (el, event, handler) ->
if window.jQuery?
el = jQuery el
if el.on? then el.on event, handler else el.bind event, handler
else if window.addEventListener?
el.addEventListener event, handler, false
else
event = 'on' + event
el.attachEvent event, handler
# Remove a single DOM event binding.
unbindEvent: (el, event, handler) ->
if window.jQuery?
el = jQuery el
if el.off? then el.off event, handler else el.unbind event, handler
else if window.removeEventListener?
el.removeEventListener event, handler, false
else
event = 'on' + event
el.detachEvent event, handler
# Get the current value of an input node.
getInputValue: (el) ->
if window.jQuery?
el = jQuery el
switch el[0].type
when 'checkbox' then el.is ':checked'
else el.val()
else
switch el.type
when 'checkbox' then el.checked
when 'select-multiple' then o.value for o in el when o.selected
else el.value
# Rivets.binders
# --------------
# Core binders that are included with Rivets.js, publicly available on
# `module.binders`. Can be overridden globally or local to a `Rivets.View`
# instance.
Rivets.binders =
enabled: (el, value) ->
el.disabled = !value
disabled: (el, value) ->
el.disabled = !!value
checked:
publishes: true
bind: (el) ->
Rivets.Util.bindEvent el, 'change', @publish
unbind: (el) ->
Rivets.Util.unbindEvent el, 'change', @publish
routine: (el, value) ->
if el.type is 'radio'
el.checked = el.value?.toString() is value?.toString()
else
el.checked = !!value
unchecked:
publishes: true
bind: (el) ->
Rivets.Util.bindEvent el, 'change', @publish
unbind: (el) ->
Rivets.Util.unbindEvent el, 'change', @publish
routine: (el, value) ->
if el.type is 'radio'
el.checked = el.value?.toString() isnt value?.toString()
else
el.checked = !value
show: (el, value) ->
el.style.display = if value then '' else 'none'
hide: (el, value) ->
el.style.display = if value then 'none' else ''
html: (el, value) ->
el.innerHTML = if value? then value else ''
value:
publishes: true
bind: (el) ->
Rivets.Util.bindEvent el, 'change', @publish
unbind: (el) ->
Rivets.Util.unbindEvent el, 'change', @publish
routine: (el, value) ->
if window.jQuery?
el = jQuery el
if value?.toString() isnt el.val()?.toString()
el.val if value? then value else ''
else
if el.type is 'select-multiple'
o.selected = o.value in value for o in el if value?
else if value?.toString() isnt el.value?.toString()
el.value = if value? then value else ''
text: (el, value) ->
if el.innerText?
el.innerText = if value? then value else ''
else
el.textContent = if value? then value else ''
if:
block: true
bind: (el) ->
unless @marker?
attr = ['data', @view.config.prefix, @type].join('-').replace '--', '-'
declaration = el.getAttribute attr
@marker = document.createComment " rivets: #{@type} #{declaration} "
el.removeAttribute attr
el.parentNode.insertBefore @marker, el
el.parentNode.removeChild el
unbind: ->
@nested?.unbind()
routine: (el, value) ->
if !!value is not @nested?
if value
models = {}
models[key] = model for key, model of @view.models
options =
binders: @view.options.binders
formatters: @view.options.formatters
adapters: @view.options.adapters
config: @view.options.config
(@nested = new Rivets.View(el, models, options)).bind()
@marker.parentNode.insertBefore el, @marker.nextSibling
else
el.parentNode.removeChild el
@nested.unbind()
delete @nested
update: (models) ->
@nested?.update models
unless:
block: true
bind: (el) ->
Rivets.binders.if.bind.call @, el
unbind: ->
Rivets.binders.if.unbind.call @
routine: (el, value) ->
Rivets.binders.if.routine.call @, el, not value
update: (models) ->
Rivets.binders.if.update.call @, models
"on-*":
function: true
unbind: (el) ->
Rivets.Util.unbindEvent el, @args[0], @handler if @handler
routine: (el, value) ->
Rivets.Util.unbindEvent el, @args[0], @handler if @handler
Rivets.Util.bindEvent el, @args[0], @handler = @eventHandler value
"each-*":
block: true
bind: (el) ->
unless @marker?
attr = ['data', @view.config.prefix, @type].join('-').replace '--', '-'
@marker = document.createComment " rivets: #{@type} "
@iterated = []
el.removeAttribute attr
el.parentNode.insertBefore @marker, el
el.parentNode.removeChild el
unbind: (el) ->
view.unbind() for view in @iterated if @iterated?
routine: (el, collection) ->
modelName = @args[0]
collection = collection or []
if @iterated.length > collection.length
for i in Array @iterated.length - collection.length
view = @iterated.pop()
view.unbind()
@marker.parentNode.removeChild view.els[0]
for model, index in collection
data = {}
data[modelName] = model
if not @iterated[index]?
for key, model of @view.models
data[key] ?= model
previous = if @iterated.length
@iterated[@iterated.length - 1].els[0]
else
@marker
options =
binders: @view.options.binders
formatters: @view.options.formatters
adapters: @view.options.adapters
config: {}
options.config[k] = v for k, v of @view.options.config
options.config.preloadData = true
template = el.cloneNode true
view = new Rivets.View(template, data, options)
view.bind()
@iterated.push view
@marker.parentNode.insertBefore template, previous.nextSibling
else if @iterated[index].models[modelName] isnt model
@iterated[index].update data
update: (models) ->
data = {}
for key, model of models
data[key] = model unless key is @args[0]
view.update data for view in @iterated
"class-*": (el, value) ->
elClass = " #{el.className} "
if !value is (elClass.indexOf(" #{@args[0]} ") isnt -1)
el.className = if value
"#{el.className} #{@args[0]}"
else
elClass.replace(" #{@args[0]} ", ' ').trim()
"*": (el, value) ->
if value
el.setAttribute @type, value
else
el.removeAttribute @type
# Rivets.components
# -----------------
# Default components (there aren't any), publicly accessible on
# `module.components`. Can be overridden globally or local to a `Rivets.View`
# instance.
Rivets.components = {}
# Rivets.formatters
# -----------------
# Default formatters (there aren't any), publicly accessible on
# `module.formatters`. Can be overridden globally or local to a `Rivets.View`
# instance.
Rivets.formatters = {}
# Rivets.adapters
# -----------------
# Default adapters (`.` for POJSO in ES5 natives), publicly accessible on
# `module.adapters`. Can be overridden globally or local to a `Rivets.View`
# instance.
Rivets.adapters =
'.':
id: '_rv'
counter: 0
weakmap: {}
subscribe: (obj, keypath, callback) ->
unless obj[@id]?
obj[@id] = @counter++
@weakmap[obj[@id]] = {}
map = @weakmap[obj[@id]]
unless map[keypath]?
map[keypath] = []
value = obj[keypath]
Object.defineProperty obj, keypath,
get: -> value
set: (newValue) ->
if newValue isnt value
value = newValue
callback() for callback in map[keypath]
unless callback in map[keypath]
map[keypath].push callback
unsubscribe: (obj, keypath, callback) ->
callbacks = @weakmap[obj[@id]][keypath]
callbacks.splice callbacks.indexOf(callback), 1
read: (obj, keypath) ->
obj[keypath]
publish: (obj, keypath, value) ->
obj[keypath] = value
# Rivets.config
# -------------
# Default configuration, publicly accessible on `module.config`. Can be
# overridden globally or local to a `Rivets.View` instance.
Rivets.config =
Rivets =
# Binder definitions, publicly accessible on `module.binders`. Can be
# overridden globally or local to a `Rivets.View` instance.
binders: {}
# Component definitions, publicly accessible on `module.components`. Can be
# overridden globally or local to a `Rivets.View` instance.
components: {}
# Formatter definitions, publicly accessible on `module.formatters`. Can be
# overridden globally or local to a `Rivets.View` instance.
formatters: {}
# Adapter definitions, publicly accessible on `module.adapters`. Can be
# overridden globally or local to a `Rivets.View` instance.
adapters: {}
# The default configuration, publicly accessible on `module.config`. Can be
# overridden globally or local to a `Rivets.View` instance.
config:
preloadData: true
handler: (context, ev, binding) ->
@call context, ev, binding.view.models
# Rivets.factory
# --------------
# The Rivets.js module factory.
Rivets.factory = (exports) ->
# Exposes the full Rivets namespace. This is mainly used for isolated testing.
exports._ = Rivets
# Exposes the core binding routines that can be extended or stripped down.
exports.binders = Rivets.binders
# Exposes the components object to be extended.
exports.components = Rivets.components
# Exposes the formatters object to be extended.
exports.formatters = Rivets.formatters
# Exposes the adapters object to be extended.
exports.adapters = Rivets.adapters
# Exposes the rivets configuration options. These can be set manually or from
# rivets.configure with an object literal.
exports.config = Rivets.config
# Sets configuration options by merging an object literal.
exports.configure = (options={}) ->
for property, value of options
Rivets.config[property] = value
return
# Binds a set of model objects to a parent DOM element and returns a
# `Rivets.View` instance.
exports.bind = (el, models = {}, options = {}) ->
view = new Rivets.View(el, models, options)
view.bind()
view
# Export
# ------
# Exports Rivets.js for CommonJS, AMD and the browser.
if typeof exports == 'object'
Rivets.factory(exports)
else if typeof define == 'function' && define.amd
define ['exports'], (exports) ->
Rivets.factory(@rivets = exports)
return exports
else
Rivets.factory(@rivets = {})
......
# Rivets.Util
# -----------
# Houses common utility functions used internally by Rivets.js.
Rivets.Util =
# Create a single DOM event binding.
bindEvent: (el, event, handler) ->
if window.jQuery?
el = jQuery el
if el.on? then el.on event, handler else el.bind event, handler
else if window.addEventListener?
el.addEventListener event, handler, false
else
event = 'on' + event
el.attachEvent event, handler
# Remove a single DOM event binding.
unbindEvent: (el, event, handler) ->
if window.jQuery?
el = jQuery el
if el.off? then el.off event, handler else el.unbind event, handler
else if window.removeEventListener?
el.removeEventListener event, handler, false
else
event = 'on' + event
el.detachEvent event, handler
# Get the current value of an input node.
getInputValue: (el) ->
if window.jQuery?
el = jQuery el
switch el[0].type
when 'checkbox' then el.is ':checked'
else el.val()
else
switch el.type
when 'checkbox' then el.checked
when 'select-multiple' then o.value for o in el when o.selected
else el.value
# Rivets.View
# -----------
# A collection of bindings built from a set of parent nodes.
class Rivets.View
# The DOM elements and the model objects for binding are passed into the
# constructor along with any local options that should be used throughout the
# context of the view and it's bindings.
constructor: (@els, @models, @options = {}) ->
@els = [@els] unless (@els.jquery || @els instanceof Array)
for option in ['config', 'binders', 'formatters', 'adapters']
@[option] = {}
@[option][k] = v for k, v of @options[option] if @options[option]
@[option][k] ?= v for k, v of Rivets[option]
@build()
# Regular expression used to match binding attributes.
bindingRegExp: =>
prefix = @config.prefix
if prefix then new RegExp("^data-#{prefix}-") else /^data-/
# Regular expression used to match component nodes.
componentRegExp: =>
new RegExp "^#{@config.prefix?.toUpperCase() ? 'RV'}-"
# Parses the DOM tree and builds `Rivets.Binding` instances for every matched
# binding declaration.
build: =>
@bindings = []
skipNodes = []
bindingRegExp = @bindingRegExp()
componentRegExp = @componentRegExp()
buildBinding = (binding, node, type, declaration) =>
options = {}
pipes = (pipe.trim() for pipe in declaration.split '|')
context = (ctx.trim() for ctx in pipes.shift().split '<')
keypath = context.shift()
options.formatters = pipes
if dependencies = context.shift()
options.dependencies = dependencies.split /\s+/
@bindings.push new Rivets[binding] @, node, type, keypath, options
parse = (node) =>
unless node in skipNodes
if node.nodeType is Node.TEXT_NODE
parser = Rivets.TextTemplateParser
if delimiters = @config.templateDelimiters
if (tokens = parser.parse(node.data, delimiters)).length
unless tokens.length is 1 and tokens[0].type is parser.types.text
[startToken, restTokens...] = tokens
node.data = startToken.value
if startToken.type is 0
node.data = startToken.value
else
buildBinding 'TextBinding', node, null, startToken.value
for token in restTokens
text = document.createTextNode token.value
node.parentNode.appendChild text
if token.type is 1
buildBinding 'TextBinding', text, null, token.value
else if componentRegExp.test node.tagName
type = node.tagName.replace(componentRegExp, '').toLowerCase()
@bindings.push new Rivets.ComponentBinding @, node, type
else if node.attributes?
for attribute in node.attributes
if bindingRegExp.test attribute.name
type = attribute.name.replace bindingRegExp, ''
unless binder = @binders[type]
for identifier, value of @binders
if identifier isnt '*' and identifier.indexOf('*') isnt -1
regexp = new RegExp "^#{identifier.replace('*', '.+')}$"
if regexp.test type
binder = value
binder or= @binders['*']
if binder.block
skipNodes.push n for n in node.childNodes
attributes = [attribute]
for attribute in attributes or node.attributes
if bindingRegExp.test attribute.name
type = attribute.name.replace bindingRegExp, ''
buildBinding 'Binding', node, type, attribute.value
parse childNode for childNode in node.childNodes
parse el for el in @els
return
# Returns an array of bindings where the supplied function evaluates to true.
select: (fn) =>
binding for binding in @bindings when fn binding
# Binds all of the current bindings for this view.
bind: =>
binding.bind() for binding in @bindings
# Unbinds all of the current bindings for this view.
unbind: =>
binding.unbind() for binding in @bindings
# Syncs up the view with the model by running the routines on all bindings.
sync: =>
binding.sync() for binding in @bindings
# Publishes the input values from the view back to the model (reverse sync).
publish: =>
binding.publish() for binding in @select (b) -> b.binder.publishes
# Updates the view's models along with any affected bindings.
update: (models = {}) =>
@models[key] = model for key, model of models
binding.update models for binding in @bindings