volt 0.7.1 → 0.7.2
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/Gemfile +1 -2
- data/Readme.md +97 -56
- data/VERSION +1 -1
- data/app/volt/assets/js/sockjs-0.3.4.min.js +27 -0
- data/app/volt/assets/js/vertxbus.js +216 -0
- data/app/volt/tasks/live_query/live_query.rb +5 -5
- data/app/volt/tasks/live_query/live_query_pool.rb +1 -1
- data/app/volt/tasks/query_tasks.rb +5 -0
- data/app/volt/tasks/store_tasks.rb +44 -18
- data/docs/WHY.md +10 -0
- data/lib/volt/cli.rb +18 -7
- data/lib/volt/controllers/model_controller.rb +30 -8
- data/lib/volt/extra_core/inflections.rb +63 -0
- data/lib/volt/extra_core/inflector/inflections.rb +203 -0
- data/lib/volt/extra_core/inflector/methods.rb +63 -0
- data/lib/volt/extra_core/inflector.rb +4 -0
- data/lib/volt/extra_core/object.rb +9 -0
- data/lib/volt/extra_core/string.rb +10 -14
- data/lib/volt/models/array_model.rb +45 -27
- data/lib/volt/models/cursor.rb +6 -0
- data/lib/volt/models/model.rb +127 -12
- data/lib/volt/models/model_hash_behaviour.rb +8 -5
- data/lib/volt/models/model_helpers.rb +4 -4
- data/lib/volt/models/model_state.rb +22 -0
- data/lib/volt/models/persistors/array_store.rb +49 -35
- data/lib/volt/models/persistors/base.rb +3 -3
- data/lib/volt/models/persistors/model_store.rb +17 -6
- data/lib/volt/models/persistors/query/query_listener.rb +0 -2
- data/lib/volt/models/persistors/store.rb +0 -4
- data/lib/volt/models/persistors/store_state.rb +27 -0
- data/lib/volt/models/url.rb +2 -2
- data/lib/volt/models/validations/errors.rb +0 -0
- data/lib/volt/models/validations/length.rb +13 -0
- data/lib/volt/models/validations/validations.rb +82 -0
- data/lib/volt/models.rb +1 -1
- data/lib/volt/page/bindings/attribute_binding.rb +29 -14
- data/lib/volt/page/bindings/base_binding.rb +2 -2
- data/lib/volt/page/bindings/component_binding.rb +29 -25
- data/lib/volt/page/bindings/content_binding.rb +1 -0
- data/lib/volt/page/bindings/each_binding.rb +25 -33
- data/lib/volt/page/bindings/event_binding.rb +0 -1
- data/lib/volt/page/bindings/if_binding.rb +3 -1
- data/lib/volt/page/bindings/template_binding.rb +61 -28
- data/lib/volt/page/document_events.rb +3 -1
- data/lib/volt/page/draw_cycle.rb +22 -0
- data/lib/volt/page/page.rb +10 -1
- data/lib/volt/page/reactive_template.rb +23 -16
- data/lib/volt/page/sub_context.rb +1 -1
- data/lib/volt/page/targets/attribute_section.rb +3 -2
- data/lib/volt/page/targets/attribute_target.rb +0 -4
- data/lib/volt/page/targets/base_section.rb +25 -0
- data/lib/volt/page/targets/binding_document/component_node.rb +13 -14
- data/lib/volt/page/targets/binding_document/html_node.rb +4 -0
- data/lib/volt/page/targets/dom_section.rb +16 -67
- data/lib/volt/page/targets/dom_template.rb +99 -0
- data/lib/volt/page/targets/helpers/comment_searchers.rb +29 -0
- data/lib/volt/page/template_renderer.rb +2 -14
- data/lib/volt/reactive/array_extensions.rb +0 -1
- data/lib/volt/reactive/event_chain.rb +9 -2
- data/lib/volt/reactive/events.rb +44 -37
- data/lib/volt/reactive/object_tracking.rb +1 -1
- data/lib/volt/reactive/reactive_array.rb +18 -0
- data/lib/volt/reactive/reactive_count.rb +108 -0
- data/lib/volt/reactive/reactive_generator.rb +44 -0
- data/lib/volt/reactive/reactive_value.rb +73 -73
- data/lib/volt/reactive/string_extensions.rb +1 -1
- data/lib/volt/router/routes.rb +205 -88
- data/lib/volt/server/component_handler.rb +3 -1
- data/lib/volt/server/html_parser/view_parser.rb +20 -4
- data/lib/volt/server/rack/component_paths.rb +13 -10
- data/lib/volt/server/rack/index_files.rb +4 -4
- data/lib/volt/server/socket_connection_handler.rb +5 -1
- data/lib/volt/server.rb +10 -3
- data/spec/apps/kitchen_sink/.gitignore +8 -0
- data/spec/apps/kitchen_sink/Gemfile +32 -0
- data/spec/apps/kitchen_sink/app/home/views/index/index.html +3 -5
- data/spec/apps/kitchen_sink/config.ru +4 -0
- data/spec/apps/kitchen_sink/public/index.html +2 -2
- data/spec/extra_core/inflector_spec.rb +8 -0
- data/spec/models/event_chain_spec.rb +18 -0
- data/spec/models/model_buffers_spec.rb +9 -0
- data/spec/models/model_spec.rb +22 -9
- data/spec/models/reactive_array_spec.rb +26 -1
- data/spec/models/reactive_call_times_spec.rb +28 -0
- data/spec/models/reactive_value_spec.rb +19 -0
- data/spec/models/validations_spec.rb +39 -0
- data/spec/page/bindings/content_binding_spec.rb +1 -0
- data/spec/{templates → page/bindings}/template_binding_spec.rb +54 -0
- data/spec/router/routes_spec.rb +156 -8
- data/spec/server/html_parser/sandlebars_parser_spec.rb +55 -47
- data/spec/server/html_parser/view_parser_spec.rb +3 -0
- data/spec/server/rack/asset_files_spec.rb +1 -1
- data/spec/spec_helper.rb +25 -11
- data/spec/templates/targets/binding_document/component_node_spec.rb +12 -0
- data/templates/project/Gemfile.tt +11 -0
- data/templates/project/app/home/config/routes.rb +1 -1
- data/templates/project/app/home/controllers/index_controller.rb +5 -5
- data/templates/project/app/home/views/index/index.html +6 -6
- data/volt.gemspec +5 -6
- metadata +34 -76
- data/app/volt/assets/js/sockjs-0.2.1.min.js +0 -27
- 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
|
-
|
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
|
-
|
79
|
-
|
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 =
|
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
|
-
|
88
|
+
dom_nodes = null;
|
144
89
|
}
|
145
90
|
|
146
|
-
return
|
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>-- < missing template #{template_name.inspect.gsub('<', '<').gsub('>', '>')} > --</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
|
-
|
14
|
-
html = @template['html']
|
15
|
-
bindings = @template['bindings']
|
16
|
-
else
|
17
|
-
html = "<div>-- < missing template #{template_name.inspect.gsub('<', '<').gsub('>', '>')} > --</div>"
|
18
|
-
bindings = {}
|
19
|
-
end
|
11
|
+
@sub_bindings = []
|
20
12
|
|
21
|
-
bindings = self.section.
|
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
|
@@ -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
|
-
|
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)
|
data/lib/volt/reactive/events.rb
CHANGED
@@ -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
|
-
#
|
59
|
-
#
|
60
|
-
@klass.
|
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
|
-
|
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
|
-
|
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
|
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
|
-
|
153
|
-
@listeners[event].delete(listener)
|
152
|
+
@listeners[event].delete(listener)
|
154
153
|
|
155
|
-
|
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
|
-
|
162
|
-
|
163
|
-
|
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
|
-
#
|
166
|
-
|
167
|
-
|
168
|
-
|
169
|
-
|
170
|
-
|
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
|
-
#
|
176
|
-
|
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
|