volt 0.7.1 → 0.7.2

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