203892c1 by Michael Richards

Merge pull request #186 from mikeric/components

Components
2 parents 3554d8f9 6c50fd2a
...@@ -155,6 +155,56 @@ class Rivets.Binding ...@@ -155,6 +155,56 @@ class Rivets.Binding
155 155
156 @binder.update?.call @, models 156 @binder.update?.call @, models
157 157
158 # Rivets.ComponentBinding
159 # -----------------------
160
161 # A component view encapsulated as a binding within it's parent view.
162 class Rivets.ComponentBinding extends Rivets.Binding
163 # Initializes a component binding for the specified view. The raw component
164 # element is passed in along with the component type. Attributes and scope
165 # inflections are determined based on the components defined attributes.
166 constructor: (@view, @el, @type) ->
167 @component = Rivets.components[@type]
168 @attributes = {}
169 @inflections = {}
170
171 for attribute in @el.attributes or []
172 if attribute.name in @component.attributes
173 @attributes[attribute.name] = attribute.value
174 else
175 @inflections[attribute.name] = attribute.value
176
177 # Intercepts `Rivets.Binding::sync` since component bindings are not bound to a
178 # particular model to update it's value.
179 sync: ->
180
181 # Returns an object map using the component's scope inflections.
182 locals: (models = @view.models) =>
183 result = {}
184 result[key] = models[inverse] for key, inverse of @inflections
185 result[key] ?= model for key, model of models
186
187 result
188
189 # Intercepts `Rivets.Binding::update` to be called on `@componentView` with a
190 # localized map of the models.
191 update: (models) =>
192 @componentView?.update @locals models
193
194 # Intercepts `Rivets.Binding::bind` to build `@componentView` with a localized
195 # map of models from the root view. Bind `@componentView` on subsequent calls.
196 bind: =>
197 if @componentView?
198 @componentView?.bind()
199 else
200 el = @component.build.call @attributes
201 (@componentView = new Rivets.View(el, @locals(), @view.options)).bind()
202 @el.parentNode.replaceChild el, @el
203
204 # Intercept `Rivets.Binding::unbind` to be called on `@componentView`.
205 unbind: =>
206 @componentView?.unbind()
207
158 # Rivets.View 208 # Rivets.View
159 # ----------- 209 # -----------
160 210
...@@ -178,13 +228,18 @@ class Rivets.View ...@@ -178,13 +228,18 @@ class Rivets.View
178 prefix = @config.prefix 228 prefix = @config.prefix
179 if prefix then new RegExp("^data-#{prefix}-") else /^data-/ 229 if prefix then new RegExp("^data-#{prefix}-") else /^data-/
180 230
231 # Regular expression used to match component nodes.
232 componentRegExp: =>
233 new RegExp "^#{@config.prefix?.toUpperCase() ? 'RV'}-"
234
181 # Parses the DOM tree and builds `Rivets.Binding` instances for every matched 235 # Parses the DOM tree and builds `Rivets.Binding` instances for every matched
182 # binding declaration. Subsequent calls to build will replace the previous 236 # binding declaration.
183 # `Rivets.Binding` instances with new ones, so be sure to unbind them first.
184 build: => 237 build: =>
185 @bindings = [] 238 @bindings = []
186 skipNodes = [] 239 skipNodes = []
187 bindingRegExp = @bindingRegExp() 240 bindingRegExp = @bindingRegExp()
241 componentRegExp = @componentRegExp()
242
188 243
189 buildBinding = (node, type, declaration) => 244 buildBinding = (node, type, declaration) =>
190 options = {} 245 options = {}
...@@ -208,7 +263,7 @@ class Rivets.View ...@@ -208,7 +263,7 @@ class Rivets.View
208 options.dependencies = dependencies.split /\s+/ 263 options.dependencies = dependencies.split /\s+/
209 264
210 @bindings.push new Rivets.Binding @, node, type, key, keypath, options 265 @bindings.push new Rivets.Binding @, node, type, key, keypath, options
211 266
212 parse = (node) => 267 parse = (node) =>
213 unless node in skipNodes 268 unless node in skipNodes
214 if node.nodeType is Node.TEXT_NODE 269 if node.nodeType is Node.TEXT_NODE
...@@ -227,7 +282,10 @@ class Rivets.View ...@@ -227,7 +282,10 @@ class Rivets.View
227 for token in restTokens 282 for token in restTokens
228 node.parentNode.appendChild (text = document.createTextNode token.value) 283 node.parentNode.appendChild (text = document.createTextNode token.value)
229 buildBinding text, 'textNode', token.value if token.type is 1 284 buildBinding text, 'textNode', token.value if token.type is 1
230 285 else if componentRegExp.test node.tagName
286 type = node.tagName.replace(componentRegExp, '').toLowerCase()
287 @bindings.push new Rivets.ComponentBinding @, node, type
288
231 else if node.attributes? 289 else if node.attributes?
232 for attribute in node.attributes 290 for attribute in node.attributes
233 if bindingRegExp.test attribute.name 291 if bindingRegExp.test attribute.name
...@@ -591,6 +649,14 @@ Rivets.internalBinders = ...@@ -591,6 +649,14 @@ Rivets.internalBinders =
591 textNode: (node, value) -> 649 textNode: (node, value) ->
592 node.data = value ? '' 650 node.data = value ? ''
593 651
652 # Rivets.components
653 # -----------------
654
655 # Default components (there aren't any), publicly accessible on
656 # `module.components`. Can be overridden globally or local to a `Rivets.View`
657 # instance.
658 Rivets.components = {}
659
594 # Rivets.config 660 # Rivets.config
595 # ------------- 661 # -------------
596 662
...@@ -620,6 +686,9 @@ Rivets.factory = (exports) -> ...@@ -620,6 +686,9 @@ Rivets.factory = (exports) ->
620 # Exposes the core binding routines that can be extended or stripped down. 686 # Exposes the core binding routines that can be extended or stripped down.
621 exports.binders = Rivets.binders 687 exports.binders = Rivets.binders
622 688
689 # Exposes the components object to be extended.
690 exports.components = Rivets.components
691
623 # Exposes the formatters object to be extended. 692 # Exposes the formatters object to be extended.
624 exports.formatters = Rivets.formatters 693 exports.formatters = Rivets.formatters
625 694
......