volt 0.8.27.beta3 → 0.8.27.beta4

Sign up to get free protection for your applications and to get access to all the features.
Files changed (136) hide show
  1. checksums.yaml +4 -4
  2. data/.travis.yml +2 -0
  3. data/CHANGELOG.md +11 -0
  4. data/CONTRIBUTING.md +3 -2
  5. data/{Readme.md → README.md} +9 -12
  6. data/Rakefile +2 -9
  7. data/VERSION +1 -1
  8. data/app/volt/models/user.rb +8 -0
  9. data/app/volt/tasks/live_query/data_store.rb +13 -5
  10. data/app/volt/tasks/live_query/live_query.rb +45 -3
  11. data/app/volt/tasks/live_query/live_query_pool.rb +9 -1
  12. data/app/volt/tasks/query_tasks.rb +20 -2
  13. data/app/volt/tasks/store_tasks.rb +37 -20
  14. data/app/volt/tasks/user_tasks.rb +15 -13
  15. data/lib/volt/boot.rb +5 -3
  16. data/lib/volt/cli/console.rb +1 -0
  17. data/lib/volt/cli/generate.rb +15 -0
  18. data/lib/volt/cli.rb +19 -12
  19. data/lib/volt/config.rb +1 -1
  20. data/lib/volt/controllers/model_controller.rb +13 -3
  21. data/lib/volt/extra_core/extra_core.rb +1 -0
  22. data/lib/volt/extra_core/hash.rb +26 -0
  23. data/lib/volt/extra_core/object.rb +5 -1
  24. data/lib/volt/models/array_model.rb +86 -35
  25. data/lib/volt/models/associations.rb +53 -0
  26. data/lib/volt/models/buffer.rb +22 -10
  27. data/lib/volt/models/dirty.rb +88 -0
  28. data/lib/volt/models/errors.rb +21 -0
  29. data/lib/volt/models/field_helpers.rb +2 -2
  30. data/lib/volt/models/listener_tracker.rb +17 -0
  31. data/lib/volt/models/model.rb +213 -69
  32. data/lib/volt/models/model_helpers.rb +27 -17
  33. data/lib/volt/models/permissions.rb +246 -0
  34. data/lib/volt/models/persistors/array_store.rb +149 -81
  35. data/lib/volt/models/persistors/base.rb +16 -0
  36. data/lib/volt/models/persistors/cookies.rb +14 -9
  37. data/lib/volt/models/persistors/flash.rb +3 -0
  38. data/lib/volt/models/persistors/local_store.rb +0 -16
  39. data/lib/volt/models/persistors/model_store.rb +1 -2
  40. data/lib/volt/models/persistors/query/normalizer.rb +51 -0
  41. data/lib/volt/models/persistors/query/query_listener.rb +21 -5
  42. data/lib/volt/models/persistors/query/query_listener_pool.rb +0 -9
  43. data/lib/volt/models/persistors/store.rb +8 -0
  44. data/lib/volt/models/persistors/store_state.rb +4 -27
  45. data/lib/volt/models/state_helpers.rb +11 -0
  46. data/lib/volt/models/state_manager.rb +43 -0
  47. data/lib/volt/models/url.rb +5 -5
  48. data/lib/volt/models/validations.rb +38 -41
  49. data/lib/volt/models/validators/email_validator.rb +4 -9
  50. data/lib/volt/models/validators/format_validator.rb +23 -8
  51. data/lib/volt/models/validators/length_validator.rb +2 -2
  52. data/lib/volt/models/validators/numericality_validator.rb +7 -3
  53. data/lib/volt/models/validators/phone_number_validator.rb +4 -9
  54. data/lib/volt/models/validators/presence_validator.rb +2 -2
  55. data/lib/volt/models/validators/unique_validator.rb +2 -2
  56. data/lib/volt/models/validators/user_validation.rb +6 -0
  57. data/lib/volt/models.rb +8 -3
  58. data/lib/volt/page/bindings/attribute_binding.rb +10 -4
  59. data/lib/volt/page/bindings/content_binding.rb +9 -5
  60. data/lib/volt/page/bindings/if_binding.rb +25 -2
  61. data/lib/volt/page/bindings/template_binding.rb +19 -1
  62. data/lib/volt/page/bindings/yield_binding.rb +31 -0
  63. data/lib/volt/page/page.rb +11 -16
  64. data/lib/volt/reactive/class_eventable.rb +71 -0
  65. data/lib/volt/reactive/computation.rb +79 -10
  66. data/lib/volt/reactive/dependency.rb +27 -8
  67. data/lib/volt/reactive/eventable.rb +36 -22
  68. data/lib/volt/reactive/reactive_array.rb +2 -3
  69. data/lib/volt/reactive/reactive_hash.rb +8 -3
  70. data/lib/volt/router/routes.rb +2 -1
  71. data/lib/volt/server/component_templates.rb +0 -2
  72. data/lib/volt/server/html_parser/component_view_scope.rb +59 -0
  73. data/lib/volt/server/html_parser/view_handler.rb +3 -0
  74. data/lib/volt/server/html_parser/view_parser.rb +1 -0
  75. data/lib/volt/server/html_parser/view_scope.rb +17 -41
  76. data/lib/volt/server/rack/component_paths.rb +1 -10
  77. data/lib/volt/server/rack/index_files.rb +9 -4
  78. data/lib/volt/server/rack/opal_files.rb +22 -14
  79. data/lib/volt/server/rack/quiet_common_logger.rb +1 -1
  80. data/lib/volt/server/socket_connection_handler.rb +4 -0
  81. data/lib/volt/spec/setup.rb +26 -0
  82. data/lib/volt/tasks/dispatcher.rb +11 -0
  83. data/lib/volt/utils/event_counter.rb +29 -0
  84. data/lib/volt/utils/generic_pool.rb +12 -0
  85. data/lib/volt/utils/modes.rb +40 -0
  86. data/lib/volt/utils/promise_patch.rb +66 -0
  87. data/lib/volt/utils/timers.rb +33 -0
  88. data/lib/volt/volt/users.rb +48 -5
  89. data/lib/volt.rb +4 -0
  90. data/spec/apps/kitchen_sink/Gemfile +3 -1
  91. data/spec/apps/kitchen_sink/app/main/config/routes.rb +9 -8
  92. data/spec/apps/kitchen_sink/app/main/controllers/main_controller.rb +9 -0
  93. data/spec/apps/kitchen_sink/app/main/controllers/yield_component_controller.rb +5 -0
  94. data/spec/apps/kitchen_sink/app/main/views/main/cookie_test.html +1 -1
  95. data/spec/apps/kitchen_sink/app/main/views/main/index.html +1 -1
  96. data/spec/apps/kitchen_sink/app/main/views/main/main.html +2 -1
  97. data/spec/apps/kitchen_sink/app/main/views/main/yield.html +18 -0
  98. data/spec/apps/kitchen_sink/app/main/views/yield-component/index.html +4 -0
  99. data/spec/extra_core/logger_spec.rb +4 -2
  100. data/spec/integration/user_spec.rb +42 -42
  101. data/spec/integration/yield_spec.rb +18 -0
  102. data/spec/models/associations_spec.rb +37 -0
  103. data/spec/models/dirty_spec.rb +102 -0
  104. data/spec/models/model_spec.rb +64 -8
  105. data/spec/models/model_state_spec.rb +24 -0
  106. data/spec/models/permissions_spec.rb +96 -0
  107. data/spec/models/user_spec.rb +8 -5
  108. data/spec/models/user_validation_spec.rb +24 -0
  109. data/spec/models/validations_spec.rb +44 -5
  110. data/spec/models/validators/email_validator_spec.rb +109 -82
  111. data/spec/models/validators/format_validator_spec.rb +4 -107
  112. data/spec/models/validators/length_validator_spec.rb +9 -9
  113. data/spec/models/validators/phone_number_validator_spec.rb +60 -103
  114. data/spec/models/validators/shared_examples_for_validators.rb +123 -0
  115. data/spec/reactive/class_eventable_spec.rb +37 -0
  116. data/spec/reactive/computation_spec.rb +68 -3
  117. data/spec/reactive/dependency_spec.rb +71 -0
  118. data/spec/reactive/eventable_spec.rb +21 -0
  119. data/spec/reactive/reactive_hash_spec.rb +12 -0
  120. data/spec/router/routes_spec.rb +50 -50
  121. data/spec/server/html_parser/view_parser_spec.rb +0 -3
  122. data/spec/server/rack/component_paths_spec.rb +11 -0
  123. data/spec/server/rack/quite_common_logger_spec.rb +3 -4
  124. data/spec/spec_helper.rb +7 -3
  125. data/templates/component/config/dependencies.rb +1 -7
  126. data/templates/component/config/routes.rb +1 -1
  127. data/templates/component/controllers/main_controller.rb.tt +20 -0
  128. data/templates/component/views/{index → main}/index.html.tt +0 -0
  129. data/templates/newgem/lib/newgem.rb.tt +1 -3
  130. data/templates/project/app/main/config/routes.rb +3 -3
  131. data/templates/project/app/main/views/main/main.html.tt +4 -4
  132. data/templates/project/config/app.rb.tt +6 -0
  133. data/volt.gemspec +11 -7
  134. metadata +96 -42
  135. data/lib/volt/models/model_state.rb +0 -21
  136. data/templates/component/controllers/main_controller.rb +0 -18
@@ -17,12 +17,14 @@ module Volt
17
17
  # Listen for changes
18
18
  @computation = -> do
19
19
  begin
20
- update(@context.instance_eval(&@getter))
20
+ @context.instance_eval(&@getter)
21
21
  rescue => e
22
22
  Volt.logger.error("AttributeBinding Error: #{e.inspect}")
23
- update('')
23
+ ''
24
24
  end
25
- end.watch!
25
+ end.watch_and_resolve! do |result|
26
+ update(result)
27
+ end
26
28
 
27
29
  @is_radio = element.is('[type=radio]')
28
30
  if @is_radio
@@ -122,9 +124,13 @@ module Volt
122
124
  element.off('change.attrbind', nil)
123
125
  end
124
126
 
127
+ if @computation
128
+ @computation.stop
129
+ @computation = nil
130
+ end
131
+
125
132
  @string_template_renderer.remove if @string_template_renderer
126
133
  @string_template_renderer_computation.stop if @string_template_renderer_computation
127
- @computation.stop if @computation
128
134
 
129
135
  # Clear any references
130
136
  @target = nil
@@ -9,12 +9,14 @@ module Volt
9
9
  # Listen for changes
10
10
  @computation = -> do
11
11
  begin
12
- update(@context.instance_eval(&getter))
12
+ res = @context.instance_eval(&getter)
13
13
  rescue => e
14
14
  Volt.logger.error("ContentBinding Error: #{e.inspect}")
15
- update('')
15
+ ''
16
16
  end
17
- end.watch!
17
+ end.watch_and_resolve! do |result|
18
+ update(result)
19
+ end
18
20
  end
19
21
 
20
22
  def update(value)
@@ -29,8 +31,10 @@ module Volt
29
31
  end
30
32
 
31
33
  def remove
32
- @computation.stop if @computation
33
- @computation = nil
34
+ if @computation
35
+ @computation.stop
36
+ @computation = nil
37
+ end
34
38
 
35
39
  super
36
40
  end
@@ -24,7 +24,15 @@ module Volt
24
24
  @branches << [value, template_name]
25
25
  end
26
26
 
27
- @computation = -> { update }.watch!
27
+ # The promise dependency can be invalidated when we need to rerun the update
28
+ # manually because a promise resolved.
29
+ @promise_dependency = Dependency.new
30
+
31
+ @computation = -> do
32
+ @promise_dependency.depend
33
+
34
+ update
35
+ end.watch!
28
36
  end
29
37
 
30
38
  def update
@@ -36,6 +44,22 @@ module Volt
36
44
  if value.is_a?(Proc)
37
45
  begin
38
46
  current_value = @context.instance_eval(&value)
47
+
48
+ if current_value.is_a?(Promise)
49
+ # If we got a promise, use its value if resolved.
50
+ if current_value.resolved?
51
+ current_value = current_value.value
52
+ else
53
+ # if its not, resolve it and try again.
54
+ # TODO: we maybe could cache this so we don't have to run a full update again
55
+ current_value.then do |val|
56
+ # Run update again
57
+ @promise_dependency.changed!
58
+ end
59
+
60
+ current_value = nil
61
+ end
62
+ end
39
63
  rescue => e
40
64
  Volt.logger.error("IfBinding:#{object_id} error: #{e.inspect}\n" + `value.toString()`)
41
65
  current_value = false
@@ -44,7 +68,6 @@ module Volt
44
68
  current_value = value
45
69
  end
46
70
 
47
- # TODO: A bug in opal requires us to check == true
48
71
  if current_value && !current_value.nil? && !current_value.is_a?(Exception)
49
72
  # This branch is currently true
50
73
  true_template = template_name
@@ -3,11 +3,19 @@ require 'volt/page/template_renderer'
3
3
  require 'volt/page/bindings/template_binding/grouped_controllers'
4
4
  require 'volt/page/bindings/template_binding/view_lookup_for_path'
5
5
 
6
+
6
7
  module Volt
7
8
  class TemplateBinding < BaseBinding
8
- def initialize(page, target, context, binding_name, binding_in_path, getter)
9
+
10
+ # @param [String] binding_in_path is the path this binding was rendered from. Used to
11
+ # lookup paths in ViewLookupForPath
12
+ # @param [String|nil] content_template_path is the path to the template for the content
13
+ # provided in the tag.
14
+ def initialize(page, target, context, binding_name, binding_in_path, getter, content_template_path=nil)
9
15
  super(page, target, context, binding_name)
10
16
 
17
+ @content_template_path = content_template_path
18
+
11
19
  # Setup the view lookup helper
12
20
  @view_lookup = Volt::ViewLookupForPath.new(page, binding_in_path)
13
21
 
@@ -47,6 +55,13 @@ module Volt
47
55
  else
48
56
  # Use the value passed in as the default arguments
49
57
  @arguments = section_or_arguments
58
+
59
+ # Include content_template_path in attrs
60
+ if @content_template_path
61
+ @arguments ||= {}
62
+ @arguments[:content_template_path] = @content_template_path
63
+ @arguments[:content_controller] = @context
64
+ end
50
65
  end
51
66
 
52
67
  # Sometimes we want multiple template bindings to share the same controller (usually
@@ -140,6 +155,9 @@ module Volt
140
155
  end
141
156
 
142
157
  def remove
158
+ @computation.stop
159
+ @computation = nil
160
+
143
161
  controller_send(:"before_#{@action}_remove") if @controller && @action
144
162
 
145
163
  clear_grouped_controller
@@ -0,0 +1,31 @@
1
+ # The yield binding renders the content of a tag which passes in
2
+
3
+ require 'volt/page/bindings/base_binding'
4
+ require 'volt/page/template_renderer'
5
+
6
+ module Volt
7
+ class YieldBinding < BaseBinding
8
+ def initialize(page, target, context, binding_name)
9
+ super(page, target, context, binding_name)
10
+
11
+ # Get the path to the template to yield
12
+ full_path = @context.attrs.content_template_path
13
+
14
+ # Grab the controller for the content
15
+ controller = @context.attrs.content_controller
16
+
17
+ @current_template = TemplateRenderer.new(@page, @target, controller, @binding_name, full_path)
18
+ end
19
+
20
+ def remove
21
+ if @current_template
22
+ # Remove the template if one has been rendered, when the template binding is
23
+ # removed.
24
+ @current_template.remove
25
+ end
26
+
27
+ super
28
+
29
+ end
30
+ end
31
+ end
@@ -1,5 +1,5 @@
1
+ require 'opal'
1
2
  if RUBY_PLATFORM == 'opal'
2
- require 'opal'
3
3
  require 'opal-jquery'
4
4
  end
5
5
  require 'volt/models'
@@ -10,6 +10,7 @@ require 'volt/page/bindings/content_binding'
10
10
  require 'volt/page/bindings/each_binding'
11
11
  require 'volt/page/bindings/if_binding'
12
12
  require 'volt/page/bindings/template_binding'
13
+ require 'volt/page/bindings/yield_binding'
13
14
  require 'volt/page/bindings/component_binding'
14
15
  require 'volt/page/bindings/event_binding'
15
16
  require 'volt/page/template_renderer'
@@ -31,11 +32,9 @@ require 'volt/page/tasks'
31
32
 
32
33
  module Volt
33
34
  class Page
34
- attr_reader :url, :params, :page, :templates, :routes, :events, :model_classes
35
+ attr_reader :url, :params, :page, :templates, :routes, :events
35
36
 
36
37
  def initialize
37
- @model_classes = {}
38
-
39
38
  # Run the code to setup the page
40
39
  @page = Model.new
41
40
 
@@ -139,14 +138,6 @@ module Volt
139
138
 
140
139
  attr_reader :events
141
140
 
142
- def add_model(model_name)
143
- model_name = model_name.camelize.to_sym
144
- @model_classes[model_name] = Object.const_get(model_name)
145
- rescue NameError => e
146
- # Handle if the model is user (Volt's provided user model is scoped under Volt::)
147
- raise unless model_name == :User
148
- end
149
-
150
141
  def add_template(name, template, bindings)
151
142
  @templates ||= {}
152
143
 
@@ -159,7 +150,6 @@ module Volt
159
150
  unless @templates[name]
160
151
  @templates[name] = { 'html' => template, 'bindings' => bindings }
161
152
  end
162
- # puts "Add Template: #{name}"
163
153
  end
164
154
 
165
155
  def add_routes(&block)
@@ -210,7 +200,7 @@ module Volt
210
200
  end
211
201
  end
212
202
  rescue => e
213
- puts "Unable to restore: #{e.inspect}"
203
+ Volt.logger.error("Unable to restore: #{e.inspect}")
214
204
  end
215
205
  end
216
206
 
@@ -218,8 +208,13 @@ module Volt
218
208
  $page = Page.new
219
209
 
220
210
  # Call start once the page is loaded
221
- Document.ready? do
211
+ # Document.ready? do
212
+ # $page.start
213
+ # end
214
+
215
+ # For some reason Document.ready? (using opal-jquery) quit working.
216
+ `$(document).ready(function() {`
222
217
  $page.start
223
- end
218
+ `});`
224
219
  end
225
220
  end
@@ -0,0 +1,71 @@
1
+ require 'volt/reactive/eventable'
2
+
3
+ module Volt
4
+ # ClassEventable behaves like Eventable, except events can be bound with a class #on method.
5
+ # When triggered on an instance, the self in the block will be the instance it was triggered
6
+ # on. This allows classes to easy setup listeners.
7
+ #
8
+ # Example:
9
+ # class Post
10
+ # on(:create) do
11
+ # deny if owner?
12
+ # end
13
+ # end
14
+ module ClassEventable
15
+ module ClassMethods
16
+ # Eventable also provides a static version of on, which allows you to setup on
17
+ # events at the class level. When the event triggers, self will be set to the
18
+ # instance it was triggered on.
19
+ def on(*events, &callback)
20
+ raise '.on requires an event' if events.size == 0
21
+
22
+ listener = Listener.new(self, events, callback)
23
+
24
+ self.__listeners__ ||= {}
25
+
26
+ events.each do |event|
27
+ listeners = self.__listeners__
28
+ listeners[event] ||= []
29
+ listeners[event] << listener
30
+ end
31
+ end
32
+
33
+ def remove_listener(event, listener)
34
+ listeners = self.__listeners__
35
+ if listeners
36
+ listeners[event].delete(listener)
37
+
38
+ if listeners[event].size == 0
39
+ # No registered listeners now on this event
40
+ listeners.delete(event)
41
+ end
42
+ end
43
+ end
44
+ end
45
+
46
+ module InstanceMethods
47
+ # Extend trigger! to also trigger class listeners
48
+ def trigger!(event, *args)
49
+ event = event.to_sym
50
+
51
+ super
52
+
53
+ if (klass_listeners = self.class.__listeners__)
54
+ klass_listeners[event].dup.each do |listener|
55
+ # Call each class listener with self set to the current instance
56
+ listener.instance_call(self, *args)
57
+ end
58
+ end
59
+ end
60
+ end
61
+
62
+ def self.included(base)
63
+ base.class_attribute :__listeners__
64
+
65
+ # Include the base eventable so the class can be triggered on
66
+ base.send :include, Volt::Eventable
67
+ base.send :extend, ClassMethods
68
+ base.send :include, InstanceMethods
69
+ end
70
+ end
71
+ end
@@ -11,12 +11,14 @@ module Volt
11
11
  @@current
12
12
  end
13
13
 
14
+ # @param [Proc] the code to run when the computation needs to compute
14
15
  def initialize(computation)
15
16
  @computation = computation
16
17
  @invalidations = []
17
18
  end
18
19
 
19
- # Runs the computation
20
+ # Runs the computation, called on initial run and
21
+ # when changed!
20
22
  def compute!
21
23
  @invalidated = false
22
24
 
@@ -24,7 +26,12 @@ module Volt
24
26
 
25
27
  @computing = true
26
28
  run_in do
27
- @computation.call
29
+ if @computation.arity > 0
30
+ # Pass in the Computation so it can be canceled from within
31
+ @computation.call(self)
32
+ else
33
+ @computation.call
34
+ end
28
35
  end
29
36
  @computing = false
30
37
  end
@@ -49,7 +56,7 @@ module Volt
49
56
  unless @invalidated
50
57
  @invalidated = true
51
58
 
52
- if !@stopped && !@computing
59
+ unless @stopped || @computing
53
60
  @@flush_queue << self
54
61
 
55
62
  # If we are in the browser, we queue a flush for the next tick
@@ -79,16 +86,24 @@ module Volt
79
86
  def run_in
80
87
  previous = Computation.current
81
88
  Computation.current = self
82
- yield
83
- Computation.current = previous
89
+ begin
90
+ yield
91
+ ensure
92
+ Computation.current = previous
93
+ end
94
+
84
95
  self
85
96
  end
86
97
 
98
+ # Run a block without tracking any dependencies
87
99
  def self.run_without_tracking
88
100
  previous = Computation.current
89
101
  Computation.current = nil
90
- return_value = yield
91
- Computation.current = previous
102
+ begin
103
+ return_value = yield
104
+ ensure
105
+ Computation.current = previous
106
+ end
92
107
  return_value
93
108
  end
94
109
 
@@ -118,9 +133,63 @@ end
118
133
 
119
134
  class Proc
120
135
  def watch!
121
- Volt::Computation.new(self).run_in do
122
- # run self
123
- call
136
+ computation = Volt::Computation.new(self)
137
+
138
+ # Initial run
139
+ computation.compute!
140
+
141
+ # return the computation
142
+ computation
143
+ end
144
+
145
+ # Watches a proc until the value returned equals the passed
146
+ # in value. When the value matches, the block is called.
147
+ #
148
+ # @param the value to match
149
+ # @return [Volt::Computation] the initial computation is returned.
150
+ def watch_until!(value, &block)
151
+ computation = -> do
152
+ # First fetch the value
153
+ result = self.call
154
+
155
+ if result == value
156
+ # Values match
157
+
158
+ # call the block
159
+ block.call
160
+
161
+ # stop the computation
162
+ computation.stop
163
+ end
164
+ end.watch!
165
+
166
+ computation
167
+ end
168
+
169
+ # Does an watch and if the result is a promise, resolves the promise.
170
+ # #watch_and_resolve! takes a block that will be passed the resolved results
171
+ # of the proc.
172
+ #
173
+ # Example:
174
+ # -> { }
175
+ def watch_and_resolve!
176
+ unless block_given?
177
+ raise "watch_and_resolve! requires a block to call when the value is resolved or another value other than a promise is returned in the watch."
124
178
  end
179
+
180
+ computation = Proc.new do
181
+ result = self.call
182
+
183
+ if result.is_a?(Promise)
184
+ result.then do |final|
185
+ yield(final)
186
+ end
187
+ else
188
+ yield(result)
189
+ end
190
+ end.watch!
191
+
192
+ # Return the computation
193
+ computation
125
194
  end
126
195
  end
@@ -3,12 +3,9 @@ require 'set'
3
3
 
4
4
  class Set
5
5
  def delete(o)
6
- @hash.delete(o)
7
- end
8
-
9
- def delete?(o)
10
6
  if include?(o)
11
- delete(o)
7
+ @hash.delete(o)
8
+ true
12
9
  else
13
10
  nil
14
11
  end
@@ -28,9 +25,21 @@ class Set
28
25
  end
29
26
 
30
27
  module Volt
28
+ # Dependencies are used to track the current computation so it can be re-run
29
+ # at a later point if this dependency changes.
30
+ #
31
+ # You can also pass an on_dep and on_stop_dep proc's to #initialize.
31
32
  class Dependency
32
- def initialize
33
+ # Setup a new dependency.
34
+ #
35
+ # @param on_dep [Proc] a proc to be called the first time a computation depends
36
+ # on this dependency.
37
+ # @param on_stop_dep [Proc] a proc to be called when no computations are depending
38
+ # on this dependency anymore.
39
+ def initialize(on_dep=nil, on_stop_dep=nil)
33
40
  @dependencies = Set.new
41
+ @on_dep = on_dep
42
+ @on_stop_dep = on_stop_dep
34
43
  end
35
44
 
36
45
  def depend
@@ -42,10 +51,18 @@ module Volt
42
51
  added = @dependencies.add?(current)
43
52
 
44
53
  if added
45
- # puts "Added #{self.inspect} to #{current.inspect}"
54
+ # The first time the dependency is depended on by this computation, we call on_dep
55
+ @on_dep.call if @on_dep && @dependencies.size == 1
56
+
46
57
  current.on_invalidate do
47
58
  # If @dependencies is nil, this Dependency has been removed
48
- @dependencies.delete(current) if @dependencies
59
+ if @dependencies
60
+ # For set, .delete returns a boolean if it was deleted
61
+ deleted = @dependencies.delete(current)
62
+
63
+ # Call on stop dep if no more deps
64
+ @on_stop_dep.call if @on_stop_dep && deleted && @dependencies.size == 0
65
+ end
49
66
  end
50
67
  end
51
68
  end
@@ -60,6 +77,8 @@ module Volt
60
77
  @dependencies = Set.new
61
78
 
62
79
  deps.each(&:invalidate!)
80
+
81
+ @on_stop_dep.call if @on_stop_dep
63
82
  end
64
83
 
65
84
  # Called when a dependency is no longer needed
@@ -2,9 +2,9 @@ module Volt
2
2
  # Listeners are returned from #on on a class with Eventable included.
3
3
  # Listeners can be stopped by calling #remove
4
4
  class Listener
5
- def initialize(klass, event, callback)
5
+ def initialize(klass, events, callback)
6
6
  @klass = klass
7
- @event = event
7
+ @events = events
8
8
  @callback = callback
9
9
  end
10
10
 
@@ -12,10 +12,17 @@ module Volt
12
12
  @callback.call(*args) unless @removed
13
13
  end
14
14
 
15
+ # Call the callback with self set to instance
16
+ def instance_call(instance, *args)
17
+ instance.instance_exec(*args, &@callback)
18
+ end
19
+
15
20
  def remove
16
21
  @removed = true
17
22
 
18
- @klass.remove_listener(@event, self)
23
+ @events.each do |event|
24
+ @klass.remove_listener(event, self)
25
+ end
19
26
 
20
27
  # Make things easier on the GC
21
28
  @klass = nil
@@ -23,7 +30,7 @@ module Volt
23
30
  end
24
31
 
25
32
  def inspect
26
- "<Listener:#{object_id} event=#{@event}>"
33
+ "<Listener:#{object_id} events=#{@events}>"
27
34
  end
28
35
  end
29
36
 
@@ -35,37 +42,44 @@ module Volt
35
42
  # the class, it will trigger any listener with the same event name.
36
43
  #
37
44
  # returns: a listener that has a #remove method to stop the listener.
38
- def on(event, &callback)
39
- event = event.to_sym
40
- listener = Listener.new(self, event, callback)
45
+ def on(*events, &callback)
46
+ raise '.on requires an event' if events.size == 0
47
+
48
+ listener = Listener.new(self, events, callback)
49
+
41
50
  @listeners ||= {}
42
- @listeners[event] ||= []
43
- @listeners[event] << listener
44
51
 
45
- first_for_event = @listeners[event].size == 1
46
- first = first_for_event && @listeners.size == 1
52
+ events.each do |event|
53
+ event = event.to_sym
54
+ @listeners[event] ||= []
55
+ @listeners[event] << listener
56
+
57
+ first_for_event = @listeners[event].size == 1
58
+ first = first_for_event && @listeners.size == 1
47
59
 
48
- # Let the included class know that an event was registered. (if it cares)
49
- if self.respond_to?(:event_added)
50
- # call event added passing the event, the scope, and a boolean if it
51
- # is the first time this event has been added.
52
- event_added(event, first, first_for_event)
60
+ # Let the included class know that an event was registered. (if it cares)
61
+ if self.respond_to?(:event_added)
62
+ # call event added passing the event, the scope, and a boolean if it
63
+ # is the first time this event has been added.
64
+ event_added(event, first, first_for_event)
65
+ end
53
66
  end
54
67
 
55
68
  listener
56
69
  end
57
70
 
71
+
58
72
  # Triggers event on the class the module was includeded. Any .on listeners
59
73
  # will have their block called passing in *args.
60
74
  def trigger!(event, *args)
61
75
  event = event.to_sym
62
76
 
63
- return unless @listeners && @listeners[event]
64
-
65
- # TODO: We have to dup here because one trigger might remove another
66
- @listeners[event].dup.each do |listener|
67
- # Call the event on each listener
68
- listener.call(*args)
77
+ if @listeners && @listeners[event]
78
+ # TODO: We have to dup here because one trigger might remove another
79
+ @listeners[event].dup.each do |listener|
80
+ # Call the event on each listener
81
+ listener.call(*args)
82
+ end
69
83
  end
70
84
  end
71
85
 
@@ -91,7 +91,7 @@ module Volt
91
91
 
92
92
  # TODO: Handle a range
93
93
  def [](index)
94
- # Handle a negative index
94
+ # Handle a negative index, depend on size
95
95
  index = size + index if index < 0
96
96
 
97
97
  # Get or create the dependency
@@ -125,7 +125,7 @@ module Volt
125
125
  # Handle a negative index
126
126
  index = size + index if index < 0
127
127
 
128
- model = @array.delete_at(index)
128
+ model = @array.delete_at(index)
129
129
 
130
130
  # Remove the dependency for that cell, and #remove it
131
131
  index_deps = @array_deps.delete_at(index)
@@ -244,7 +244,6 @@ module Volt
244
244
  def trigger_for_index!(index)
245
245
  # Trigger a change for the cell
246
246
  dep = @array_deps[index]
247
-
248
247
  dep.changed! if dep
249
248
  end
250
249
 
@@ -13,10 +13,13 @@ module Volt
13
13
  @hash == val
14
14
  end
15
15
 
16
+ def blank?
17
+ @hash.blank?
18
+ end
19
+
16
20
  # TODO: We should finish off this class for reactivity
17
21
  def method_missing(method_name, *args, &block)
18
22
  @all_deps.depend
19
-
20
23
  @hash.send(method_name, *args, &block)
21
24
  end
22
25
 
@@ -40,7 +43,9 @@ module Volt
40
43
  end
41
44
 
42
45
  def clear
43
- @hash.each_pair do |key, _|
46
+ # Don't use .each_key so we get a clone here since we are
47
+ # deleting as we go.
48
+ @hash.keys.each do |key|
44
49
  delete(key)
45
50
  end
46
51
 
@@ -62,7 +67,7 @@ module Volt
62
67
 
63
68
  def inspect
64
69
  @all_deps.depend
65
- "#<ReactiveHash #{@hash.inspect}>"
70
+ "#<#{self.class.name} #{@hash.inspect}>"
66
71
  end
67
72
  end
68
73
  end