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.
- 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
|