203892c1 by Michael Richards

Merge pull request #186 from mikeric/components

Components
2 parents 3554d8f9 6c50fd2a
......@@ -155,6 +155,56 @@ class Rivets.Binding
@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 = {}
result[key] = models[inverse] for key, inverse of @inflections
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.View
# -----------
......@@ -178,13 +228,18 @@ class Rivets.View
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. Subsequent calls to build will replace the previous
# `Rivets.Binding` instances with new ones, so be sure to unbind them first.
# binding declaration.
build: =>
@bindings = []
skipNodes = []
bindingRegExp = @bindingRegExp()
componentRegExp = @componentRegExp()
buildBinding = (node, type, declaration) =>
options = {}
......@@ -208,7 +263,7 @@ class Rivets.View
options.dependencies = dependencies.split /\s+/
@bindings.push new Rivets.Binding @, node, type, key, keypath, options
parse = (node) =>
unless node in skipNodes
if node.nodeType is Node.TEXT_NODE
......@@ -227,7 +282,10 @@ class Rivets.View
for token in restTokens
node.parentNode.appendChild (text = document.createTextNode token.value)
buildBinding text, 'textNode', token.value if token.type is 1
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
......@@ -591,6 +649,14 @@ Rivets.internalBinders =
textNode: (node, value) ->
node.data = value ? ''
# 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.config
# -------------
......@@ -620,6 +686,9 @@ Rivets.factory = (exports) ->
# 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
......