volt 0.7.23 → 0.8.0

Sign up to get free protection for your applications and to get access to all the features.
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)