7cf13491 by Michael Richards

Merge pull request #240 from mikeric/full-keypath-observers

Full Keypath Observers
2 parents d82996d2 f9acdd9b
......@@ -20,7 +20,7 @@ module.exports = (grunt) ->
'src/view.coffee'
'src/bindings.coffee'
'src/parsers.coffee'
'src/keypath_observer.coffee'
'src/observer.coffee'
'src/binders.coffee'
'src/adapters.coffee'
'src/export.coffee'
......
......@@ -8,7 +8,7 @@ describe('Rivets.Binding', function() {
el = document.createElement('div');
el.setAttribute('data-text', 'obj.name');
view = rivets.bind(el, {obj: {}});
view = rivets.bind(el, {obj: {name: 'test'}});
binding = view.bindings[0];
model = binding.model;
});
......@@ -40,12 +40,10 @@ describe('Rivets.Binding', function() {
rivets.config.preloadData = true;
});
it('sets the initial value via the adapter', function() {
it('sets the initial value', function() {
spyOn(binding, 'set');
spyOn(adapter, 'read');
binding.bind();
expect(adapter.read).toHaveBeenCalledWith(model, 'name');
expect(binding.set).toHaveBeenCalled();
expect(binding.set).toHaveBeenCalledWith('test');
});
});
......
......@@ -9,8 +9,8 @@ class Rivets.Binding
constructor: (@view, @el, @type, @keypath, @options = {}) ->
@formatters = @options.formatters || []
@dependencies = []
@model = undefined
@setBinder()
@setObserver()
# Sets the binder to use when binding and syncing.
setBinder: =>
......@@ -26,18 +26,6 @@ class Rivets.Binding
@binder or= @view.binders['*']
@binder = {routine: @binder} if @binder instanceof Function
# Sets a keypath observer that will notify this binding when any intermediary
# keys are changed.
setObserver: =>
@observer = new Rivets.KeypathObserver @view, @view.models, @keypath, (obs) =>
@unbind true if @key
@model = obs.target
@bind true if @key
@sync()
@key = @observer.key
@model = @observer.target
# Applies all the current formatters to the supplied value and returns the
# formatted value.
formattedValue: (value) =>
......@@ -70,10 +58,16 @@ class Rivets.Binding
# Syncs up the view binding with the model.
sync: =>
@set if @key
@view.adapters[@key.interface].read @model, @key.path
else
@model
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 = new Rivets.Observer @view, @model, dependency, @sync
@dependencies.push observer
@set @observer.value()
# Publishes the value currently set on the input element back to the model.
publish: =>
......@@ -86,42 +80,30 @@ class Rivets.Binding
if @view.formatters[id]?.publish
value = @view.formatters[id].publish value, args...
@view.adapters[@key.interface].publish @model, @key.path, value
@observer.publish 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: (silent = false) =>
@binder.bind?.call @, @el unless silent
@view.adapters[@key.interface].subscribe(@model, @key.path, @sync) if @key
@sync() if @view.config.preloadData unless silent
bind: =>
@binder.bind?.call @, @el
@observer = new Rivets.Observer @view, @view.models, @keypath, @sync
@model = @observer.target
if @options.dependencies?.length
if @model? and @options.dependencies?.length
for dependency in @options.dependencies
observer = new Rivets.KeypathObserver @view, @model, dependency, (obs, prev) =>
key = obs.key
@view.adapters[key.interface].unsubscribe prev, key.path, @sync
@view.adapters[key.interface].subscribe obs.target, key.path, @sync
@sync()
key = observer.key
@view.adapters[key.interface].subscribe observer.target, key.path, @sync
observer = new Rivets.Observer @view, @model, dependency, @sync
@dependencies.push observer
# Unsubscribes from the model and the element.
unbind: (silent = false) =>
unless silent
@binder.unbind?.call @, @el
@observer.unobserve()
@view.adapters[@key.interface].unsubscribe(@model, @key.path, @sync) if @key
@sync() if @view.config.preloadData
if @dependencies.length
for obs in @dependencies
key = obs.key
@view.adapters[key.interface].unsubscribe obs.target, key.path, @sync
# Unsubscribes from the model and the element.
unbind: =>
@binder.unbind?.call @, @el
@observer.unobserve()
@dependencies = []
observer.unobserve() for observer in @dependencies
@dependencies = []
# 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.
......@@ -190,7 +172,6 @@ class Rivets.TextBinding extends Rivets.Binding
constructor: (@view, @el, @type, @keypath, @options = {}) ->
@formatters = @options.formatters || []
@dependencies = []
@setObserver()
# A standard routine binder used for text node bindings.
binder:
......
# Rivets.KeypathObserver
# Rivets.Observer
# ----------------------
# Parses and observes a full keypath with the appropriate adapters. Also
# intelligently re-realizes the keypath when intermediary keys change.
class Rivets.KeypathObserver
class Rivets.Observer
# Performs the initial parse, variable instantiation and keypath realization.
constructor: (@view, @model, @keypath, @callback) ->
@parse()
@objectPath = []
@target = @realize()
@initialize()
# Parses the keypath using the interfaces defined on the view. Sets variables
# for the tokenized keypath, as well as the end key.
......@@ -25,34 +24,67 @@ class Rivets.KeypathObserver
@tokens = Rivets.KeypathParser.parse path, interfaces, root
@key = @tokens.pop()
# Updates the keypath. This is called when any intermediate key is changed.
initialize: =>
@objectPath = []
@target = @realize()
@set true, @key, @target, @callback if @target?
# Updates the keypath. This is called when any intermediary key is changed.
update: =>
unless (next = @realize()) is @target
prev = @target
@set false, @key, @target, @callback if @target?
@set true, @key, next, @callback if next?
oldValue = @value()
@target = next
@callback @, prev
@callback() unless @value() is oldValue
adapter: (key) =>
@view.adapters[key.interface]
set: (active, key, obj, callback) =>
action = if active then 'subscribe' else 'unsubscribe'
@adapter(key)[action] obj, key.path, callback
read: (key, obj) =>
@adapter(key).read obj, key.path
publish: (value) =>
if @target?
@adapter(@key).publish @target, @key.path, value
value: =>
@read @key, @target if @target?
# Realizes the full keypath, attaching observers for every key and correcting
# old observers to any changed objects in the keypath.
realize: =>
current = @model
unreached = null
for token, index in @tokens
if @objectPath[index]?
if current isnt prev = @objectPath[index]
@view.adapters[token.interface].unsubscribe prev, token.path, @update
@view.adapters[token.interface].subscribe current, token.path, @update
if current?
if @objectPath[index]?
if current isnt prev = @objectPath[index]
@set false, token, prev, @update
@set true, token, current, @update
@objectPath[index] = current
else
@set true, token, current, @update
@objectPath[index] = current
current = @read token, current
else
@view.adapters[token.interface].subscribe current, token.path, @update
@objectPath[index] = current
unreached ?= index
current = @view.adapters[token.interface].read current, token.path
if prev = @objectPath[index]
@set false, token, prev, @update
@objectPath.splice unreached if unreached?
current
# Unobserves any current observers set up on the keys.
unobserve: =>
for token, index in @tokens
if obj = @objectPath[index]
@view.adapters[token.interface].unsubscribe obj, token.path, @update
@set false, token, obj, @update
......