volt 0.2.3
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 +7 -0
- data/.gitignore +21 -0
- data/.rspec +2 -0
- data/.ruby-version +1 -0
- data/Gemfile +37 -0
- data/Guardfile +9 -0
- data/LICENSE.txt +22 -0
- data/Rakefile +23 -0
- data/Readme.md +34 -0
- data/VERSION +1 -0
- data/bin/volt +4 -0
- data/docs/GETTING_STARTED.md +7 -0
- data/docs/GUIDE.md +33 -0
- data/lib/volt.rb +15 -0
- data/lib/volt/benchmark/benchmark.rb +25 -0
- data/lib/volt/cli.rb +34 -0
- data/lib/volt/console.rb +19 -0
- data/lib/volt/controllers/model_controller.rb +29 -0
- data/lib/volt/extra_core/array.rb +10 -0
- data/lib/volt/extra_core/blank.rb +88 -0
- data/lib/volt/extra_core/extra_core.rb +7 -0
- data/lib/volt/extra_core/numeric.rb +9 -0
- data/lib/volt/extra_core/object.rb +36 -0
- data/lib/volt/extra_core/string.rb +29 -0
- data/lib/volt/extra_core/stringify_keys.rb +7 -0
- data/lib/volt/extra_core/true_false.rb +44 -0
- data/lib/volt/extra_core/try.rb +31 -0
- data/lib/volt/models.rb +5 -0
- data/lib/volt/models/array_model.rb +37 -0
- data/lib/volt/models/model.rb +210 -0
- data/lib/volt/models/model_wrapper.rb +23 -0
- data/lib/volt/models/params.rb +67 -0
- data/lib/volt/models/url.rb +192 -0
- data/lib/volt/page/url_tracker.rb +36 -0
- data/lib/volt/reactive/array_extensions.rb +13 -0
- data/lib/volt/reactive/event_chain.rb +126 -0
- data/lib/volt/reactive/events.rb +283 -0
- data/lib/volt/reactive/object_tracker.rb +99 -0
- data/lib/volt/reactive/object_tracking.rb +15 -0
- data/lib/volt/reactive/reactive_array.rb +222 -0
- data/lib/volt/reactive/reactive_tags.rb +64 -0
- data/lib/volt/reactive/reactive_value.rb +368 -0
- data/lib/volt/reactive/string_extensions.rb +34 -0
- data/lib/volt/router/routes.rb +83 -0
- data/lib/volt/server.rb +121 -0
- data/lib/volt/server/binding_setup.rb +2 -0
- data/lib/volt/server/channel_handler.rb +31 -0
- data/lib/volt/server/component_handler.rb +88 -0
- data/lib/volt/server/if_binding_setup.rb +29 -0
- data/lib/volt/server/request_handler.rb +16 -0
- data/lib/volt/server/scope.rb +43 -0
- data/lib/volt/server/source_map_server.rb +31 -0
- data/lib/volt/server/template_parser.rb +452 -0
- data/lib/volt/store/mongo.rb +5 -0
- data/lib/volt/templates/attribute_binding.rb +110 -0
- data/lib/volt/templates/base_binding.rb +37 -0
- data/lib/volt/templates/channel.rb +48 -0
- data/lib/volt/templates/content_binding.rb +35 -0
- data/lib/volt/templates/document_events.rb +80 -0
- data/lib/volt/templates/each_binding.rb +115 -0
- data/lib/volt/templates/event_binding.rb +51 -0
- data/lib/volt/templates/if_binding.rb +74 -0
- data/lib/volt/templates/memory_test.rb +26 -0
- data/lib/volt/templates/page.rb +146 -0
- data/lib/volt/templates/reactive_template.rb +38 -0
- data/lib/volt/templates/render_queue.rb +5 -0
- data/lib/volt/templates/sub_context.rb +23 -0
- data/lib/volt/templates/targets/attribute_section.rb +33 -0
- data/lib/volt/templates/targets/attribute_target.rb +18 -0
- data/lib/volt/templates/targets/base_section.rb +14 -0
- data/lib/volt/templates/targets/binding_document/base_node.rb +3 -0
- data/lib/volt/templates/targets/binding_document/component_node.rb +112 -0
- data/lib/volt/templates/targets/binding_document/html_node.rb +11 -0
- data/lib/volt/templates/targets/dom_section.rb +147 -0
- data/lib/volt/templates/targets/dom_target.rb +11 -0
- data/lib/volt/templates/template_binding.rb +159 -0
- data/lib/volt/templates/template_renderer.rb +50 -0
- data/spec/models/event_chain_spec.rb +129 -0
- data/spec/models/model_spec.rb +340 -0
- data/spec/models/old_model_spec.rb +109 -0
- data/spec/models/reactive_array_spec.rb +262 -0
- data/spec/models/reactive_tags_spec.rb +35 -0
- data/spec/models/reactive_value_spec.rb +336 -0
- data/spec/models/string_extensions_spec.rb +57 -0
- data/spec/router/routes_spec.rb +24 -0
- data/spec/server/template_parser_spec.rb +50 -0
- data/spec/spec_helper.rb +20 -0
- data/spec/store/mongo_spec.rb +4 -0
- data/spec/templates/targets/binding_document/component_node_spec.rb +18 -0
- data/spec/templates/template_binding_spec.rb +98 -0
- data/templates/.gitignore +12 -0
- data/templates/Gemfile.tt +8 -0
- data/templates/app/.empty_directory +0 -0
- data/templates/app/home/config/routes.rb +1 -0
- data/templates/app/home/controllers/index_controller.rb +5 -0
- data/templates/app/home/css/.empty_directory +0 -0
- data/templates/app/home/models/.empty_directory +0 -0
- data/templates/app/home/views/index/about.html +9 -0
- data/templates/app/home/views/index/home.html +7 -0
- data/templates/app/home/views/index/index.html +28 -0
- data/templates/config.ru +4 -0
- data/templates/public/css/ansi.css +0 -0
- data/templates/public/css/bootstrap-theme.css +459 -0
- data/templates/public/css/bootstrap.css +7098 -0
- data/templates/public/css/jumbotron.css +79 -0
- data/templates/public/fonts/glyphicons-halflings-regular.eot +0 -0
- data/templates/public/fonts/glyphicons-halflings-regular.svg +229 -0
- data/templates/public/fonts/glyphicons-halflings-regular.ttf +0 -0
- data/templates/public/fonts/glyphicons-halflings-regular.woff +0 -0
- data/templates/public/index.html +25 -0
- data/templates/public/js/bootstrap.js +0 -0
- data/templates/public/js/jquery-2.0.3.js +8829 -0
- data/templates/public/js/sockjs-0.2.1.min.js +27 -0
- data/templates/spec/spec_helper.rb +20 -0
- data/volt.gemspec +41 -0
- metadata +412 -0
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
OBJECT_TRACKER_DEBUG = false
|
|
2
|
+
|
|
3
|
+
class ObjectTracker
|
|
4
|
+
@@queue = {}
|
|
5
|
+
@@cache_enabled = false
|
|
6
|
+
@@cache_version = 0
|
|
7
|
+
|
|
8
|
+
def self.cache_enabled
|
|
9
|
+
@@cache_enabled
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def self.cache_version
|
|
13
|
+
@@cache_version
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def self.clear_cache
|
|
17
|
+
@@cache_version = (@@cache_version || 0) + 1
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def initialize(main_object)
|
|
21
|
+
# puts "NEW OBJECT TRACKER FOR: #{main_object.inspect}"
|
|
22
|
+
@main_object = main_object
|
|
23
|
+
@enabled = false
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def self.queue
|
|
27
|
+
@@queue
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def queue_update
|
|
31
|
+
puts "QUEUE UPDATE" if OBJECT_TRACKER_DEBUG
|
|
32
|
+
@@queue[self] = true
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
# Run through the queue and update the followers for each
|
|
36
|
+
def self.process_queue
|
|
37
|
+
# puts "PROCESS QUEUE: #{@@queue.size}"
|
|
38
|
+
puts "Process #{@@queue.size} items" if OBJECT_TRACKER_DEBUG
|
|
39
|
+
# TODO: Doing a full dup here is expensive?
|
|
40
|
+
queue = @@queue.dup
|
|
41
|
+
|
|
42
|
+
# Clear before running incase someone adds during
|
|
43
|
+
@@queue = {}
|
|
44
|
+
|
|
45
|
+
@@cache_enabled = true
|
|
46
|
+
self.clear_cache
|
|
47
|
+
|
|
48
|
+
queue.each_pair do |object_tracker,val|
|
|
49
|
+
object_tracker.update_followers
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
@@cache_enabled = false
|
|
53
|
+
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def enable!
|
|
57
|
+
unless @enabled
|
|
58
|
+
puts "Enable OBJ Tracker" if OBJECT_TRACKER_DEBUG
|
|
59
|
+
@enabled = true
|
|
60
|
+
queue_update
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def disable!
|
|
65
|
+
puts "Disable OBJ Tracker" if OBJECT_TRACKER_DEBUG
|
|
66
|
+
remove_followers
|
|
67
|
+
@@queue.delete(self)
|
|
68
|
+
@enabled = false
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def update_followers
|
|
72
|
+
if @enabled
|
|
73
|
+
puts "UPDATE" if OBJECT_TRACKER_DEBUG
|
|
74
|
+
current_obj = @main_object.cur#(true)
|
|
75
|
+
|
|
76
|
+
# puts "UPDATE ON #{current_obj.inspect}"
|
|
77
|
+
|
|
78
|
+
remove_followers
|
|
79
|
+
|
|
80
|
+
# Add to current
|
|
81
|
+
should_attach = current_obj.respond_to?(:on)
|
|
82
|
+
if should_attach
|
|
83
|
+
current_obj.add_event_follower(@main_object)
|
|
84
|
+
@cached_current_obj = current_obj
|
|
85
|
+
end
|
|
86
|
+
else
|
|
87
|
+
puts "DISABLED, no update" if OBJECT_TRACKER_DEBUG
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
# Remove follower
|
|
92
|
+
def remove_followers
|
|
93
|
+
# Remove from previous
|
|
94
|
+
if @cached_current_obj
|
|
95
|
+
@cached_current_obj.remove_event_follower(@main_object)
|
|
96
|
+
@cached_current_obj = nil
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
end
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# Provides methods for objects that store reactive value's to trigger
|
|
2
|
+
module ObjectTracking
|
|
3
|
+
def __setup_tracking(key, value)
|
|
4
|
+
if value.reactive?
|
|
5
|
+
puts "Value: #{value.inspect} - #{key}"
|
|
6
|
+
# TODO: We should build this in so it fires just for the current index.
|
|
7
|
+
# Currently this is a big performance hit.
|
|
8
|
+
chain_listener = event_chain.add_object(value.reactive_manager) do |event, *args|
|
|
9
|
+
yield(event, key, args)
|
|
10
|
+
end
|
|
11
|
+
@reactive_element_listeners ||= {}
|
|
12
|
+
@reactive_element_listeners[key] = chain_listener
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
end
|
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
require 'volt/reactive/object_tracking'
|
|
2
|
+
|
|
3
|
+
class ReactiveArray# < Array
|
|
4
|
+
include ReactiveTags
|
|
5
|
+
include ObjectTracking
|
|
6
|
+
|
|
7
|
+
def initialize(array=[])
|
|
8
|
+
@array = array
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
# Forward any missing methods to the array
|
|
12
|
+
def method_missing(method_name, *args, &block)
|
|
13
|
+
@array.send(method_name, *args, &block)
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def ==(*args)
|
|
17
|
+
@array.==(*args)
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
tag_method(:[]=) do
|
|
21
|
+
destructive!
|
|
22
|
+
pass_reactive!
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
# alias :__old_assign :[]=
|
|
26
|
+
def []=(index, value)
|
|
27
|
+
index_val = index.cur
|
|
28
|
+
# Clean old value
|
|
29
|
+
__clear_element(index)
|
|
30
|
+
|
|
31
|
+
@array[index.cur] = value
|
|
32
|
+
|
|
33
|
+
# Track new value
|
|
34
|
+
__track_element(index, value)
|
|
35
|
+
|
|
36
|
+
# Also track the index if its reactive
|
|
37
|
+
if index.reactive?
|
|
38
|
+
# TODO: Need to clean this up when the index changes
|
|
39
|
+
event_chain.add_object(index.reactive_manager) do |event, *args|
|
|
40
|
+
trigger_for_index!(event, index.cur)
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
# Trigger changed
|
|
45
|
+
trigger_for_index!('changed', index_val)
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
tag_method(:delete_at) do
|
|
49
|
+
destructive!
|
|
50
|
+
end
|
|
51
|
+
# alias :__old_delete_at :delete_at
|
|
52
|
+
def delete_at(index)
|
|
53
|
+
index_val = index.cur
|
|
54
|
+
|
|
55
|
+
__clear_element(index)
|
|
56
|
+
|
|
57
|
+
@array.delete_at(index_val)
|
|
58
|
+
|
|
59
|
+
trigger_on_direct_listeners!('removed', index_val)
|
|
60
|
+
|
|
61
|
+
# Trigger a changed event for each element in the zone where the
|
|
62
|
+
# lookup would change
|
|
63
|
+
index.upto(self.size+1) do |position|
|
|
64
|
+
trigger_for_index!('changed', position)
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
trigger_size_change!
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
tag_method(:<<) do
|
|
72
|
+
destructive!
|
|
73
|
+
pass_reactive!
|
|
74
|
+
end
|
|
75
|
+
# alias :__old_append :<<
|
|
76
|
+
def <<(value)
|
|
77
|
+
result = (@array << value)
|
|
78
|
+
|
|
79
|
+
# Track new value
|
|
80
|
+
__track_element(self.size-1, value)
|
|
81
|
+
|
|
82
|
+
trigger_for_index!('changed', self.size-1)
|
|
83
|
+
trigger_on_direct_listeners!('added', self.size-1)
|
|
84
|
+
|
|
85
|
+
trigger_size_change!
|
|
86
|
+
|
|
87
|
+
return result
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def +(array)
|
|
92
|
+
old_size = self.size
|
|
93
|
+
|
|
94
|
+
# TODO: += is funky here, might need to make a .plus! method
|
|
95
|
+
result = ReactiveArray.new(@array.dup + array)
|
|
96
|
+
|
|
97
|
+
old_size.upto(result.size-1) do |index|
|
|
98
|
+
trigger_for_index!('changed', index)
|
|
99
|
+
trigger_on_direct_listeners!('added', old_size + index)
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
trigger_size_change!
|
|
103
|
+
|
|
104
|
+
return result
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
tag_method(:insert) do
|
|
108
|
+
destructive!
|
|
109
|
+
end
|
|
110
|
+
# alias :__old_insert :insert
|
|
111
|
+
def insert(*args)
|
|
112
|
+
old_size = self.size
|
|
113
|
+
result = @array.insert(*args)
|
|
114
|
+
|
|
115
|
+
old_size.upto(result.size-1) do |index|
|
|
116
|
+
trigger_for_index!('changed', index)
|
|
117
|
+
trigger_on_direct_listeners!('added', old_size+index)
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
trigger_size_change!
|
|
121
|
+
|
|
122
|
+
return result
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
def trigger_on_direct_listeners!(event, *args)
|
|
126
|
+
trigger_by_scope!(event, *args) do |scope|
|
|
127
|
+
# Only if it is bound directly to us. Don't pass
|
|
128
|
+
# down the chain
|
|
129
|
+
!scope || scope[0] == nil
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
def trigger_size_change!
|
|
135
|
+
trigger_by_scope!('changed') do |scope|
|
|
136
|
+
# method_name, *args, block = scope
|
|
137
|
+
method_name, args, block = split_scope(scope)
|
|
138
|
+
|
|
139
|
+
result = case method_name && method_name.to_sym
|
|
140
|
+
when :size, :length
|
|
141
|
+
true
|
|
142
|
+
else
|
|
143
|
+
false
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
result
|
|
147
|
+
end
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
# TODO: This is an opal work around. Currently there is a bug with destructuring
|
|
151
|
+
# method_name, *args, block = scope
|
|
152
|
+
def split_scope(scope)
|
|
153
|
+
if scope
|
|
154
|
+
scope = scope.dup
|
|
155
|
+
method_name = scope.shift
|
|
156
|
+
block = scope.pop
|
|
157
|
+
|
|
158
|
+
return method_name, scope, block
|
|
159
|
+
else
|
|
160
|
+
return nil,[],nil
|
|
161
|
+
end
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
# Trigger the changed event to any values fetched either through the
|
|
165
|
+
# lookup ([]), #last, or any fetched through the array its self. (sum, max, etc...)
|
|
166
|
+
# On an array, when an element is added or removed, we need to trigger change
|
|
167
|
+
# events on each method that does the following:
|
|
168
|
+
# 1. uses the whole array (max, sum, etc...)
|
|
169
|
+
# 2. accesses this specific element - array[index]
|
|
170
|
+
# 3. accesses an element via a method (first, last)
|
|
171
|
+
def trigger_for_index!(event_name, index, *passed_args)
|
|
172
|
+
self.trigger_by_scope!(event_name, *passed_args) do |scope|
|
|
173
|
+
# method_name, *args, block = scope
|
|
174
|
+
method_name, args, block = split_scope(scope)
|
|
175
|
+
|
|
176
|
+
result = case method_name
|
|
177
|
+
when nil
|
|
178
|
+
# no method name means the event was bound directly, we don't
|
|
179
|
+
# want to trigger changed on the array its self.
|
|
180
|
+
false
|
|
181
|
+
when :[]
|
|
182
|
+
# Extract the current index if its reactive
|
|
183
|
+
arg_index = args[0].cur
|
|
184
|
+
|
|
185
|
+
# TODO: we could handle negative indicies better
|
|
186
|
+
arg_index == index.cur || arg_index < 0
|
|
187
|
+
when :last
|
|
188
|
+
index.cur == self.size-1
|
|
189
|
+
when :first
|
|
190
|
+
index.cur == 0
|
|
191
|
+
when :size, :length
|
|
192
|
+
# Size does not depend on the contents of the cells
|
|
193
|
+
false
|
|
194
|
+
else
|
|
195
|
+
true
|
|
196
|
+
end
|
|
197
|
+
|
|
198
|
+
result
|
|
199
|
+
end
|
|
200
|
+
end
|
|
201
|
+
|
|
202
|
+
def inspect
|
|
203
|
+
"#<#{self.class.to_s} #{@array.inspect}>"
|
|
204
|
+
end
|
|
205
|
+
|
|
206
|
+
private
|
|
207
|
+
|
|
208
|
+
def __clear_element(index)
|
|
209
|
+
# Cleanup any tracking on an index
|
|
210
|
+
if @reactive_element_listeners && self[index].reactive?
|
|
211
|
+
@reactive_element_listeners[index].remove
|
|
212
|
+
@reactive_element_listeners.delete(index)
|
|
213
|
+
end
|
|
214
|
+
end
|
|
215
|
+
|
|
216
|
+
def __track_element(index, value)
|
|
217
|
+
__setup_tracking(index, value) do |event, index, args|
|
|
218
|
+
trigger_for_index!(event, index, *args)
|
|
219
|
+
end
|
|
220
|
+
end
|
|
221
|
+
|
|
222
|
+
end
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
# ReactiveTags provide an easy way to specify how a class deals with
|
|
2
|
+
# reactive events and method calls.als
|
|
3
|
+
module ReactiveTags
|
|
4
|
+
class MethodTags
|
|
5
|
+
attr_accessor :destructive, :pass_reactive, :reacts_with
|
|
6
|
+
end
|
|
7
|
+
|
|
8
|
+
class MethodTagger
|
|
9
|
+
attr_reader :method_tags
|
|
10
|
+
|
|
11
|
+
def initialize
|
|
12
|
+
@method_tags = MethodTags.new
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def destructive!(&block)
|
|
16
|
+
@method_tags.destructive = block || true
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def pass_reactive!
|
|
20
|
+
@method_tags.pass_reactive = true
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
module ClassMethods
|
|
25
|
+
def tag_method(method_name, &block)
|
|
26
|
+
tagger = MethodTagger.new
|
|
27
|
+
|
|
28
|
+
tagger.instance_eval(&block)
|
|
29
|
+
|
|
30
|
+
@reactive_method_tags ||= {}
|
|
31
|
+
@reactive_method_tags[method_name.to_sym] = tagger.method_tags
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def tag_all_methods(&block)
|
|
35
|
+
tagger = MethodTagger.new
|
|
36
|
+
|
|
37
|
+
tagger.instance_eval(&block)
|
|
38
|
+
|
|
39
|
+
@reactive_method_tags ||= {}
|
|
40
|
+
@reactive_method_tags[:__all_methods] = tagger.method_tags
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
# Returns a reference to the tags on a method
|
|
45
|
+
def reactive_method_tag(method_name, tag_name, klass=self.class)
|
|
46
|
+
# Check to make sure we haven't gone above a class that has included
|
|
47
|
+
# ReactiveTags
|
|
48
|
+
return nil if !klass || !klass.method_defined?(:reactive_method_tag)
|
|
49
|
+
|
|
50
|
+
tags = klass.instance_variable_get('@reactive_method_tags')
|
|
51
|
+
|
|
52
|
+
if tags && (tag = tags[method_name.to_sym]) && (tag = tag.send(tag_name))
|
|
53
|
+
return tag
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
return self.reactive_method_tag(method_name, tag_name, klass.superclass)
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def self.included(base)
|
|
61
|
+
base.send(:extend, ClassMethods)
|
|
62
|
+
base.send(:include, Events)
|
|
63
|
+
end
|
|
64
|
+
end
|
|
@@ -0,0 +1,368 @@
|
|
|
1
|
+
require 'volt/reactive/events'
|
|
2
|
+
require 'volt/reactive/reactive_tags'
|
|
3
|
+
require 'volt/reactive/string_extensions'
|
|
4
|
+
require 'volt/reactive/array_extensions'
|
|
5
|
+
require 'volt/reactive/reactive_array'
|
|
6
|
+
require 'volt/reactive/object_tracker'
|
|
7
|
+
|
|
8
|
+
class Object
|
|
9
|
+
def cur
|
|
10
|
+
self
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def reactive?
|
|
14
|
+
false
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
class ReactiveValue < BasicObject
|
|
19
|
+
# methods on ReactiveValues:
|
|
20
|
+
# reactive?, cur, with, on, data, trigger!
|
|
21
|
+
# - everything else is forwarded to the ReactiveManager
|
|
22
|
+
|
|
23
|
+
# Methods we should skip wrapping the results in
|
|
24
|
+
# We skip .hash because in uniq it has .to_int called on it, which needs to
|
|
25
|
+
# return a Fixnum instance.
|
|
26
|
+
# :hash - needs something where .to_int can be called on it and it will
|
|
27
|
+
# return an int
|
|
28
|
+
# :methods- needs to return a straight up array to work with irb tab completion
|
|
29
|
+
# :eql? - needed for .uniq to work correctly
|
|
30
|
+
# :to_ary - in some places ruby expects to get an array back from this method
|
|
31
|
+
SKIP_METHODS = [:hash, :methods, :eql?, :respond_to?, :respond_to_missing?, :to_ary, :to_int]#, :instance_of?, :kind_of?, :to_s, :to_str]
|
|
32
|
+
|
|
33
|
+
def initialize(getter, setter=nil, scope=nil)
|
|
34
|
+
@reactive_manager = ::ReactiveManager.new(getter, setter, scope)
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def reactive?
|
|
38
|
+
true
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# Proxy methods to the ReactiveManager. We want to have as few
|
|
42
|
+
# as possible methods on reactive values, so all other methods
|
|
43
|
+
# are forwarded to the object the reactive value points to.
|
|
44
|
+
[:cur, :cur=, :on, :trigger!].each do |method_name|
|
|
45
|
+
define_method(method_name) do |*args, &block|
|
|
46
|
+
@reactive_manager.send(method_name, *args, &block)
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def reactive_manager
|
|
51
|
+
@reactive_manager
|
|
52
|
+
end
|
|
53
|
+
alias_method :rm, :reactive_manager
|
|
54
|
+
|
|
55
|
+
def check_tag(method_name, tag_name)
|
|
56
|
+
current_obj = cur # TODO: should be cached somehow
|
|
57
|
+
|
|
58
|
+
if current_obj.respond_to?(:reactive_method_tag)
|
|
59
|
+
tag = current_obj.reactive_method_tag(method_name, tag_name)
|
|
60
|
+
|
|
61
|
+
unless tag
|
|
62
|
+
# Get the tag from the all methods if its not directly specified
|
|
63
|
+
tag = current_obj.reactive_method_tag(:__all_methods, tag_name)
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
# Evaluate now if its a proc
|
|
67
|
+
tag = tag.call(method_name) if tag.class == ::Proc
|
|
68
|
+
|
|
69
|
+
return tag
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
return nil
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def puts(*args)
|
|
76
|
+
::Object.send(:puts, *args)
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def method_missing(method_name, *args, &block)
|
|
80
|
+
# Unroll send into a direct call
|
|
81
|
+
if method_name == :send
|
|
82
|
+
method_name, *args = args
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
# Check to see if the method we're calling wants to receive reactive values.
|
|
86
|
+
pass_reactive = check_tag(method_name, :pass_reactive)
|
|
87
|
+
|
|
88
|
+
# For some methods, we pass directly to the current object. This
|
|
89
|
+
# helps ReactiveValue's be well behaved ruby citizens.
|
|
90
|
+
# Also skip if this is a destructive method
|
|
91
|
+
if SKIP_METHODS.include?(method_name) || check_tag(method_name, :destructive)# || (method_name[0] =~ /[a-zA-Z]/ && !cur.is_a?(::Exception))
|
|
92
|
+
pass_args = pass_reactive ? args : args.map{|v| v.cur }
|
|
93
|
+
return cur.__send__(method_name, *pass_args, &block)
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
@block_reactives = []
|
|
97
|
+
result = @reactive_manager.with_and_options(args, pass_reactive) do |val, in_args|
|
|
98
|
+
# When a method is called with a block, we pass in our own block that wraps the
|
|
99
|
+
# block passed in. This way we can pass in any arguments as reactive and track
|
|
100
|
+
# the return values.
|
|
101
|
+
new_block = block
|
|
102
|
+
# index_cache = []
|
|
103
|
+
# index = 0
|
|
104
|
+
#
|
|
105
|
+
# if false && new_block
|
|
106
|
+
# new_block = ::Proc.new do |*block_args|
|
|
107
|
+
# res = block.call(*block_args.map {|v| ::ReactiveValue.new(v) })
|
|
108
|
+
#
|
|
109
|
+
# result.rm.remove_parent!(index_cache[index]) if index_cache[index]
|
|
110
|
+
# puts "index: #{index}"
|
|
111
|
+
# index_cache[index] = res
|
|
112
|
+
#
|
|
113
|
+
# # @block_reactives << res
|
|
114
|
+
# result.rm.add_parent!(res)
|
|
115
|
+
# # puts "Parent Size: #{result.rm.parents.size}"
|
|
116
|
+
#
|
|
117
|
+
# index += 1
|
|
118
|
+
#
|
|
119
|
+
# res.cur
|
|
120
|
+
# end
|
|
121
|
+
# end
|
|
122
|
+
|
|
123
|
+
val.__send__(method_name, *in_args, &new_block)
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
manager = result.reactive_manager
|
|
127
|
+
|
|
128
|
+
setup_setter(manager, method_name, args)
|
|
129
|
+
|
|
130
|
+
manager.set_scope!([method_name, *args, block])
|
|
131
|
+
|
|
132
|
+
# result = result.with(block_reactives) if block
|
|
133
|
+
|
|
134
|
+
return result
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
def setup_setter(manager, method_name, args)
|
|
138
|
+
# See if we can automatically create a setter. If we are fetching a
|
|
139
|
+
# value via a read, we can probably reassign it with .name=
|
|
140
|
+
if args.size == 0
|
|
141
|
+
# TODO: At the moment we are defining a setter on all "reads", this
|
|
142
|
+
# probably has some performance implications
|
|
143
|
+
manager.setter! do |val|
|
|
144
|
+
# Call setter
|
|
145
|
+
self.cur.send(:"#{method_name}=", val)
|
|
146
|
+
end
|
|
147
|
+
elsif args.size == 1 && method_name == :[]
|
|
148
|
+
manager.setter! do |val|
|
|
149
|
+
# Call an array setter
|
|
150
|
+
self.cur.send(:"#{method_name}=", args[0], val)
|
|
151
|
+
end
|
|
152
|
+
end
|
|
153
|
+
end
|
|
154
|
+
#
|
|
155
|
+
# def respond_to?(name, include_private=false)
|
|
156
|
+
# [:event_added, :event_removed].include?(name) || super
|
|
157
|
+
# end
|
|
158
|
+
|
|
159
|
+
def respond_to_missing?(name, include_private=false)
|
|
160
|
+
cur.respond_to?(name)
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
def with(*args, &block)
|
|
164
|
+
return @reactive_manager.with(*args, &block)
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
def inspect
|
|
168
|
+
"@#{cur.inspect}"
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
def pretty_inspect
|
|
172
|
+
inspect
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
# Not 100% sure why, but we need to define this directly, it doesn't call
|
|
176
|
+
# on method missing
|
|
177
|
+
def ==(val)
|
|
178
|
+
method_missing(:==, val)
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
# TODO: this is broke in opal
|
|
182
|
+
def !
|
|
183
|
+
method_missing(:!)
|
|
184
|
+
end
|
|
185
|
+
|
|
186
|
+
def to_s
|
|
187
|
+
cur.to_s
|
|
188
|
+
end
|
|
189
|
+
|
|
190
|
+
def coerce(other)
|
|
191
|
+
if other.reactive?
|
|
192
|
+
return [other, self]
|
|
193
|
+
else
|
|
194
|
+
wrapped_object = ::ReactiveValue.new(other, [])
|
|
195
|
+
return [wrapped_object, self]
|
|
196
|
+
end
|
|
197
|
+
end
|
|
198
|
+
end
|
|
199
|
+
|
|
200
|
+
class ReactiveManager
|
|
201
|
+
include ::Events
|
|
202
|
+
|
|
203
|
+
attr_reader :scope, :parents
|
|
204
|
+
|
|
205
|
+
# When created, ReactiveValue's get a getter (a proc)
|
|
206
|
+
def initialize(getter, setter=nil, scope=nil)
|
|
207
|
+
@getter = getter
|
|
208
|
+
@setter = setter
|
|
209
|
+
@scope = scope
|
|
210
|
+
|
|
211
|
+
@parents = []
|
|
212
|
+
|
|
213
|
+
object_tracker.enable!
|
|
214
|
+
end
|
|
215
|
+
|
|
216
|
+
def reactive?
|
|
217
|
+
true
|
|
218
|
+
end
|
|
219
|
+
|
|
220
|
+
def inspect
|
|
221
|
+
"@<#{self.class.to_s}:#{reactive_object_id} #{cur.inspect}>"
|
|
222
|
+
end
|
|
223
|
+
|
|
224
|
+
def reactive_object_id
|
|
225
|
+
@reactive_object_id ||= rand(100000)
|
|
226
|
+
end
|
|
227
|
+
|
|
228
|
+
|
|
229
|
+
# def event_added(event, scope, first)
|
|
230
|
+
# # When the first event is registered, we need to start listening on our current object
|
|
231
|
+
# # for it to publish events.
|
|
232
|
+
# # object_tracker.enable! if first
|
|
233
|
+
# end
|
|
234
|
+
#
|
|
235
|
+
# def event_removed(event, last)
|
|
236
|
+
# # If no one is listening on the reactive value, then we don't need to listen on our
|
|
237
|
+
# # current object for events, because no one cares.
|
|
238
|
+
# # Note: when we're tracking the current object, it will have one registered changed
|
|
239
|
+
# # event.
|
|
240
|
+
# # update_current_object(true) if last && !has_non_tracking_listeners?
|
|
241
|
+
# # object_tracker.disable! if @listeners.size == 0
|
|
242
|
+
# end
|
|
243
|
+
|
|
244
|
+
def object_tracker
|
|
245
|
+
@object_tracker ||= ::ObjectTracker.new(self)
|
|
246
|
+
end
|
|
247
|
+
|
|
248
|
+
|
|
249
|
+
# Fetch the current value
|
|
250
|
+
def cur
|
|
251
|
+
# if @cached_obj && ObjectTracker.cache_version == @cached_version
|
|
252
|
+
# return @cached_obj
|
|
253
|
+
# end
|
|
254
|
+
|
|
255
|
+
if @getter.class == ::Proc
|
|
256
|
+
# Get the current value, capture any errors
|
|
257
|
+
begin
|
|
258
|
+
result = @getter.call
|
|
259
|
+
rescue => e
|
|
260
|
+
result = e
|
|
261
|
+
end
|
|
262
|
+
else
|
|
263
|
+
# getter is just an object, return it
|
|
264
|
+
result = @getter
|
|
265
|
+
end
|
|
266
|
+
|
|
267
|
+
if result.reactive?
|
|
268
|
+
# Unwrap any stored reactive values
|
|
269
|
+
result = result.cur
|
|
270
|
+
end
|
|
271
|
+
|
|
272
|
+
# if ObjectTracker.cache_enabled
|
|
273
|
+
# @cached_obj = result
|
|
274
|
+
# @cached_version = ObjectTracker.cache_version
|
|
275
|
+
# end
|
|
276
|
+
|
|
277
|
+
return result
|
|
278
|
+
end
|
|
279
|
+
|
|
280
|
+
def cur=(val)
|
|
281
|
+
if @setter
|
|
282
|
+
@setter.call(val)
|
|
283
|
+
elsif @scope == nil
|
|
284
|
+
@getter = val
|
|
285
|
+
@setter = nil
|
|
286
|
+
|
|
287
|
+
trigger!('changed')
|
|
288
|
+
else
|
|
289
|
+
raise "Value can not be updated"
|
|
290
|
+
end
|
|
291
|
+
end
|
|
292
|
+
|
|
293
|
+
# With returns a new reactive value dependent on any arguments passed in.
|
|
294
|
+
# If a block is passed in, the getter is the block its self, which will
|
|
295
|
+
# be passed the .cur and the .cur of any reactive arguments.
|
|
296
|
+
def with(*args, &block)
|
|
297
|
+
return with_and_options(args, false, &block)
|
|
298
|
+
end
|
|
299
|
+
|
|
300
|
+
def with_and_options(args, pass_reactive, &block)
|
|
301
|
+
getter = @getter
|
|
302
|
+
setter = @setter
|
|
303
|
+
scope = @scope
|
|
304
|
+
|
|
305
|
+
if block
|
|
306
|
+
# If a block was passed in, the getter now becomes a proc that calls
|
|
307
|
+
# the passed in block with the right arguments.
|
|
308
|
+
getter = ::Proc.new do
|
|
309
|
+
# Unwrap arguments if the method doesn't want reactive values
|
|
310
|
+
pass_args = pass_reactive ? args : args.map{|v| v.cur }
|
|
311
|
+
|
|
312
|
+
# TODO: Calling cur every time
|
|
313
|
+
current_val = self.cur
|
|
314
|
+
|
|
315
|
+
if current_val.is_a?(Exception)
|
|
316
|
+
current_val
|
|
317
|
+
else
|
|
318
|
+
block.call(current_val, pass_args)
|
|
319
|
+
end
|
|
320
|
+
end
|
|
321
|
+
|
|
322
|
+
# TODO: Make this work with custom setters
|
|
323
|
+
setter = nil
|
|
324
|
+
|
|
325
|
+
# Scope also gets set to nil, because now we should always retrigger this
|
|
326
|
+
# method because we don't know enough about what methods its calling.
|
|
327
|
+
scope = nil
|
|
328
|
+
end
|
|
329
|
+
|
|
330
|
+
new_val = ReactiveValue.new(getter, setter, scope)
|
|
331
|
+
|
|
332
|
+
# Add the ReactiveValue we're building from
|
|
333
|
+
new_val.reactive_manager.add_parent!(self)
|
|
334
|
+
|
|
335
|
+
# Add any reactive arguments as parents
|
|
336
|
+
args.select(&:reactive?).each do |arg|
|
|
337
|
+
new_val.reactive_manager.add_parent!(arg.reactive_manager)
|
|
338
|
+
end
|
|
339
|
+
|
|
340
|
+
return new_val
|
|
341
|
+
end
|
|
342
|
+
|
|
343
|
+
def add_parent!(parent)
|
|
344
|
+
@parents << parent
|
|
345
|
+
event_chain.add_object(parent)
|
|
346
|
+
end
|
|
347
|
+
|
|
348
|
+
def remove_parent!(parent)
|
|
349
|
+
@parents.delete(parent)
|
|
350
|
+
event_chain.remove_object(parent)
|
|
351
|
+
end
|
|
352
|
+
|
|
353
|
+
def set_scope!(new_scope)
|
|
354
|
+
@scope = new_scope
|
|
355
|
+
|
|
356
|
+
self
|
|
357
|
+
end
|
|
358
|
+
|
|
359
|
+
def set_scope(new_scope)
|
|
360
|
+
dup.scope!(new_scope)
|
|
361
|
+
end
|
|
362
|
+
|
|
363
|
+
# Sets the setter
|
|
364
|
+
def setter!(setter=nil, &block)
|
|
365
|
+
@setter = setter || block
|
|
366
|
+
end
|
|
367
|
+
|
|
368
|
+
end
|