volt 0.7.23 → 0.8.0

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 (106) hide show
  1. checksums.yaml +4 -4
  2. data/.travis.yml +8 -1
  3. data/CHANGELOG.md +22 -0
  4. data/Gemfile +8 -0
  5. data/Guardfile +2 -2
  6. data/Readme.md +139 -136
  7. data/VERSION +1 -1
  8. data/app/volt/assets/js/setImmediate.js +175 -0
  9. data/app/volt/tasks/live_query/data_store.rb +0 -2
  10. data/app/volt/tasks/live_query/live_query.rb +4 -4
  11. data/docs/GETTING_STARTED.md +24 -3
  12. data/docs/WHY.md +1 -22
  13. data/lib/volt.rb +20 -1
  14. data/lib/volt/console.rb +20 -0
  15. data/lib/volt/controllers/model_controller.rb +25 -11
  16. data/lib/volt/extra_core/object.rb +2 -14
  17. data/lib/volt/extra_core/string.rb +4 -0
  18. data/lib/volt/models.rb +0 -1
  19. data/lib/volt/models/array_model.rb +8 -16
  20. data/lib/volt/models/cursor.rb +1 -1
  21. data/lib/volt/models/model.rb +40 -60
  22. data/lib/volt/models/model_hash_behaviour.rb +10 -24
  23. data/lib/volt/models/model_helpers.rb +2 -2
  24. data/lib/volt/models/model_state.rb +1 -1
  25. data/lib/volt/models/model_wrapper.rb +4 -4
  26. data/lib/volt/models/persistors/array_store.rb +44 -28
  27. data/lib/volt/models/persistors/base.rb +1 -1
  28. data/lib/volt/models/persistors/model_store.rb +1 -1
  29. data/lib/volt/models/persistors/params.rb +5 -1
  30. data/lib/volt/models/persistors/query/query_listener.rb +2 -0
  31. data/lib/volt/models/persistors/store.rb +3 -2
  32. data/lib/volt/models/persistors/store_state.rb +7 -2
  33. data/lib/volt/models/url.rb +35 -29
  34. data/lib/volt/models/validations.rb +7 -17
  35. data/lib/volt/page/bindings/attribute_binding.rb +57 -39
  36. data/lib/volt/page/bindings/base_binding.rb +0 -14
  37. data/lib/volt/page/bindings/content_binding.rb +15 -18
  38. data/lib/volt/page/bindings/each_binding.rb +67 -34
  39. data/lib/volt/page/bindings/if_binding.rb +15 -12
  40. data/lib/volt/page/bindings/template_binding.rb +77 -59
  41. data/lib/volt/page/bindings/template_binding/grouped_controllers.rb +19 -4
  42. data/lib/volt/page/channel.rb +22 -38
  43. data/lib/volt/page/channel_stub.rb +3 -6
  44. data/lib/volt/page/page.rb +24 -26
  45. data/lib/volt/page/string_template_renderer.rb +46 -0
  46. data/lib/volt/page/sub_context.rb +7 -1
  47. data/lib/volt/page/targets/binding_document/component_node.rb +11 -9
  48. data/lib/volt/page/tasks.rb +3 -2
  49. data/lib/volt/page/url_tracker.rb +4 -3
  50. data/lib/volt/reactive/computation.rb +131 -0
  51. data/lib/volt/reactive/dependency.rb +71 -0
  52. data/lib/volt/reactive/eventable.rb +82 -0
  53. data/lib/volt/reactive/hash_dependency.rb +36 -0
  54. data/lib/volt/{controllers → reactive}/reactive_accessors.rb +8 -11
  55. data/lib/volt/reactive/reactive_array.rb +100 -193
  56. data/lib/volt/reactive/reactive_hash.rb +49 -0
  57. data/lib/volt/server/html_parser/attribute_scope.rb +24 -4
  58. data/lib/volt/server/html_parser/if_view_scope.rb +15 -15
  59. data/lib/volt/server/html_parser/view_scope.rb +31 -1
  60. data/spec/apps/kitchen_sink/Gemfile +4 -8
  61. data/spec/apps/kitchen_sink/app/main/config/dependencies.rb +8 -0
  62. data/spec/apps/kitchen_sink/app/main/config/routes.rb +8 -1
  63. data/spec/apps/kitchen_sink/app/main/controllers/main_controller.rb +8 -0
  64. data/spec/apps/kitchen_sink/app/main/views/main/bindings.html +73 -0
  65. data/spec/apps/kitchen_sink/app/main/views/main/index.html +6 -1
  66. data/spec/apps/kitchen_sink/app/main/views/main/main.html +26 -6
  67. data/spec/apps/kitchen_sink/app/main/views/main/store.html +6 -0
  68. data/spec/controllers/reactive_accessors_spec.rb +13 -15
  69. data/spec/integration/bindings_spec.rb +159 -0
  70. data/spec/integration/templates_spec.rb +15 -0
  71. data/spec/models/model_spec.rb +130 -228
  72. data/spec/reactive/computation_spec.rb +63 -0
  73. data/spec/reactive/dependency_spec.rb +5 -0
  74. data/spec/reactive/eventable_spec.rb +48 -0
  75. data/spec/reactive/reactive_array_spec.rb +97 -0
  76. data/spec/router/routes_spec.rb +26 -27
  77. data/spec/server/html_parser/view_parser_spec.rb +3 -21
  78. data/spec/server/rack/asset_files_spec.rb +1 -1
  79. data/templates/project/app/main/views/main/main.html +2 -2
  80. metadata +29 -41
  81. data/lib/volt/extra_core/time.rb +0 -16
  82. data/lib/volt/page/draw_cycle.rb +0 -31
  83. data/lib/volt/page/memory_test.rb +0 -26
  84. data/lib/volt/page/reactive_template.rb +0 -32
  85. data/lib/volt/reactive/array_extensions.rb +0 -12
  86. data/lib/volt/reactive/destructive_methods.rb +0 -19
  87. data/lib/volt/reactive/event_chain.rb +0 -125
  88. data/lib/volt/reactive/events.rb +0 -216
  89. data/lib/volt/reactive/object_tracking.rb +0 -14
  90. data/lib/volt/reactive/reactive_block.rb +0 -88
  91. data/lib/volt/reactive/reactive_generator.rb +0 -44
  92. data/lib/volt/reactive/reactive_tags.rb +0 -71
  93. data/lib/volt/reactive/reactive_value.rb +0 -427
  94. data/lib/volt/reactive/string_extensions.rb +0 -31
  95. data/spec/integration/test_integration_spec.rb +0 -14
  96. data/spec/models/event_chain_spec.rb +0 -150
  97. data/spec/models/model_buffers_spec.rb +0 -9
  98. data/spec/models/old_model_spec.rb +0 -67
  99. data/spec/models/reactive_array_spec.rb +0 -364
  100. data/spec/models/reactive_block_spec.rb +0 -13
  101. data/spec/models/reactive_call_times_spec.rb +0 -28
  102. data/spec/models/reactive_generator_spec.rb +0 -58
  103. data/spec/models/reactive_tags_spec.rb +0 -35
  104. data/spec/models/reactive_value_spec.rb +0 -370
  105. data/spec/models/store_spec.rb +0 -16
  106. data/spec/models/string_extensions_spec.rb +0 -57
@@ -2,24 +2,23 @@ require 'volt/page/bindings/base_binding'
2
2
 
3
3
  class ContentBinding < BaseBinding
4
4
  def initialize(page, target, context, binding_name, getter)
5
+ # puts "New Content Binding: #{self.inspect}"
5
6
  super(page, target, context, binding_name)
6
7
 
7
- # Find the source for the content binding
8
- @value = value_from_getter(getter)
9
-
10
- # Run the initial render
11
- update
12
-
13
- if @value.reactive?
14
- @changed_listener = @value.on('changed') { update }
15
- end
8
+ # Listen for changes
9
+ @computation = -> do
10
+ begin
11
+ update(@context.instance_eval(&getter))
12
+ rescue => e
13
+ Volt.logger.error("ContentBinding Error: #{e.inspect}")
14
+ update('')
15
+ end
16
+ end.watch!
16
17
  end
17
18
 
18
- def update
19
- value = @value.cur.or('')
20
- if value.reactive?
21
- puts "GOT CUR: #{value.inspect}"
22
- end
19
+ def update(value)
20
+ # TODORW:
21
+ value = value.nil? ? '' : value
23
22
 
24
23
  # Exception values display the exception as a string
25
24
  value = value.to_s
@@ -30,10 +29,8 @@ class ContentBinding < BaseBinding
30
29
  end
31
30
 
32
31
  def remove
33
- if @changed_listener
34
- @changed_listener.remove
35
- @changed_listener = nil
36
- end
32
+ @computation.stop if @computation
33
+ @computation = nil
37
34
 
38
35
  super
39
36
  end
@@ -7,40 +7,57 @@ class EachBinding < BaseBinding
7
7
  @item_name = variable_name
8
8
  @template_name = template_name
9
9
 
10
- # Find the source for the content binding
11
- @value = value_from_getter(getter)
12
-
13
10
  @templates = []
14
11
 
15
- # Run the initial render
16
- # update
17
- reload
12
+ @getter = getter
18
13
 
19
- @added_listener = @value.on('added') { |_, position, item| item_added(position) }
20
- @changed_listener = @value.on('changed') { reload }
21
- @removed_listener = @value.on('removed') { |_, position| item_removed(position) }
14
+ # Listen for changes
15
+ @computation = -> { reload }.watch!
22
16
  end
23
17
 
24
18
  # When a changed event happens, we update to the new size.
25
19
  def reload
26
- # Adjust to the new size
27
- values = current_values
28
- templates_size = @templates.size
29
- values_size = values.size
20
+ begin
21
+ value = @context.instance_eval(&@getter)
22
+ rescue => e
23
+ Volt.logger.error("EachBinding Error: #{e.inspect}")
24
+ value = []
25
+ end
30
26
 
31
- if templates_size < values_size
32
- (templates_size).upto(values_size-1) do |index|
33
- item_added(index)
27
+ # Since we're checking things like size, we don't want this to be re-triggered on a
28
+ # size change, so we run without tracking.
29
+ Computation.run_without_tracking do
30
+ # puts "RELOAD:-------------- #{value.inspect}"
31
+ # Adjust to the new size
32
+ values = current_values(value)
33
+ @value = values
34
+
35
+ @added_listener.remove if @added_listener
36
+ @removed_listener.remove if @removed_listener
37
+
38
+ if @value.respond_to?(:on)
39
+ @added_listener = @value.on('added') { |position| item_added(position) }
40
+ @removed_listener = @value.on('removed') { |position| item_removed(position) }
34
41
  end
35
- elsif templates_size > values_size
36
- (templates_size-1).downto(values_size) do |index|
42
+
43
+ templates_size = @templates.size
44
+ values_size = values.size
45
+
46
+ # Start over, re-create all nodes
47
+ (templates_size-1).downto(0) do |index|
37
48
  item_removed(index)
38
49
  end
50
+ 0.upto(values_size-1) do |index|
51
+ item_added(index)
52
+ end
39
53
  end
40
54
  end
41
55
 
42
56
  def item_removed(position)
43
- position = position.cur
57
+ # Remove dependency
58
+ @templates[position].context.locals[:index_dependency].remove
59
+
60
+ # puts "REMOVE AT: #{position.inspect} - #{@templates[position].inspect} - #{@templates.inspect}"
44
61
  @templates[position].remove_anchors
45
62
  @templates[position].remove
46
63
  @templates.delete_at(position)
@@ -50,7 +67,6 @@ class EachBinding < BaseBinding
50
67
  end
51
68
 
52
69
  def item_added(position)
53
- # ObjectTracker.enable_cache
54
70
  binding_name = @@binding_number
55
71
  @@binding_number += 1
56
72
 
@@ -62,15 +78,28 @@ class EachBinding < BaseBinding
62
78
  dom_section.insert_anchor_before(binding_name, @templates[position].binding_name)
63
79
  end
64
80
 
65
- index = ReactiveValue.new(position)
66
- value = @value[index]
81
+ # TODORW: :parent => @value may change
82
+ item_context = SubContext.new({:_index_value => position, :parent => @value}, @context)
83
+ item_context.locals[@item_name.to_sym] = Proc.new { @value[item_context.locals[:_index_value]] }
67
84
 
68
- item_context = SubContext.new({@item_name => value, :index => index, :parent => @value}, @context)
85
+ position_dependency = Dependency.new
86
+ item_context.locals[:index_dependency] = position_dependency
87
+
88
+ # Get and set index
89
+ item_context.locals[:index=] = Proc.new do |val|
90
+ position_dependency.changed!
91
+ item_context.locals[:_index_value] = val
92
+ end
93
+
94
+ item_context.locals[:index] = Proc.new do
95
+ position_dependency.depend
96
+ item_context.locals[:_index_value]
97
+ end
69
98
 
70
99
  item_template = TemplateRenderer.new(@page, @target, item_context, binding_name, @template_name)
71
100
  @templates.insert(position, item_template)
72
101
 
73
- # update_indexes_after(position)
102
+ update_indexes_after(position)
74
103
  end
75
104
 
76
105
  # When items are added or removed in the middle of the list, we need
@@ -78,18 +107,15 @@ class EachBinding < BaseBinding
78
107
  def update_indexes_after(start_index)
79
108
  size = @templates.size
80
109
  if size > 0
81
- puts @templates.inspect
82
110
  start_index.upto(size-1) do |index|
83
- @templates[index].context.locals[:index].cur = index
111
+ @templates[index].context.locals[:index=].call(index)
84
112
  end
85
113
  end
86
114
  end
87
115
 
88
- def current_values
89
- values = @value.cur
90
-
116
+ def current_values(values)
91
117
  return [] if values.is_a?(Model) || values.is_a?(Exception)
92
- values = values.attributes unless values.is_a?(ReactiveArray)
118
+ values = values.attributes if values.respond_to?(:attributes)
93
119
 
94
120
  return values
95
121
  end
@@ -97,17 +123,24 @@ class EachBinding < BaseBinding
97
123
 
98
124
  # When this each_binding is removed, cleanup.
99
125
  def remove
126
+ @computation.stop
127
+ @computation = nil
128
+
129
+ # Clear value
130
+ @value = nil
131
+
100
132
  @added_listener.remove
101
133
  @added_listener = nil
102
134
 
103
- @changed_listener.remove
104
- @changed_listener = nil
105
-
106
135
  @removed_listener.remove
107
136
  @removed_listener = nil
108
137
 
109
138
  if @templates
110
- @templates.compact.each(&:remove)
139
+ template_count = @templates.size
140
+ template_count.times do |index|
141
+ item_removed(template_count - index - 1)
142
+ end
143
+ # @templates.compact.each(&:remove)
111
144
  @templates = nil
112
145
  end
113
146
 
@@ -13,13 +13,7 @@ class IfBinding < BaseBinding
13
13
  getter, template_name = branch
14
14
 
15
15
  if getter.present?
16
- # Lookup the value
17
- value = value_from_getter(getter)
18
-
19
- if value.reactive?
20
- # Trigger change when value changes
21
- @listeners << value.on('changed') { update }
22
- end
16
+ value = getter
23
17
  else
24
18
  # A nil value means this is an unconditional else branch, it
25
19
  # should always be true
@@ -29,7 +23,7 @@ class IfBinding < BaseBinding
29
23
  @branches << [value, template_name]
30
24
  end
31
25
 
32
- update
26
+ @computation = -> { update }.watch!
33
27
  end
34
28
 
35
29
  def update
@@ -38,10 +32,19 @@ class IfBinding < BaseBinding
38
32
  @branches.each do |branch|
39
33
  value, template_name = branch
40
34
 
41
- current_value = value.cur
35
+ if value.is_a?(Proc)
36
+ begin
37
+ current_value = @context.instance_eval(&value)
38
+ rescue => e
39
+ Volt.logger.error("IfBinding:#{object_id} error: #{e.inspect}\n" + `value.toString()`)
40
+ current_value = false
41
+ end
42
+ else
43
+ current_value = value
44
+ end
42
45
 
43
46
  # TODO: A bug in opal requires us to check == true
44
- if current_value.true? == true && !current_value.is_a?(Exception)
47
+ if current_value && !current_value.nil? && !current_value.is_a?(Exception)
45
48
  # This branch is currently true
46
49
  true_template = template_name
47
50
  break
@@ -64,8 +67,8 @@ class IfBinding < BaseBinding
64
67
  end
65
68
 
66
69
  def remove
67
- # Remove all listeners on any reactive values
68
- @listeners.each(&:remove)
70
+ @computation.stop if @computation
71
+ @computation = nil
69
72
 
70
73
  @template.remove if @template
71
74
 
@@ -11,28 +11,10 @@ class TemplateBinding < BaseBinding
11
11
 
12
12
  @current_template = nil
13
13
 
14
- # Find the source for the getter binding
15
- @path, section, @options = value_from_getter(getter)
16
-
17
- if section.is_a?(String)
18
- # Render this as a section
19
- @section = section
20
- else
21
- # Use the value passed in as the default arguments
22
- @arguments = section
23
- end
24
-
25
- # Sometimes we want multiple template bindings to share the same controller (usually
26
- # when displaying a :Title and a :Body), this instance tracks those.
27
- if @options && (controller_group = @options[:controller_group])
28
- @grouped_controller = GroupedControllers.new(controller_group)
29
- end
14
+ @getter = getter
30
15
 
31
16
  # Run the initial render
32
- update
33
-
34
- @path_changed_listener = @path.on('changed') { queue_update } if @path.reactive?
35
- @section_changed_listener = @section.on('changed') { queue_update } if @section && @section.reactive?
17
+ @computation = -> { update(*@context.instance_eval(&getter)) }.watch!
36
18
  end
37
19
 
38
20
  def setup_path(binding_in_path)
@@ -112,43 +94,88 @@ class TemplateBinding < BaseBinding
112
94
  return nil, nil
113
95
  end
114
96
 
115
- # Called when the path changes. If we are sharing a controller, clear the cached
116
- # controller before we queue
117
- def queue_update
118
- @grouped_controller.clear if @grouped_controller
97
+ def update(path, section_or_arguments=nil, options={})
98
+ Computation.run_without_tracking do
99
+ # Remove existing template and call _removed
100
+ controller_send(:"#{@action}_removed") if @action && @controller
101
+ @current_template.remove if @current_template
119
102
 
120
- super
121
- end
103
+ @options = options
122
104
 
123
- def update
124
- full_path, controller_path = path_for_template(@path.cur, @section.cur)
105
+ # A blank path needs to load a missing template, otherwise it tries to load
106
+ # the same template.
107
+ path = path.blank? ? '---missing---' : path
125
108
 
126
- @current_template.remove if @current_template
109
+ section = nil
110
+ @arguments = nil
127
111
 
128
- if @arguments
129
- # Load in any procs
130
- @arguments.each_pair do |key,value|
131
- if value.class == Proc
132
- @arguments[key.gsub('-', '_')] = value.call
133
- end
112
+ if section_or_arguments.is_a?(String)
113
+ # Render this as a section
114
+ section = section_or_arguments
115
+ else
116
+ # Use the value passed in as the default arguments
117
+ @arguments = section_or_arguments
118
+ end
119
+
120
+ # Sometimes we want multiple template bindings to share the same controller (usually
121
+ # when displaying a :Title and a :Body), this instance tracks those.
122
+ if @options && (controller_group = @options[:controller_group])
123
+ @grouped_controller = GroupedControllers.new(controller_group)
124
+ else
125
+ clear_grouped_controller
134
126
  end
127
+
128
+ full_path, controller_path = path_for_template(path, section)
129
+ render_template(full_path, controller_path)
130
+
131
+ queue_clear_grouped_controller
132
+ end
133
+ end
134
+
135
+ # On the next tick, we clear the grouped controller so that any changes to template paths
136
+ # will create a new controller and trigger the action.
137
+ def queue_clear_grouped_controller
138
+ if Volt.in_browser?
139
+ # In the browser, we want to keep a grouped controller around during a single run
140
+ # of the event loop. To make that happen, we clear it on the next tick.
141
+ `setImmediate(function() {`
142
+ clear_grouped_controller
143
+ `})`
144
+ else
145
+ # For the backend, clear it immediately
146
+ clear_grouped_controller
135
147
  end
148
+ end
136
149
 
137
- render_template(full_path, controller_path)
150
+ def clear_grouped_controller
151
+ if @grouped_controller
152
+ @grouped_controller.clear
153
+ @grouped_controller = nil
154
+ end
138
155
  end
139
156
 
140
157
  # The context for templates can be either a controller, or the original context.
141
158
  def render_template(full_path, controller_path)
142
- args = @arguments ? [@arguments] : []
159
+ if @arguments
160
+ args = [SubContext.new(@arguments)]
161
+ else
162
+ args = []
163
+ end
143
164
 
144
165
  @controller = nil
145
166
 
146
167
  # Fetch grouped controllers if we're grouping
147
168
  @controller = @grouped_controller.get if @grouped_controller
148
169
 
149
- # Otherwise, make a new controller
150
- unless @controller
151
- controller_class, action = get_controller(controller_path)
170
+ # The action to be called and rendered
171
+ @action = nil
172
+
173
+ if @controller
174
+ # Track that we're using the group controller
175
+ @grouped_controller.inc if @grouped_controller
176
+ else
177
+ # Otherwise, make a new controller
178
+ controller_class, @action = get_controller(controller_path)
152
179
 
153
180
  if controller_class
154
181
  # Setup the controller
@@ -158,7 +185,7 @@ class TemplateBinding < BaseBinding
158
185
  end
159
186
 
160
187
  # Trigger the action
161
- @controller.send(action) if @controller.respond_to?(action)
188
+ controller_send(@action) if @action
162
189
 
163
190
  # Track the grouped controller
164
191
  @grouped_controller.set(@controller) if @grouped_controller
@@ -177,24 +204,12 @@ class TemplateBinding < BaseBinding
177
204
  @controller.section = @current_template.dom_section
178
205
  end
179
206
 
180
- if @controller.respond_to?(:dom_ready)
181
- @controller.dom_ready
182
- end
207
+ controller_send(:"#{@action}_ready") if @action
183
208
  end
184
209
  end
185
210
 
186
211
  def remove
187
- @grouped_controller.clear if @grouped_controller
188
-
189
- if @path_changed_listener
190
- @path_changed_listener.remove
191
- @path_changed_listener = nil
192
- end
193
-
194
- if @section_changed_listener
195
- @section_changed_listener.remove
196
- @section_changed_listener = nil
197
- end
212
+ clear_grouped_controller
198
213
 
199
214
  if @current_template
200
215
  # Remove the template if one has been rendered, when the template binding is
@@ -205,16 +220,19 @@ class TemplateBinding < BaseBinding
205
220
  super
206
221
 
207
222
  if @controller
208
- # Let the controller know we removed
209
- if @controller.respond_to?(:dom_removed)
210
- @controller.dom_removed
211
- end
223
+ controller_send(:"#{@action}_removed") if @action
212
224
 
213
225
  @controller = nil
214
226
  end
215
227
  end
216
228
 
217
229
  private
230
+ # Sends the action to the controller if it exists
231
+ def controller_send(action_name)
232
+ if @controller.respond_to?(action_name)
233
+ @controller.send(action_name)
234
+ end
235
+ end
218
236
 
219
237
  # Fetch the controller class
220
238
  def get_controller(controller_path)