Merge pull request #240 from mikeric/full-keypath-observers
Full Keypath Observers
Showing
4 changed files
with
76 additions
and
65 deletions
... | @@ -20,7 +20,7 @@ module.exports = (grunt) -> | ... | @@ -20,7 +20,7 @@ module.exports = (grunt) -> |
20 | 'src/view.coffee' | 20 | 'src/view.coffee' |
21 | 'src/bindings.coffee' | 21 | 'src/bindings.coffee' |
22 | 'src/parsers.coffee' | 22 | 'src/parsers.coffee' |
23 | 'src/keypath_observer.coffee' | 23 | 'src/observer.coffee' |
24 | 'src/binders.coffee' | 24 | 'src/binders.coffee' |
25 | 'src/adapters.coffee' | 25 | 'src/adapters.coffee' |
26 | 'src/export.coffee' | 26 | 'src/export.coffee' | ... | ... |
... | @@ -8,7 +8,7 @@ describe('Rivets.Binding', function() { | ... | @@ -8,7 +8,7 @@ describe('Rivets.Binding', function() { |
8 | el = document.createElement('div'); | 8 | el = document.createElement('div'); |
9 | el.setAttribute('data-text', 'obj.name'); | 9 | el.setAttribute('data-text', 'obj.name'); |
10 | 10 | ||
11 | view = rivets.bind(el, {obj: {}}); | 11 | view = rivets.bind(el, {obj: {name: 'test'}}); |
12 | binding = view.bindings[0]; | 12 | binding = view.bindings[0]; |
13 | model = binding.model; | 13 | model = binding.model; |
14 | }); | 14 | }); |
... | @@ -40,12 +40,10 @@ describe('Rivets.Binding', function() { | ... | @@ -40,12 +40,10 @@ describe('Rivets.Binding', function() { |
40 | rivets.config.preloadData = true; | 40 | rivets.config.preloadData = true; |
41 | }); | 41 | }); |
42 | 42 | ||
43 | it('sets the initial value via the adapter', function() { | 43 | it('sets the initial value', function() { |
44 | spyOn(binding, 'set'); | 44 | spyOn(binding, 'set'); |
45 | spyOn(adapter, 'read'); | ||
46 | binding.bind(); | 45 | binding.bind(); |
47 | expect(adapter.read).toHaveBeenCalledWith(model, 'name'); | 46 | expect(binding.set).toHaveBeenCalledWith('test'); |
48 | expect(binding.set).toHaveBeenCalled(); | ||
49 | }); | 47 | }); |
50 | }); | 48 | }); |
51 | 49 | ... | ... |
... | @@ -9,8 +9,8 @@ class Rivets.Binding | ... | @@ -9,8 +9,8 @@ class Rivets.Binding |
9 | constructor: (@view, @el, @type, @keypath, @options = {}) -> | 9 | constructor: (@view, @el, @type, @keypath, @options = {}) -> |
10 | @formatters = @options.formatters || [] | 10 | @formatters = @options.formatters || [] |
11 | @dependencies = [] | 11 | @dependencies = [] |
12 | @model = undefined | ||
12 | @setBinder() | 13 | @setBinder() |
13 | @setObserver() | ||
14 | 14 | ||
15 | # Sets the binder to use when binding and syncing. | 15 | # Sets the binder to use when binding and syncing. |
16 | setBinder: => | 16 | setBinder: => |
... | @@ -26,18 +26,6 @@ class Rivets.Binding | ... | @@ -26,18 +26,6 @@ class Rivets.Binding |
26 | @binder or= @view.binders['*'] | 26 | @binder or= @view.binders['*'] |
27 | @binder = {routine: @binder} if @binder instanceof Function | 27 | @binder = {routine: @binder} if @binder instanceof Function |
28 | 28 | ||
29 | # Sets a keypath observer that will notify this binding when any intermediary | ||
30 | # keys are changed. | ||
31 | setObserver: => | ||
32 | @observer = new Rivets.KeypathObserver @view, @view.models, @keypath, (obs) => | ||
33 | @unbind true if @key | ||
34 | @model = obs.target | ||
35 | @bind true if @key | ||
36 | @sync() | ||
37 | |||
38 | @key = @observer.key | ||
39 | @model = @observer.target | ||
40 | |||
41 | # Applies all the current formatters to the supplied value and returns the | 29 | # Applies all the current formatters to the supplied value and returns the |
42 | # formatted value. | 30 | # formatted value. |
43 | formattedValue: (value) => | 31 | formattedValue: (value) => |
... | @@ -70,10 +58,16 @@ class Rivets.Binding | ... | @@ -70,10 +58,16 @@ class Rivets.Binding |
70 | 58 | ||
71 | # Syncs up the view binding with the model. | 59 | # Syncs up the view binding with the model. |
72 | sync: => | 60 | sync: => |
73 | @set if @key | 61 | if @model isnt @observer.target |
74 | @view.adapters[@key.interface].read @model, @key.path | 62 | observer.unobserve() for observer in @dependencies |
75 | else | 63 | @dependencies = [] |
76 | @model | 64 | |
65 | if (@model = @observer.target)? and @options.dependencies?.length | ||
66 | for dependency in @options.dependencies | ||
67 | observer = new Rivets.Observer @view, @model, dependency, @sync | ||
68 | @dependencies.push observer | ||
69 | |||
70 | @set @observer.value() | ||
77 | 71 | ||
78 | # Publishes the value currently set on the input element back to the model. | 72 | # Publishes the value currently set on the input element back to the model. |
79 | publish: => | 73 | publish: => |
... | @@ -86,42 +80,30 @@ class Rivets.Binding | ... | @@ -86,42 +80,30 @@ class Rivets.Binding |
86 | if @view.formatters[id]?.publish | 80 | if @view.formatters[id]?.publish |
87 | value = @view.formatters[id].publish value, args... | 81 | value = @view.formatters[id].publish value, args... |
88 | 82 | ||
89 | @view.adapters[@key.interface].publish @model, @key.path, value | 83 | @observer.publish value |
90 | 84 | ||
91 | # Subscribes to the model for changes at the specified keypath. Bi-directional | 85 | # Subscribes to the model for changes at the specified keypath. Bi-directional |
92 | # routines will also listen for changes on the element to propagate them back | 86 | # routines will also listen for changes on the element to propagate them back |
93 | # to the model. | 87 | # to the model. |
94 | bind: (silent = false) => | 88 | bind: => |
95 | @binder.bind?.call @, @el unless silent | 89 | @binder.bind?.call @, @el |
96 | @view.adapters[@key.interface].subscribe(@model, @key.path, @sync) if @key | 90 | @observer = new Rivets.Observer @view, @view.models, @keypath, @sync |
97 | @sync() if @view.config.preloadData unless silent | 91 | @model = @observer.target |
98 | 92 | ||
99 | if @options.dependencies?.length | 93 | if @model? and @options.dependencies?.length |
100 | for dependency in @options.dependencies | 94 | for dependency in @options.dependencies |
101 | observer = new Rivets.KeypathObserver @view, @model, dependency, (obs, prev) => | 95 | observer = new Rivets.Observer @view, @model, dependency, @sync |
102 | key = obs.key | ||
103 | @view.adapters[key.interface].unsubscribe prev, key.path, @sync | ||
104 | @view.adapters[key.interface].subscribe obs.target, key.path, @sync | ||
105 | @sync() | ||
106 | |||
107 | key = observer.key | ||
108 | @view.adapters[key.interface].subscribe observer.target, key.path, @sync | ||
109 | @dependencies.push observer | 96 | @dependencies.push observer |
110 | 97 | ||
111 | # Unsubscribes from the model and the element. | 98 | @sync() if @view.config.preloadData |
112 | unbind: (silent = false) => | ||
113 | unless silent | ||
114 | @binder.unbind?.call @, @el | ||
115 | @observer.unobserve() | ||
116 | |||
117 | @view.adapters[@key.interface].unsubscribe(@model, @key.path, @sync) if @key | ||
118 | 99 | ||
119 | if @dependencies.length | 100 | # Unsubscribes from the model and the element. |
120 | for obs in @dependencies | 101 | unbind: => |
121 | key = obs.key | 102 | @binder.unbind?.call @, @el |
122 | @view.adapters[key.interface].unsubscribe obs.target, key.path, @sync | 103 | @observer.unobserve() |
123 | 104 | ||
124 | @dependencies = [] | 105 | observer.unobserve() for observer in @dependencies |
106 | @dependencies = [] | ||
125 | 107 | ||
126 | # Updates the binding's model from what is currently set on the view. Unbinds | 108 | # Updates the binding's model from what is currently set on the view. Unbinds |
127 | # the old model first and then re-binds with the new model. | 109 | # the old model first and then re-binds with the new model. |
... | @@ -190,7 +172,6 @@ class Rivets.TextBinding extends Rivets.Binding | ... | @@ -190,7 +172,6 @@ class Rivets.TextBinding extends Rivets.Binding |
190 | constructor: (@view, @el, @type, @keypath, @options = {}) -> | 172 | constructor: (@view, @el, @type, @keypath, @options = {}) -> |
191 | @formatters = @options.formatters || [] | 173 | @formatters = @options.formatters || [] |
192 | @dependencies = [] | 174 | @dependencies = [] |
193 | @setObserver() | ||
194 | 175 | ||
195 | # A standard routine binder used for text node bindings. | 176 | # A standard routine binder used for text node bindings. |
196 | binder: | 177 | binder: | ... | ... |
1 | # Rivets.KeypathObserver | 1 | # Rivets.Observer |
2 | # ---------------------- | 2 | # ---------------------- |
3 | 3 | ||
4 | # Parses and observes a full keypath with the appropriate adapters. Also | 4 | # Parses and observes a full keypath with the appropriate adapters. Also |
5 | # intelligently re-realizes the keypath when intermediary keys change. | 5 | # intelligently re-realizes the keypath when intermediary keys change. |
6 | class Rivets.KeypathObserver | 6 | class Rivets.Observer |
7 | # Performs the initial parse, variable instantiation and keypath realization. | 7 | # Performs the initial parse, variable instantiation and keypath realization. |
8 | constructor: (@view, @model, @keypath, @callback) -> | 8 | constructor: (@view, @model, @keypath, @callback) -> |
9 | @parse() | 9 | @parse() |
10 | @objectPath = [] | 10 | @initialize() |
11 | @target = @realize() | ||
12 | 11 | ||
13 | # Parses the keypath using the interfaces defined on the view. Sets variables | 12 | # Parses the keypath using the interfaces defined on the view. Sets variables |
14 | # for the tokenized keypath, as well as the end key. | 13 | # for the tokenized keypath, as well as the end key. |
... | @@ -25,34 +24,67 @@ class Rivets.KeypathObserver | ... | @@ -25,34 +24,67 @@ class Rivets.KeypathObserver |
25 | @tokens = Rivets.KeypathParser.parse path, interfaces, root | 24 | @tokens = Rivets.KeypathParser.parse path, interfaces, root |
26 | @key = @tokens.pop() | 25 | @key = @tokens.pop() |
27 | 26 | ||
28 | # Updates the keypath. This is called when any intermediate key is changed. | 27 | initialize: => |
28 | @objectPath = [] | ||
29 | @target = @realize() | ||
30 | @set true, @key, @target, @callback if @target? | ||
31 | |||
32 | # Updates the keypath. This is called when any intermediary key is changed. | ||
29 | update: => | 33 | update: => |
30 | unless (next = @realize()) is @target | 34 | unless (next = @realize()) is @target |
31 | prev = @target | 35 | @set false, @key, @target, @callback if @target? |
36 | @set true, @key, next, @callback if next? | ||
37 | |||
38 | oldValue = @value() | ||
32 | @target = next | 39 | @target = next |
33 | @callback @, prev | 40 | @callback() unless @value() is oldValue |
41 | |||
42 | adapter: (key) => | ||
43 | @view.adapters[key.interface] | ||
44 | |||
45 | set: (active, key, obj, callback) => | ||
46 | action = if active then 'subscribe' else 'unsubscribe' | ||
47 | @adapter(key)[action] obj, key.path, callback | ||
48 | |||
49 | read: (key, obj) => | ||
50 | @adapter(key).read obj, key.path | ||
51 | |||
52 | publish: (value) => | ||
53 | if @target? | ||
54 | @adapter(@key).publish @target, @key.path, value | ||
55 | |||
56 | value: => | ||
57 | @read @key, @target if @target? | ||
34 | 58 | ||
35 | # Realizes the full keypath, attaching observers for every key and correcting | 59 | # Realizes the full keypath, attaching observers for every key and correcting |
36 | # old observers to any changed objects in the keypath. | 60 | # old observers to any changed objects in the keypath. |
37 | realize: => | 61 | realize: => |
38 | current = @model | 62 | current = @model |
63 | unreached = null | ||
39 | 64 | ||
40 | for token, index in @tokens | 65 | for token, index in @tokens |
41 | if @objectPath[index]? | 66 | if current? |
42 | if current isnt prev = @objectPath[index] | 67 | if @objectPath[index]? |
43 | @view.adapters[token.interface].unsubscribe prev, token.path, @update | 68 | if current isnt prev = @objectPath[index] |
44 | @view.adapters[token.interface].subscribe current, token.path, @update | 69 | @set false, token, prev, @update |
70 | @set true, token, current, @update | ||
71 | @objectPath[index] = current | ||
72 | else | ||
73 | @set true, token, current, @update | ||
45 | @objectPath[index] = current | 74 | @objectPath[index] = current |
75 | |||
76 | current = @read token, current | ||
46 | else | 77 | else |
47 | @view.adapters[token.interface].subscribe current, token.path, @update | 78 | unreached ?= index |
48 | @objectPath[index] = current | ||
49 | 79 | ||
50 | current = @view.adapters[token.interface].read current, token.path | 80 | if prev = @objectPath[index] |
81 | @set false, token, prev, @update | ||
51 | 82 | ||
83 | @objectPath.splice unreached if unreached? | ||
52 | current | 84 | current |
53 | 85 | ||
54 | # Unobserves any current observers set up on the keys. | 86 | # Unobserves any current observers set up on the keys. |
55 | unobserve: => | 87 | unobserve: => |
56 | for token, index in @tokens | 88 | for token, index in @tokens |
57 | if obj = @objectPath[index] | 89 | if obj = @objectPath[index] |
58 | @view.adapters[token.interface].unsubscribe obj, token.path, @update | 90 | @set false, token, obj, @update | ... | ... |
-
Please register or sign in to post a comment