volt 0.7.1 → 0.7.2

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 (103) hide show
  1. checksums.yaml +4 -4
  2. data/Gemfile +1 -2
  3. data/Readme.md +97 -56
  4. data/VERSION +1 -1
  5. data/app/volt/assets/js/sockjs-0.3.4.min.js +27 -0
  6. data/app/volt/assets/js/vertxbus.js +216 -0
  7. data/app/volt/tasks/live_query/live_query.rb +5 -5
  8. data/app/volt/tasks/live_query/live_query_pool.rb +1 -1
  9. data/app/volt/tasks/query_tasks.rb +5 -0
  10. data/app/volt/tasks/store_tasks.rb +44 -18
  11. data/docs/WHY.md +10 -0
  12. data/lib/volt/cli.rb +18 -7
  13. data/lib/volt/controllers/model_controller.rb +30 -8
  14. data/lib/volt/extra_core/inflections.rb +63 -0
  15. data/lib/volt/extra_core/inflector/inflections.rb +203 -0
  16. data/lib/volt/extra_core/inflector/methods.rb +63 -0
  17. data/lib/volt/extra_core/inflector.rb +4 -0
  18. data/lib/volt/extra_core/object.rb +9 -0
  19. data/lib/volt/extra_core/string.rb +10 -14
  20. data/lib/volt/models/array_model.rb +45 -27
  21. data/lib/volt/models/cursor.rb +6 -0
  22. data/lib/volt/models/model.rb +127 -12
  23. data/lib/volt/models/model_hash_behaviour.rb +8 -5
  24. data/lib/volt/models/model_helpers.rb +4 -4
  25. data/lib/volt/models/model_state.rb +22 -0
  26. data/lib/volt/models/persistors/array_store.rb +49 -35
  27. data/lib/volt/models/persistors/base.rb +3 -3
  28. data/lib/volt/models/persistors/model_store.rb +17 -6
  29. data/lib/volt/models/persistors/query/query_listener.rb +0 -2
  30. data/lib/volt/models/persistors/store.rb +0 -4
  31. data/lib/volt/models/persistors/store_state.rb +27 -0
  32. data/lib/volt/models/url.rb +2 -2
  33. data/lib/volt/models/validations/errors.rb +0 -0
  34. data/lib/volt/models/validations/length.rb +13 -0
  35. data/lib/volt/models/validations/validations.rb +82 -0
  36. data/lib/volt/models.rb +1 -1
  37. data/lib/volt/page/bindings/attribute_binding.rb +29 -14
  38. data/lib/volt/page/bindings/base_binding.rb +2 -2
  39. data/lib/volt/page/bindings/component_binding.rb +29 -25
  40. data/lib/volt/page/bindings/content_binding.rb +1 -0
  41. data/lib/volt/page/bindings/each_binding.rb +25 -33
  42. data/lib/volt/page/bindings/event_binding.rb +0 -1
  43. data/lib/volt/page/bindings/if_binding.rb +3 -1
  44. data/lib/volt/page/bindings/template_binding.rb +61 -28
  45. data/lib/volt/page/document_events.rb +3 -1
  46. data/lib/volt/page/draw_cycle.rb +22 -0
  47. data/lib/volt/page/page.rb +10 -1
  48. data/lib/volt/page/reactive_template.rb +23 -16
  49. data/lib/volt/page/sub_context.rb +1 -1
  50. data/lib/volt/page/targets/attribute_section.rb +3 -2
  51. data/lib/volt/page/targets/attribute_target.rb +0 -4
  52. data/lib/volt/page/targets/base_section.rb +25 -0
  53. data/lib/volt/page/targets/binding_document/component_node.rb +13 -14
  54. data/lib/volt/page/targets/binding_document/html_node.rb +4 -0
  55. data/lib/volt/page/targets/dom_section.rb +16 -67
  56. data/lib/volt/page/targets/dom_template.rb +99 -0
  57. data/lib/volt/page/targets/helpers/comment_searchers.rb +29 -0
  58. data/lib/volt/page/template_renderer.rb +2 -14
  59. data/lib/volt/reactive/array_extensions.rb +0 -1
  60. data/lib/volt/reactive/event_chain.rb +9 -2
  61. data/lib/volt/reactive/events.rb +44 -37
  62. data/lib/volt/reactive/object_tracking.rb +1 -1
  63. data/lib/volt/reactive/reactive_array.rb +18 -0
  64. data/lib/volt/reactive/reactive_count.rb +108 -0
  65. data/lib/volt/reactive/reactive_generator.rb +44 -0
  66. data/lib/volt/reactive/reactive_value.rb +73 -73
  67. data/lib/volt/reactive/string_extensions.rb +1 -1
  68. data/lib/volt/router/routes.rb +205 -88
  69. data/lib/volt/server/component_handler.rb +3 -1
  70. data/lib/volt/server/html_parser/view_parser.rb +20 -4
  71. data/lib/volt/server/rack/component_paths.rb +13 -10
  72. data/lib/volt/server/rack/index_files.rb +4 -4
  73. data/lib/volt/server/socket_connection_handler.rb +5 -1
  74. data/lib/volt/server.rb +10 -3
  75. data/spec/apps/kitchen_sink/.gitignore +8 -0
  76. data/spec/apps/kitchen_sink/Gemfile +32 -0
  77. data/spec/apps/kitchen_sink/app/home/views/index/index.html +3 -5
  78. data/spec/apps/kitchen_sink/config.ru +4 -0
  79. data/spec/apps/kitchen_sink/public/index.html +2 -2
  80. data/spec/extra_core/inflector_spec.rb +8 -0
  81. data/spec/models/event_chain_spec.rb +18 -0
  82. data/spec/models/model_buffers_spec.rb +9 -0
  83. data/spec/models/model_spec.rb +22 -9
  84. data/spec/models/reactive_array_spec.rb +26 -1
  85. data/spec/models/reactive_call_times_spec.rb +28 -0
  86. data/spec/models/reactive_value_spec.rb +19 -0
  87. data/spec/models/validations_spec.rb +39 -0
  88. data/spec/page/bindings/content_binding_spec.rb +1 -0
  89. data/spec/{templates → page/bindings}/template_binding_spec.rb +54 -0
  90. data/spec/router/routes_spec.rb +156 -8
  91. data/spec/server/html_parser/sandlebars_parser_spec.rb +55 -47
  92. data/spec/server/html_parser/view_parser_spec.rb +3 -0
  93. data/spec/server/rack/asset_files_spec.rb +1 -1
  94. data/spec/spec_helper.rb +25 -11
  95. data/spec/templates/targets/binding_document/component_node_spec.rb +12 -0
  96. data/templates/project/Gemfile.tt +11 -0
  97. data/templates/project/app/home/config/routes.rb +1 -1
  98. data/templates/project/app/home/controllers/index_controller.rb +5 -5
  99. data/templates/project/app/home/views/index/index.html +6 -6
  100. data/volt.gemspec +5 -6
  101. metadata +34 -76
  102. data/app/volt/assets/js/sockjs-0.2.1.min.js +0 -27
  103. data/lib/volt/reactive/object_tracker.rb +0 -107
@@ -1,19 +1,14 @@
1
1
  require 'volt/page/targets/base_section'
2
+ require 'volt/page/targets/helpers/comment_searchers'
2
3
 
3
4
  class DomSection < BaseSection
5
+ include CommentSearchers
6
+
4
7
  def initialize(binding_name)
5
8
  @start_node = find_by_comment("$#{binding_name}")
6
9
  @end_node = find_by_comment("$/#{binding_name}")
7
10
  end
8
11
 
9
- def find_by_comment(text, in_node=`document`)
10
- node = nil
11
-
12
- %x{
13
- node = document.evaluate("//comment()[. = ' " + text + " ']", in_node, null, XPathResult.UNORDERED_NODE_ITERATOR_TYPE, null).iterateNext();
14
- }
15
- return node
16
- end
17
12
 
18
13
  def text=(value)
19
14
  %x{
@@ -23,7 +18,9 @@ class DomSection < BaseSection
23
18
  end
24
19
 
25
20
  def html=(value)
26
- set_content_and_rezero_bindings(value, {})
21
+ new_nodes = build_from_html(value)
22
+
23
+ self.nodes = `new_nodes.childNodes`
27
24
  end
28
25
 
29
26
  def remove
@@ -48,6 +45,7 @@ class DomSection < BaseSection
48
45
  end
49
46
 
50
47
  def insert_anchor_before(binding_name, insert_after_binding)
48
+ puts "insert_anchor_before"
51
49
  node = find_by_comment("$#{insert_after_binding}")
52
50
  Element.find(node).before("<!-- $#{binding_name} --><!-- $/#{binding_name} -->")
53
51
  end
@@ -75,75 +73,22 @@ class DomSection < BaseSection
75
73
  return `range.commonAncestorContainer`
76
74
  end
77
75
 
78
- # Takes in our html and bindings, and rezero's the comment names, and the
79
- # bindings. Returns an updated bindings hash
80
- def set_content_and_rezero_bindings(html, bindings)
81
- sub_nodes = nil
82
- temp_div = nil
83
-
84
- %x{
85
- temp_div = document.createElement('div');
86
- var doc = jQuery.parseHTML(html);
87
-
88
- if (doc) {
89
- for (var i=0;i < doc.length;i++) {
90
- temp_div.appendChild(doc[i]);
91
- }
92
- }
93
- }
94
-
95
- new_bindings = {}
96
- # Loop through the bindings, and rezero.
97
- bindings.each_pair do |name,binding|
98
- new_name = @@binding_number
99
-
100
- if name.cur.is_a?(String)
101
- if name[0..1] == 'id'
102
- # Find by id
103
- %x{
104
- var node = temp_div.querySelector('#' + name);
105
- node.setAttribute('id', 'id' +new_name);
106
- }
107
-
108
- new_bindings["id#{new_name}"] = binding
109
- else
110
- # Assume a fixed id
111
- # TODO: We should raise an exception if this id is already on the page
112
- new_bindings[name] = binding
113
- end
114
- else
115
- # puts "----- #{name.inspect} - #{new_name}"
116
- # `console.log(temp_div);`
117
- # Change the comment ids
118
- start_comment = find_by_comment("$#{name}", temp_div)
119
- end_comment = find_by_comment("$/#{name}", temp_div)
120
-
121
- %x{
122
- start_comment.textContent = " $" + new_name + " ";
123
- end_comment.textContent = " $/" + new_name + " ";
124
- }
125
-
126
- new_bindings[new_name] = binding
127
- end
128
-
129
-
130
- @@binding_number += 1
131
- end
132
-
76
+ def set_template(dom_template)
77
+ dom_nodes, bindings = dom_template.make_new
133
78
 
134
79
  children = nil
135
80
  %x{
136
- children = temp_div.childNodes;
81
+ children = dom_nodes.childNodes;
137
82
  }
138
83
 
139
84
  # Update the nodes
140
85
  self.nodes = children
141
86
 
142
87
  %x{
143
- temp_div = null;
88
+ dom_nodes = null;
144
89
  }
145
90
 
146
- return new_bindings
91
+ return bindings
147
92
  end
148
93
 
149
94
  def range
@@ -161,4 +106,8 @@ class DomSection < BaseSection
161
106
  return range
162
107
  end
163
108
 
109
+ def inspect
110
+ "<#{self.class.to_s}>"
111
+ end
112
+
164
113
  end
@@ -0,0 +1,99 @@
1
+ require 'volt/page/targets/helpers/comment_searchers'
2
+
3
+ # A dom template is used to optimize going from a template name to
4
+ # dom nodes and bindings. It stores a copy of the template's parsed
5
+ # dom nodes, then when a new instance is requested, it updates the
6
+ # dom markers (comments) for new binding numbers and returns a cloneNode'd
7
+ # version of the dom nodes and the bindings.
8
+ class DomTemplate
9
+ include CommentSearchers
10
+
11
+ def initialize(page, template_name)
12
+ template = page.templates[template_name]
13
+
14
+ if template
15
+ html = template['html']
16
+ @bindings = template['bindings']
17
+ else
18
+ html = "<div>-- &lt; missing template #{template_name.inspect.gsub('<', '&lt;').gsub('>', '&gt;')} &gt; --</div>"
19
+ @bindings = {}
20
+ end
21
+
22
+ @nodes = build_from_html(html)
23
+
24
+ track_binding_anchors
25
+ end
26
+
27
+ # Returns the dom nodes and bindings
28
+ def make_new
29
+ bindings = update_binding_anchors!
30
+
31
+ new_nodes = `self.nodes.cloneNode(true)`
32
+
33
+ return [new_nodes, bindings]
34
+ end
35
+
36
+
37
+ # Finds each of the binding anchors in the temp dom, then stores a reference
38
+ # to them so they can be quickly updated without using xpath to find them again.
39
+ def track_binding_anchors
40
+ @binding_anchors = {}
41
+
42
+ # Loop through the bindings, find in nodes.
43
+ @bindings.each_pair do |name,binding|
44
+ if name.is_a?(String)
45
+ # Find the dom node for an attribute anchor
46
+ node = nil
47
+ %x{
48
+ node = self.nodes.querySelector('#' + name);
49
+ }
50
+ @binding_anchors[name] = node
51
+ else
52
+ # Find the dom node for a comment anchor
53
+ start_comment = find_by_comment("$#{name}", @nodes)
54
+ end_comment = find_by_comment("$/#{name}", @nodes)
55
+
56
+ @binding_anchors[name] = [start_comment, end_comment]
57
+ end
58
+ end
59
+ end
60
+
61
+ # Takes the binding_anchors and updates them with new numbers (comments and id's)
62
+ # then returns the bindings updated to the new numbers.
63
+ def update_binding_anchors!
64
+ new_bindings = {}
65
+
66
+ @binding_anchors.each_pair do |name, anchors|
67
+ new_name = @@binding_number
68
+ @@binding_number += 1
69
+
70
+ if name.is_a?(String)
71
+ if name[0..1] == 'id'
72
+ # A generated id
73
+ # update the id
74
+ `anchors.setAttribute('id', 'id' + new_name);`
75
+
76
+ new_bindings["id#{new_name}"] = @bindings[name]
77
+ else
78
+ # Assume a fixed id, should not be updated
79
+ # TODO: Might want to check the page to see if a node
80
+ # with this id already exists and raise if it does.
81
+
82
+ # Copy from existing binding
83
+ new_bindings[name] = @bindings[name]
84
+ end
85
+ else
86
+ start_comment, end_comment = anchors
87
+
88
+ %x{
89
+ start_comment.textContent = " $" + new_name + " ";
90
+ end_comment.textContent = " $/" + new_name + " ";
91
+ }
92
+
93
+ new_bindings[new_name] = @bindings[name]
94
+ end
95
+ end
96
+
97
+ return new_bindings
98
+ end
99
+ end
@@ -0,0 +1,29 @@
1
+ module CommentSearchers
2
+
3
+ def find_by_comment(text, in_node=`document`)
4
+ node = nil
5
+
6
+ %x{
7
+ node = document.evaluate("//comment()[. = ' " + text + " ']", in_node, null, XPathResult.UNORDERED_NODE_ITERATOR_TYPE, null).iterateNext();
8
+ }
9
+ return node
10
+ end
11
+
12
+
13
+ # Returns an unattached div with the nodes from the passed
14
+ # in html.
15
+ def build_from_html(html)
16
+ temp_div = nil
17
+ %x{
18
+ temp_div = document.createElement('div');
19
+ var doc = jQuery.parseHTML(html);
20
+
21
+ if (doc) {
22
+ for (var i=0;i < doc.length;i++) {
23
+ temp_div.appendChild(doc[i]);
24
+ }
25
+ }
26
+ }
27
+ return temp_div
28
+ end
29
+ end
@@ -7,18 +7,10 @@ class TemplateRenderer < BaseBinding
7
7
  super(page, target, context, binding_name)
8
8
 
9
9
  # puts "Template Name: #{template_name}"
10
- @template = @page.templates[template_name]
11
- @sub_bindings = []
12
10
 
13
- if @template
14
- html = @template['html']
15
- bindings = @template['bindings']
16
- else
17
- html = "<div>-- &lt; missing template #{template_name.inspect.gsub('<', '&lt;').gsub('>', '&gt;')} &gt; --</div>"
18
- bindings = {}
19
- end
11
+ @sub_bindings = []
20
12
 
21
- bindings = self.section.set_content_and_rezero_bindings(html, bindings)
13
+ bindings = self.section.set_content_to_template(page, template_name)
22
14
 
23
15
  bindings.each_pair do |id,bindings_for_id|
24
16
  bindings_for_id.each do |binding|
@@ -43,8 +35,4 @@ class TemplateRenderer < BaseBinding
43
35
 
44
36
  super
45
37
  end
46
-
47
- def remove_anchors
48
- section.remove_anchors
49
- end
50
38
  end
@@ -1,5 +1,4 @@
1
1
  class Array
2
- include ReactiveTags
3
2
  alias :__old_plus :+
4
3
 
5
4
  def +(val)
@@ -16,7 +16,12 @@ class ChainListener
16
16
  end
17
17
 
18
18
  def remove
19
- raise "event chain already removed" if @removed
19
+ # raise "event chain already removed" if @removed
20
+ if @removed
21
+ puts "event chain already removed"
22
+ return
23
+ end
24
+
20
25
  @removed = true
21
26
  @event_chain.remove_object(self)
22
27
 
@@ -27,6 +32,7 @@ class ChainListener
27
32
 
28
33
  if RUBY_PLATFORM == 'opal' && CHAIN_DEBUG
29
34
  `window.chain_listeners -= 1;`
35
+ `console.log('del chain listeners: ', window.chain_listeners)`
30
36
  end
31
37
  end
32
38
  end
@@ -81,7 +87,8 @@ class EventChain
81
87
  def remove_object(chain_listener)
82
88
  @event_chain[chain_listener].each_pair do |event,listener|
83
89
  # Unbind each listener
84
- listener.remove
90
+ # TODO: The if shouldn't be needed, but sometimes we get nil for some reason?
91
+ listener.remove if listener
85
92
  end
86
93
 
87
94
  @event_chain.delete(chain_listener)
@@ -1,5 +1,4 @@
1
1
  require 'volt/reactive/event_chain'
2
- require 'volt/reactive/object_tracker'
3
2
 
4
3
  DEBUG = false
5
4
 
@@ -47,18 +46,15 @@ class Listener
47
46
  end
48
47
 
49
48
  def call(*args)
50
- # raise "Triggered on removed: #{@event} on #{@klass2.inspect}" if @removed
51
49
  if @removed
52
- puts "Triggered on a removed event: #{@event}"
50
+ # puts "Triggered on a removed event: #{@event}"
53
51
  return
54
52
  end
55
53
 
56
- # Queue a live value update
57
54
  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}"
55
+ # Update the reactive value's current value to let it know it is being
56
+ # followed.
57
+ @klass.update_followers if @klass.respond_to?(:update_followers)
62
58
  end
63
59
 
64
60
  @callback.call(*args)
@@ -67,7 +63,11 @@ class Listener
67
63
  # Removes the listener from where ever it was created.
68
64
  def remove
69
65
  # puts "FAIL:" if @removed
70
- raise "event #{@event} already removed" if @removed
66
+ if @removed
67
+ # raise "event #{@event} already removed"
68
+ puts "event #{@event} already removed"
69
+ return
70
+ end
71
71
 
72
72
  # puts "e rem: #{@event} on #{@klass.inspect}"
73
73
  if DEBUG && RUBY_PLATFORM == 'opal'
@@ -101,33 +101,29 @@ module Events
101
101
  # Add a listener for an event
102
102
  def on(event, scope_provider=nil, &block)
103
103
 
104
-
105
- # if reactive? && [:added, :removed].include?(event)
106
- # self.object_tracker.queue_update
107
- # ObjectTracker.process_queue
108
- # end
109
-
110
-
111
104
  # puts "Register: #{event} on #{self.inspect}"
112
105
  event = event.to_sym
113
106
 
107
+ @has_listeners = true
108
+
114
109
  new_listener = Listener.new(self, event, scope_provider, block)
115
110
 
116
111
  @listeners ||= {}
117
112
  @listeners[event] ||= []
118
113
  @listeners[event] << new_listener
119
114
 
120
- first = @listeners[event].size == 1
115
+ first_for_event = @listeners[event].size == 1
116
+ first = first_for_event && @listeners.size == 1
121
117
 
122
118
  # When events get added, we need to notify event chains so they
123
119
  # can update and chain any new events.
124
- event_chain.add_event(event) if first
120
+ event_chain.add_event(event) if first_for_event
125
121
 
126
122
  # Let the included class know that an event was registered. (if it cares)
127
123
  if self.respond_to?(:event_added)
128
124
  # call event added passing the event, the scope, and a boolean if it
129
125
  # is the first time this event has been added.
130
- self.event_added(event, scope_provider, first)
126
+ self.event_added(event, scope_provider, first, first_for_event)
131
127
  end
132
128
 
133
129
  return new_listener
@@ -141,6 +137,10 @@ module Events
141
137
  @listeners || {}
142
138
  end
143
139
 
140
+ def has_listeners?
141
+ @has_listeners
142
+ end
143
+
144
144
  # Typically you would call .remove on the listener returned from the .on
145
145
  # method. However, here you can also pass in the original proc to remove
146
146
  # a listener
@@ -149,31 +149,38 @@ module Events
149
149
 
150
150
  raise "Unable to delete #{event} from #{self.inspect}" unless @listeners && @listeners[event]
151
151
 
152
- # if @listeners && @listeners[event]
153
- @listeners[event].delete(listener)
152
+ @listeners[event].delete(listener)
154
153
 
155
- no_more_events = @listeners[event].size == 0
156
- if no_more_events
157
- # When events are removed, we need to notify any relevent chains so they
158
- # can remove any chained events.
159
- event_chain.remove_event(event)
154
+ last_for_event = @listeners[event].size == 0
160
155
 
161
- # No registered listeners now on this event
162
- @listeners.delete(event)
163
- end
156
+ if last_for_event
157
+ # When events are removed, we need to notify any relevent chains so they
158
+ # can remove any chained events.
159
+ event_chain.remove_event(event)
164
160
 
165
- # Let the class we're included on know that we removed a listener (if it cares)
166
- if self.respond_to?(:event_removed)
167
- # Pass in the event and a boolean indicating if it is the last event
168
- self.event_removed(event, no_more_events)
169
- end
170
- # end
161
+ # No registered listeners now on this event
162
+ @listeners.delete(event)
163
+ end
164
+
165
+ last = last_for_event && @listeners.size == 0
166
+
167
+ # Let the class we're included on know that we removed a listener (if it cares)
168
+ if self.respond_to?(:event_removed)
169
+ # Pass in the event and a boolean indicating if it is the last event
170
+ self.event_removed(event, last, last_for_event)
171
+ end
172
+
173
+ if last
174
+ @has_listeners = nil
175
+ end
171
176
  end
172
177
 
173
178
  def trigger!(event, filter=nil, *args)
174
179
  are_reactive = reactive?
175
- # puts "TRIGGER FOR: #{event} on #{self.inspect}" if !reactive?
176
- ObjectTracker.process_queue if !are_reactive# && !respond_to?(:skip_current_queue_flush)
180
+ # ObjectTracker.process_queue if !are_reactive
181
+ # puts "DT"
182
+ # insp = self.inspect
183
+ # puts "TRIGGER #{event} on #{insp}"
177
184
 
178
185
  event = event.to_sym
179
186
 
@@ -4,7 +4,7 @@ module ObjectTracking
4
4
  if value.reactive?
5
5
  # TODO: We should build this in so it fires just for the current index.
6
6
  # Currently this is a big performance hit.
7
- chain_listener = event_chain.add_object(value.reactive_manager) do |event, *args|
7
+ chain_listener = event_chain.add_object(value.reactive_manager) do |event, filter, *args|
8
8
  yield(event, key, args)
9
9
  end
10
10
  @reactive_element_listeners ||= {}
@@ -1,4 +1,5 @@
1
1
  require 'volt/reactive/object_tracking'
2
+ require 'volt/reactive/reactive_count'
2
3
 
3
4
  class ReactiveArray# < Array
4
5
  include ReactiveTags
@@ -32,6 +33,12 @@ class ReactiveArray# < Array
32
33
  # alias :__old_assign :[]=
33
34
  def []=(index, value)
34
35
  index_val = index.cur
36
+
37
+ if index_val < 0
38
+ # Handle a negative index
39
+ index_val = size + index_val
40
+ end
41
+
35
42
  # Clean old value
36
43
  __clear_element(index)
37
44
 
@@ -233,6 +240,17 @@ class ReactiveArray# < Array
233
240
  "#<#{self.class.to_s} #{@array.inspect}>"
234
241
  end
235
242
 
243
+ # tag_method(:count) do
244
+ # destructive!
245
+ # end
246
+ def count(&block)
247
+ if block
248
+ return ReactiveCount.new(self, block)
249
+ else
250
+ @array.count
251
+ end
252
+ end
253
+
236
254
  private
237
255
 
238
256
  def __clear_element(index)
@@ -0,0 +1,108 @@
1
+ class ReactiveCount
2
+ include ReactiveTags
3
+
4
+ def reactive?
5
+ true
6
+ end
7
+
8
+ def initialize(source, block)
9
+ @source = ReactiveValue.new(source)
10
+ @block = block
11
+ end
12
+
13
+ def cur
14
+ direct_count
15
+ end
16
+
17
+ # After events are bound, we keep a cache of each cell's count
18
+ # value, and base the results
19
+ def cached_count
20
+
21
+ end
22
+
23
+ # Before events are bound, when .cur is called, we simply
24
+ # run the count on the source object.
25
+ def direct_count
26
+ count = 0
27
+ @source.size.cur.times do |index|
28
+ val = @source[index]
29
+ result = @block.call(val).cur
30
+ if result == true
31
+ count += 1
32
+ end
33
+ end
34
+
35
+ count
36
+ end
37
+
38
+ def setup_listeners
39
+ puts "SETUP LISTENERS"
40
+ @cell_trackers = []
41
+ @added_tracker = @source.on('added') do |_, index|
42
+ change_cell_count(@source.size.cur)
43
+ trigger!('changed')
44
+ end
45
+
46
+ @removed_tracker = @source.on('removed') do |_, index|
47
+ change_cell_count(@source.size.cur)
48
+ trigger!('changed')
49
+ end
50
+
51
+ # Initial cell tracking
52
+ change_cell_count(@source.size.cur)
53
+ end
54
+
55
+ # We need to make sure we're listening on the result from each cell,
56
+ # that way we can trigger when the value changes.
57
+ def change_cell_count(size)
58
+ current_size = @cell_trackers.size
59
+
60
+ if current_size < size
61
+ # Add trackers
62
+
63
+ current_size.upto(size-1) do |index|
64
+ # Get the reactive value for the index
65
+ val = @source[index]
66
+
67
+ result = @block.call(val)
68
+ # puts "TRACK AT #{index} on #{result.inspect}"
69
+
70
+ @cell_trackers << result.on('changed') do
71
+ trigger!('changed')
72
+ end
73
+ end
74
+ elsif current_size > size
75
+ (current_size-1).downto(size) do |index|
76
+ # puts "Remove at: #{index}"
77
+ @cell_trackers[index].remove
78
+ @cell_trackers.delete_at(index)
79
+ end
80
+ end
81
+ end
82
+
83
+
84
+ def teardown_listeners
85
+ @added_tracker.remove
86
+ @added_tracker = nil
87
+
88
+ @removed_tracker.remove
89
+ @removed_tracker = nil
90
+
91
+ change_cell_count(0)
92
+
93
+ @cell_trackers = nil
94
+ puts "TEARDOWN"
95
+ end
96
+
97
+ def event_added(event, scope_provider, first, first_for_event)
98
+ setup_listeners if first
99
+ end
100
+
101
+ def event_removed(event, last, last_for_event)
102
+ teardown_listeners if last
103
+ end
104
+
105
+ def inspect
106
+ "@#{cur}"
107
+ end
108
+ end
@@ -0,0 +1,44 @@
1
+ class ReactiveGenerator
2
+ # Takes a hash and returns a ReactiveValue that depends on
3
+ # any ReactiveValue's inside of the hash (or children).
4
+ def self.from_hash(hash, skip_if_no_reactives=false)
5
+ reactives = find_reactives(hash)
6
+
7
+ if skip_if_no_reactives && reactives.size == 0
8
+ # There weren't any reactives, we can just use the hash
9
+ return hash
10
+ else
11
+ # Create a new reactive value that listens on all of its
12
+ # child reactive values.
13
+ value = ReactiveValue.new(hash)
14
+
15
+ reactives.each do |child|
16
+ value.reactive_manager.add_parent!(child)
17
+ end
18
+
19
+ return value
20
+ end
21
+ end
22
+
23
+ # Recursively loop through the data, returning a list of all
24
+ # reactive values in the hash, array, etc..
25
+ def self.find_reactives(object)
26
+ found = []
27
+ if object.reactive?
28
+ found << object
29
+
30
+ found += find_reactives(object.cur)
31
+ elsif object.is_a?(Array)
32
+ object.each do |item|
33
+ found += find_reactives(item)
34
+ end
35
+ elsif object.is_a?(Hash)
36
+ object.each_pair do |key, value|
37
+ found += find_reactives(key)
38
+ found += find_reactives(value)
39
+ end
40
+ end
41
+
42
+ return found.flatten
43
+ end
44
+ end