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