volt 0.2.3

Sign up to get free protection for your applications and to get access to all the features.
Files changed (116) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +21 -0
  3. data/.rspec +2 -0
  4. data/.ruby-version +1 -0
  5. data/Gemfile +37 -0
  6. data/Guardfile +9 -0
  7. data/LICENSE.txt +22 -0
  8. data/Rakefile +23 -0
  9. data/Readme.md +34 -0
  10. data/VERSION +1 -0
  11. data/bin/volt +4 -0
  12. data/docs/GETTING_STARTED.md +7 -0
  13. data/docs/GUIDE.md +33 -0
  14. data/lib/volt.rb +15 -0
  15. data/lib/volt/benchmark/benchmark.rb +25 -0
  16. data/lib/volt/cli.rb +34 -0
  17. data/lib/volt/console.rb +19 -0
  18. data/lib/volt/controllers/model_controller.rb +29 -0
  19. data/lib/volt/extra_core/array.rb +10 -0
  20. data/lib/volt/extra_core/blank.rb +88 -0
  21. data/lib/volt/extra_core/extra_core.rb +7 -0
  22. data/lib/volt/extra_core/numeric.rb +9 -0
  23. data/lib/volt/extra_core/object.rb +36 -0
  24. data/lib/volt/extra_core/string.rb +29 -0
  25. data/lib/volt/extra_core/stringify_keys.rb +7 -0
  26. data/lib/volt/extra_core/true_false.rb +44 -0
  27. data/lib/volt/extra_core/try.rb +31 -0
  28. data/lib/volt/models.rb +5 -0
  29. data/lib/volt/models/array_model.rb +37 -0
  30. data/lib/volt/models/model.rb +210 -0
  31. data/lib/volt/models/model_wrapper.rb +23 -0
  32. data/lib/volt/models/params.rb +67 -0
  33. data/lib/volt/models/url.rb +192 -0
  34. data/lib/volt/page/url_tracker.rb +36 -0
  35. data/lib/volt/reactive/array_extensions.rb +13 -0
  36. data/lib/volt/reactive/event_chain.rb +126 -0
  37. data/lib/volt/reactive/events.rb +283 -0
  38. data/lib/volt/reactive/object_tracker.rb +99 -0
  39. data/lib/volt/reactive/object_tracking.rb +15 -0
  40. data/lib/volt/reactive/reactive_array.rb +222 -0
  41. data/lib/volt/reactive/reactive_tags.rb +64 -0
  42. data/lib/volt/reactive/reactive_value.rb +368 -0
  43. data/lib/volt/reactive/string_extensions.rb +34 -0
  44. data/lib/volt/router/routes.rb +83 -0
  45. data/lib/volt/server.rb +121 -0
  46. data/lib/volt/server/binding_setup.rb +2 -0
  47. data/lib/volt/server/channel_handler.rb +31 -0
  48. data/lib/volt/server/component_handler.rb +88 -0
  49. data/lib/volt/server/if_binding_setup.rb +29 -0
  50. data/lib/volt/server/request_handler.rb +16 -0
  51. data/lib/volt/server/scope.rb +43 -0
  52. data/lib/volt/server/source_map_server.rb +31 -0
  53. data/lib/volt/server/template_parser.rb +452 -0
  54. data/lib/volt/store/mongo.rb +5 -0
  55. data/lib/volt/templates/attribute_binding.rb +110 -0
  56. data/lib/volt/templates/base_binding.rb +37 -0
  57. data/lib/volt/templates/channel.rb +48 -0
  58. data/lib/volt/templates/content_binding.rb +35 -0
  59. data/lib/volt/templates/document_events.rb +80 -0
  60. data/lib/volt/templates/each_binding.rb +115 -0
  61. data/lib/volt/templates/event_binding.rb +51 -0
  62. data/lib/volt/templates/if_binding.rb +74 -0
  63. data/lib/volt/templates/memory_test.rb +26 -0
  64. data/lib/volt/templates/page.rb +146 -0
  65. data/lib/volt/templates/reactive_template.rb +38 -0
  66. data/lib/volt/templates/render_queue.rb +5 -0
  67. data/lib/volt/templates/sub_context.rb +23 -0
  68. data/lib/volt/templates/targets/attribute_section.rb +33 -0
  69. data/lib/volt/templates/targets/attribute_target.rb +18 -0
  70. data/lib/volt/templates/targets/base_section.rb +14 -0
  71. data/lib/volt/templates/targets/binding_document/base_node.rb +3 -0
  72. data/lib/volt/templates/targets/binding_document/component_node.rb +112 -0
  73. data/lib/volt/templates/targets/binding_document/html_node.rb +11 -0
  74. data/lib/volt/templates/targets/dom_section.rb +147 -0
  75. data/lib/volt/templates/targets/dom_target.rb +11 -0
  76. data/lib/volt/templates/template_binding.rb +159 -0
  77. data/lib/volt/templates/template_renderer.rb +50 -0
  78. data/spec/models/event_chain_spec.rb +129 -0
  79. data/spec/models/model_spec.rb +340 -0
  80. data/spec/models/old_model_spec.rb +109 -0
  81. data/spec/models/reactive_array_spec.rb +262 -0
  82. data/spec/models/reactive_tags_spec.rb +35 -0
  83. data/spec/models/reactive_value_spec.rb +336 -0
  84. data/spec/models/string_extensions_spec.rb +57 -0
  85. data/spec/router/routes_spec.rb +24 -0
  86. data/spec/server/template_parser_spec.rb +50 -0
  87. data/spec/spec_helper.rb +20 -0
  88. data/spec/store/mongo_spec.rb +4 -0
  89. data/spec/templates/targets/binding_document/component_node_spec.rb +18 -0
  90. data/spec/templates/template_binding_spec.rb +98 -0
  91. data/templates/.gitignore +12 -0
  92. data/templates/Gemfile.tt +8 -0
  93. data/templates/app/.empty_directory +0 -0
  94. data/templates/app/home/config/routes.rb +1 -0
  95. data/templates/app/home/controllers/index_controller.rb +5 -0
  96. data/templates/app/home/css/.empty_directory +0 -0
  97. data/templates/app/home/models/.empty_directory +0 -0
  98. data/templates/app/home/views/index/about.html +9 -0
  99. data/templates/app/home/views/index/home.html +7 -0
  100. data/templates/app/home/views/index/index.html +28 -0
  101. data/templates/config.ru +4 -0
  102. data/templates/public/css/ansi.css +0 -0
  103. data/templates/public/css/bootstrap-theme.css +459 -0
  104. data/templates/public/css/bootstrap.css +7098 -0
  105. data/templates/public/css/jumbotron.css +79 -0
  106. data/templates/public/fonts/glyphicons-halflings-regular.eot +0 -0
  107. data/templates/public/fonts/glyphicons-halflings-regular.svg +229 -0
  108. data/templates/public/fonts/glyphicons-halflings-regular.ttf +0 -0
  109. data/templates/public/fonts/glyphicons-halflings-regular.woff +0 -0
  110. data/templates/public/index.html +25 -0
  111. data/templates/public/js/bootstrap.js +0 -0
  112. data/templates/public/js/jquery-2.0.3.js +8829 -0
  113. data/templates/public/js/sockjs-0.2.1.min.js +27 -0
  114. data/templates/spec/spec_helper.rb +20 -0
  115. data/volt.gemspec +41 -0
  116. metadata +412 -0
@@ -0,0 +1,67 @@
1
+ # All url related data is stored in params. This includes the main uri
2
+ # in addition to any query parameters. The router is responsible for
3
+ # converting any uri sections into params. Sections in the uri will
4
+ # override any specified parameters.
5
+ #
6
+ # The params value can be updated the same way a model would be, only
7
+ # the updates will trigger an updated url via the browser history api.
8
+ # TODO: Support # for browsers without the history api.
9
+
10
+ class Params < Model
11
+ def initialize(*args)
12
+ super(*args)
13
+ end
14
+
15
+ def deep_clone
16
+ new_obj = clone
17
+
18
+ new_obj.attributes = new_obj.attributes.dup
19
+
20
+ new_obj
21
+ end
22
+
23
+ tag_method(:delete) do
24
+ destructive!
25
+ end
26
+ def delete(*args)
27
+ super
28
+
29
+ value_updated
30
+ end
31
+
32
+ def method_missing(method_name, *args, &block)
33
+ result = super
34
+
35
+ if method_name[0] == '_' && method_name[-1] == '='
36
+ # Trigger value updated after an assignment
37
+ self.value_updated
38
+ end
39
+
40
+ return result
41
+ end
42
+
43
+ def value_updated
44
+ # Once the initial url has been parsed and set into the attributes,
45
+ # start triggering updates on change events.
46
+ # TODO: This is a temp solution, we need to make it so value_updated
47
+ # is called after the reactive_value has been updated.
48
+ if RUBY_PLATFORM == 'opal'
49
+ %x{
50
+ if (window.setTimeout && this.$run_update.bind) {
51
+ if (window.paramsUpdateTimer) {
52
+ clearTimeout(window.paramsUpdateTimer);
53
+ }
54
+ window.paramsUpdateTimer = setTimeout(this.$run_update.bind(this), 0);
55
+ }
56
+ }
57
+ end
58
+ end
59
+
60
+ def run_update
61
+ $page.params.trigger!('child_changed') if Volt.client?
62
+ end
63
+
64
+ def new_model(*args)
65
+ Params.new(*args)
66
+ end
67
+ end
@@ -0,0 +1,192 @@
1
+ # The url class handles parsing and updating the url
2
+ class URL
3
+ include ReactiveTags
4
+
5
+ # TODO: we need to make it so change events only trigger on changes
6
+ attr_reader :scheme, :host, :port, :path, :query, :params
7
+ attr_accessor :router
8
+
9
+ def initialize(router=nil)
10
+ @router = router
11
+ @params = Params.new({}, 'params')
12
+ end
13
+
14
+ # Parse takes in a url and extracts each sections.
15
+ # It also assigns and changes to the params.
16
+ tag_method(:parse) do
17
+ destructive!
18
+ end
19
+ def parse(url)
20
+ matcher = url.match(/^(https?)[:]\/\/([^\/]+)(.*)$/)
21
+ @scheme = matcher[1]
22
+ @host, @port = matcher[2].split(':')
23
+ @port ||= 80
24
+
25
+ @path = matcher[3]
26
+ @path, @fragment = @path.split('#', 2)
27
+ @path, @query = @path.split('?', 2)
28
+
29
+ assign_query_hash_to_params
30
+ end
31
+
32
+ # Full url rebuilds the url from it's constituent parts
33
+ def full_url
34
+ if @port
35
+ host_with_port = "#{@host}:#{@port}"
36
+ else
37
+ host_with_port = @host
38
+ end
39
+
40
+ path, params = @router.url_for_params(@params)
41
+
42
+ new_url = "#{@scheme}://#{host_with_port}#{path || @path}"
43
+
44
+ unless params.empty?
45
+ new_url += '?'
46
+ query_parts = []
47
+ nested_params_hash(params).each_pair do |key,value|
48
+ value = value.cur
49
+ # remove the _ from the front
50
+ value = `encodeURI(value)`
51
+ query_parts << "#{key}=#{value}"
52
+ end
53
+
54
+ new_url += query_parts.join('&')
55
+ end
56
+
57
+ return new_url
58
+ end
59
+
60
+ # Called when the state has changed and the url in the
61
+ # browser should be updated
62
+ # Called when an attribute changes to update the url
63
+ tag_method(:update!) do
64
+ destructive!
65
+ # TODO: ! methods should default to destructive
66
+ end
67
+ def update!
68
+ new_url = full_url()
69
+
70
+ if `(document.location.href != new_url)`
71
+ `history.pushState(null, null, new_url)`
72
+ end
73
+ end
74
+
75
+ private
76
+ # Assigning the params is tricky since we don't want to trigger changed on
77
+ # any values that have not changed. So we first loop through all current
78
+ # url params, removing any not present in the params, while also removing
79
+ # them from the list of new params as added. Then we loop through the
80
+ # remaining new parameters and assign them.
81
+ def assign_query_hash_to_params
82
+ # Get a nested hash representing the current url params.
83
+ query_hash = self.query_hash
84
+
85
+ # Get the params that are in the route
86
+ query_hash.merge!(@router.params_for_path(@path))
87
+
88
+ # Loop through the .params we already have assigned.
89
+ assign_from_old(@params, query_hash)
90
+ assign_new(@params, query_hash)
91
+ end
92
+
93
+ def assign_from_old(params, new_params)
94
+ queued_deletes = []
95
+
96
+ params.cur.attributes.each_pair do |name,old_val|
97
+ # If there is a new value, see if it has [name]
98
+ new_val = new_params ? new_params[name] : nil
99
+
100
+ if !new_val
101
+ # Queues the delete until after we finish the each_pair loop
102
+ queued_deletes << name
103
+ elsif new_val.is_a?(Hash)
104
+ assign_from_old(old_val, new_val)
105
+ else
106
+ # assign value
107
+ if old_val != new_val
108
+ params.send(:"#{name}=", new_val)
109
+ end
110
+ new_params.delete(name)
111
+ end
112
+ end
113
+
114
+ queued_deletes.each {|name| params.delete(name) }
115
+ end
116
+
117
+ def assign_new(params, new_params)
118
+ new_params.each_pair do |name, value|
119
+ if value.is_a?(Hash)
120
+ assign_new(params.send(name), value)
121
+ else
122
+ # assign
123
+ params.send(:"#{name}=", value)
124
+ end
125
+ end
126
+ end
127
+
128
+ def query_hash
129
+ query_hash = {}
130
+ if @query
131
+ @query.split('&').reject {|v| v == '' }.each do |part|
132
+ parts = part.split('=').reject {|v| v == '' }
133
+
134
+ # Decode string
135
+ # parts[0] = `decodeURI(parts[0])`
136
+ parts[1] = `decodeURI(parts[1])`
137
+
138
+ sections = query_key_sections(parts[0])
139
+
140
+ hash_part = query_hash
141
+ sections.each_with_index do |section,index|
142
+ if index == sections.size-1
143
+ # Last part, assign the value
144
+ hash_part[section] = parts[1]
145
+ else
146
+ hash_part = (hash_part[section] ||= {})
147
+ end
148
+ end
149
+ end
150
+ end
151
+
152
+ return query_hash
153
+ end
154
+
155
+ # Splits a key from a ?key=value&... parameter into its nested
156
+ # parts. It also adds back the _'s used to access them in params.
157
+ # Example:
158
+ # user[name]=Ryan would parse as [:_user, :_name]
159
+ def query_key_sections(key)
160
+ key.split(/\[([^\]]+)\]/).reject(&:empty?).map {|v| :"_#{v}"}
161
+ end
162
+
163
+ # Generate the key for a nested param attribute
164
+ def query_key(path)
165
+ i = 0
166
+ path.map do |v|
167
+ v = v[1..-1]
168
+ i += 1
169
+ if i != 1
170
+ "[#{v}]"
171
+ else
172
+ v
173
+ end
174
+ end.join('')
175
+ end
176
+
177
+ def nested_params_hash(params, path=[])
178
+ results = {}
179
+
180
+ params.each_pair do |key,value|
181
+ if value.cur.is_a?(Params) # TODO: Should be a param
182
+ # TODO: Broke here somehow for nested
183
+ results.merge!(nested_params_hash(value, path + [key]))
184
+ else
185
+ results[query_key(path + [key])] = value
186
+ end
187
+ end
188
+
189
+ return results
190
+ end
191
+
192
+ end
@@ -0,0 +1,36 @@
1
+ # The URLTracker is responsible for updating the url when
2
+ # a param changes, or updating the url model/params when
3
+ # the browser url changes.
4
+ class UrlTracker
5
+ def initialize(page)
6
+ @page = page
7
+
8
+ if Volt.client?
9
+ page.params.on('child_changed') do
10
+ @page.url.update!
11
+ end
12
+
13
+ that = self
14
+
15
+ # Setup popstate on the dom ready event. Prevents an extra
16
+ # popstate trigger
17
+ %x{
18
+ var first = true;
19
+ window.addEventListener("popstate", function(e) {
20
+ if (first === false) {
21
+ that.$url_updated();
22
+ }
23
+
24
+ first = false;
25
+
26
+ return true;
27
+ });
28
+ }
29
+ end
30
+ end
31
+
32
+ def url_updated(first_call=false)
33
+ @page.url.parse(`document.location.href`)
34
+ @page.url.update! unless first_call
35
+ end
36
+ end
@@ -0,0 +1,13 @@
1
+ class Array
2
+ include ReactiveTags
3
+ alias :__old_plus :+
4
+
5
+ def +(val)
6
+ result = __old_plus(val.cur)
7
+ if val.reactive? && !result.reactive?
8
+ result = ReactiveValue.new(result)
9
+ end
10
+
11
+ return result
12
+ end
13
+ end
@@ -0,0 +1,126 @@
1
+ CHAIN_DEBUG = false
2
+
3
+ class ChainListener
4
+ attr_reader :object, :callback
5
+
6
+ def initialize(event_chain, object, callback)
7
+ @event_chain = event_chain
8
+ @object = object
9
+ @callback = callback
10
+
11
+ if RUBY_PLATFORM == 'opal' && CHAIN_DEBUG
12
+ `window.chain_listeners = window.chain_listeners || 0;`
13
+ `window.chain_listeners += 1;`
14
+ `console.log('chain listeners: ', window.chain_listeners)`
15
+ end
16
+ end
17
+
18
+ def remove
19
+ raise "event chain already removed" if @removed
20
+ @removed = true
21
+ @event_chain.remove_object(self)
22
+
23
+ # We need to clear these to free memory
24
+ @event_chain = nil
25
+ @object = nil
26
+ @callback = nil
27
+
28
+ if RUBY_PLATFORM == 'opal' && CHAIN_DEBUG
29
+ `window.chain_listeners -= 1;`
30
+ end
31
+ end
32
+ end
33
+
34
+ class EventChain
35
+ def initialize(main_object)
36
+ @event_chain = {}
37
+ @main_object = main_object
38
+ @event_counts = {}
39
+ end
40
+
41
+ # Register an event listener that chains from object to self
42
+ def setup_listener(event, chain_listener)
43
+ return chain_listener.object.on(event, @main_object) do |*args|
44
+ if callback = chain_listener.callback
45
+ callback.call(event, *args)
46
+ else
47
+ # Trigger on this value, when it happens on the parent
48
+ @main_object.trigger!(event, *args)
49
+ end
50
+ end
51
+ end
52
+
53
+ def all_listening_events
54
+ all_listeners = []
55
+ all_listeners += @main_object.listeners.keys
56
+
57
+ if @main_object
58
+ @main_object.event_followers.each do |event_follower|
59
+ all_listeners += event_follower.listeners.keys
60
+ end
61
+ end
62
+
63
+ return all_listeners.uniq
64
+ end
65
+
66
+
67
+ # We can chain our events to any other object that includes
68
+ # Events
69
+ def add_object(object, &block)
70
+ # puts "ADD OBJECT: #{object.inspect} to #{self.inspect}"
71
+
72
+ chain_listener = ChainListener.new(self, object, block)
73
+
74
+ listeners = {}
75
+
76
+ all_listening_events.uniq.each do |event|
77
+ # Create a listener for each event
78
+ listeners[event] = setup_listener(event, chain_listener)
79
+ end
80
+
81
+ @event_chain[chain_listener] = listeners
82
+
83
+ return chain_listener
84
+ end
85
+
86
+
87
+ def remove_object(chain_listener)
88
+ @event_chain[chain_listener].each_pair do |event,listener|
89
+ # Unbind each listener
90
+ listener.remove
91
+ end
92
+
93
+ @event_chain.delete(chain_listener)
94
+ end
95
+
96
+ def add_event(event)
97
+ unless @event_counts[event]
98
+ @event_chain.each_pair do |chain_listener,listeners|
99
+ # Only add if we haven't already chained this event
100
+ unless listeners[event]
101
+ listeners[event] = setup_listener(event, chain_listener)
102
+ end
103
+ end
104
+ end
105
+
106
+ @event_counts[event] ||= 0
107
+ @event_counts[event] += 1
108
+ end
109
+
110
+ # Removes the event from all events in all objects
111
+ def remove_event(event)
112
+ if @event_counts[event]
113
+ count = @event_counts[event] -= 1
114
+
115
+ if count == 0
116
+ @event_chain.each_pair do |chain_listener,listeners|
117
+ listeners[event].remove# if listeners[event]
118
+ listeners.delete(event)
119
+ end
120
+
121
+ # Also remove the event count
122
+ @event_counts.delete(event)
123
+ end
124
+ end
125
+ end
126
+ end
@@ -0,0 +1,283 @@
1
+ require 'volt/reactive/event_chain'
2
+ require 'volt/reactive/object_tracker'
3
+
4
+ DEBUG = false
5
+
6
+ # A listener gets returned when adding an 'on' event listener. It can be
7
+ # used to clear the event listener.
8
+ class Listener
9
+ attr_reader :scope_provider
10
+
11
+ def initialize(klass, event, scope_provider, callback)
12
+ @klass = klass
13
+ @event = event
14
+ @scope_provider = scope_provider
15
+ @callback = callback
16
+
17
+ if DEBUG && RUBY_PLATFORM == 'opal'
18
+ # puts "e: #{event} on #{klass.inspect}"
19
+ @@all_events ||= []
20
+ @@all_events << self
21
+
22
+ # counts = {}
23
+ # @@all_events.each do |ev|
24
+ # scope = (ev.scope_provider && ev.scope_provider.scope) || nil
25
+ #
26
+ # # puts `typeof(scope)`
27
+ # if `typeof(scope) !== 'undefined'`
28
+ # counts[scope] ||= 0
29
+ # counts[scope] += 1
30
+ # end
31
+ # end
32
+ #
33
+ # puts counts.inspect
34
+
35
+ `window.total_listeners = window.total_listeners || 0;`
36
+ `window.total_listeners += 1;`
37
+ `console.log(window.total_listeners);`
38
+ end
39
+ end
40
+
41
+ def internal?
42
+ @internal
43
+ end
44
+
45
+ def scope
46
+ @scope_provider && @scope_provider.scope
47
+ end
48
+
49
+ def call(*args)
50
+ # raise "Triggered on removed: #{@event} on #{@klass2.inspect}" if @removed
51
+ if @removed
52
+ puts "Triggered on removed: #{@event}"
53
+ return
54
+ end
55
+
56
+ # Queue a live value update
57
+ if @klass.reactive?
58
+ # We are working with a reactive value. Its receiving an event meaning
59
+ # something changed. Queue an update of the value it tracks.
60
+ @klass.object_tracker.queue_update
61
+ # puts "Queued: #{ObjectTracker.queue.inspect}"
62
+ end
63
+
64
+ @callback.call(*args)
65
+ end
66
+
67
+ # Removes the listener from where ever it was created.
68
+ def remove
69
+ # puts "FAIL:" if @removed
70
+ raise "event #{@event} already removed" if @removed
71
+
72
+ # puts "e rem: #{@event} on #{@klass.inspect}"
73
+ if DEBUG && RUBY_PLATFORM == 'opal'
74
+ @@all_events.delete(self) if @@all_events
75
+
76
+ `window.total_listeners -= 1;`
77
+ `console.log("Rem", window.total_listeners);`
78
+ end
79
+
80
+
81
+ @removed = true
82
+ @klass.remove_listener(@event, self)
83
+
84
+ # puts "Removed Listener for: #{@event} - #{@scope_provider && @scope_provider.scope.inspect} from #{@klass.inspect}"
85
+
86
+ # We need to clear these references to free the memory
87
+ @scope_provider = nil
88
+ @callback = nil
89
+ # @klass2 = @klass
90
+ @klass = nil
91
+ # @event = nil
92
+
93
+ end
94
+
95
+ def inspect
96
+ "<Listener:#{object_id} event=#{@event} scope=#{scope.inspect}#{' internal' if internal?}>"
97
+ end
98
+ end
99
+
100
+ module Events
101
+ attr_accessor :scope
102
+ # Add a listener for an event
103
+ def on(event, scope_provider=nil, &block)
104
+ # puts "Register: #{event} on #{self.inspect}"
105
+ event = event.to_sym
106
+
107
+ new_listener = Listener.new(self, event, scope_provider, block)
108
+
109
+ @listeners ||= {}
110
+ @listeners[event] ||= []
111
+ @listeners[event] << new_listener
112
+
113
+ first = @listeners[event].size == 1
114
+ add_event_to_chains(event) if first
115
+
116
+ # Let the included class know that an event was registered. (if it cares)
117
+ if self.respond_to?(:event_added)
118
+ # call event added passing the event, the scope, and a boolean if it
119
+ # is the first time this event has been added.
120
+ self.event_added(event, scope_provider, first)
121
+ end
122
+
123
+ return new_listener
124
+ end
125
+
126
+ def event_chain
127
+ @event_chain ||= EventChain.new(self)
128
+ end
129
+
130
+ def listeners
131
+ @listeners || {}
132
+ end
133
+
134
+ # Typically you would call .remove on the listener returned from the .on
135
+ # method. However, here you can also pass in the original proc to remove
136
+ # a listener
137
+ def remove_listener(event, listener)
138
+ event = event.to_sym
139
+
140
+ raise "Unable to delete #{event} from #{self.inspect}" unless @listeners && @listeners[event]
141
+
142
+ # if @listeners && @listeners[event]
143
+ @listeners[event].delete(listener)
144
+
145
+ no_more_events = @listeners[event].size == 0
146
+ if no_more_events
147
+ remove_event_from_chains(event)
148
+
149
+ # No registered listeners now on this event
150
+ @listeners.delete(event)
151
+ end
152
+
153
+ # Let the class we're included on know that we removed a listener (if it cares)
154
+ if self.respond_to?(:event_removed)
155
+ # Pass in the event and a boolean indicating if it is the last event
156
+ self.event_removed(event, no_more_events)
157
+ end
158
+ # end
159
+ end
160
+
161
+ # When events get added, we need to notify event chains so they
162
+ # can update and chain any new events.
163
+ def add_event_to_chains(event)
164
+ # First time this event is added, update any chains
165
+ event_chain.add_event(event)
166
+
167
+ # We need to keep the event chain's updated for any objects we're
168
+ # following for events.
169
+ event_followings.each {|ef| ef.event_chain.add_event(event) }
170
+
171
+ if event != :changed && !@other_event_listener
172
+ @other_event_listener = on('changed') { }
173
+ end
174
+ end
175
+
176
+ # When events are removed, we need to notify any relevent chains so they
177
+ # can remove any chained events.
178
+ def remove_event_from_chains(event)
179
+ event_chain.remove_event(event)
180
+
181
+ # We need to keep the event chain's updated for any objects we're
182
+ # following for events.
183
+ event_followings.each {|ef| ef.event_chain.remove_event(event) }
184
+
185
+ if event != :changed
186
+ # See if there are any remaining events that aren't changed
187
+ if listeners.keys.reject {|k| k == :changed }.size == 0
188
+ @other_event_listener.remove
189
+ @other_event_listener = nil
190
+ end
191
+ end
192
+ end
193
+
194
+
195
+ # Track the current object that we're following.
196
+ def event_followings
197
+ @event_followings || []
198
+ end
199
+
200
+ def add_following(object)
201
+ @event_followings ||= []
202
+ @event_followings << object
203
+
204
+ # Take all of our listeners and add them to the
205
+ listeners.keys.each do |event|
206
+ object.event_chain.add_event(event)
207
+ end
208
+ end
209
+
210
+ def remove_following(object)
211
+ @event_followings.delete(object)
212
+
213
+ listeners.keys.each do |event|
214
+ object.event_chain.remove_event(event)
215
+ end
216
+ end
217
+
218
+ # Track who's following us
219
+ def event_followers
220
+ @event_followers || []
221
+ end
222
+
223
+ def add_event_follower(follower)
224
+ @event_followers ||= []
225
+ @event_followers << follower
226
+
227
+ follower.add_following(self)
228
+ end
229
+
230
+ def remove_event_follower(follower)
231
+ if @event_followers
232
+ @event_followers.delete(follower)
233
+
234
+ follower.remove_following(self)
235
+ end
236
+ end
237
+
238
+ # Return all listeners for an event on the current object and any event
239
+ # following objects.
240
+ def all_listeners_for(event)
241
+ # TODO: We dup at the moment because some events unregister events, is there
242
+ # a better solution than this?
243
+ all_listeners = []
244
+ all_listeners += @listeners[event].dup if @listeners && @listeners[event]
245
+
246
+ if @event_followers
247
+ @event_followers.each do |event_follower|
248
+ ef_listeners = event_follower.listeners
249
+ all_listeners += ef_listeners[event].dup if ef_listeners[event]
250
+ end
251
+ end
252
+
253
+ return all_listeners
254
+ end
255
+
256
+ def trigger!(event, *args)
257
+ ObjectTracker.process_queue if !reactive? && !respond_to?(:skip_current_queue_flush)
258
+
259
+ event = event.to_sym
260
+
261
+ all_listeners_for(event).each do |listener|
262
+ # Call the event on each listener
263
+ listener.call(*args)
264
+ end
265
+
266
+ nil
267
+ end
268
+
269
+ # Takes a block, which passes in
270
+ def trigger_by_scope!(event, *args, &block)
271
+ ObjectTracker.process_queue if !reactive? && !respond_to?(:skip_current_queue_flush)
272
+
273
+ event = event.to_sym
274
+
275
+ all_listeners_for(event).each do |listener|
276
+ # Call the block, pass in the scope
277
+ if block.call(listener.scope)
278
+ listener.call(*args)
279
+ end
280
+ end
281
+ end
282
+
283
+ end