volt 0.2.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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