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.
Files changed (116) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +21 -0
  3. data/.rspec +2 -0
  4. data/.ruby-version +1 -0
  5. data/Gemfile +37 -0
  6. data/Guardfile +9 -0
  7. data/LICENSE.txt +22 -0
  8. data/Rakefile +23 -0
  9. data/Readme.md +34 -0
  10. data/VERSION +1 -0
  11. data/bin/volt +4 -0
  12. data/docs/GETTING_STARTED.md +7 -0
  13. data/docs/GUIDE.md +33 -0
  14. data/lib/volt.rb +15 -0
  15. data/lib/volt/benchmark/benchmark.rb +25 -0
  16. data/lib/volt/cli.rb +34 -0
  17. data/lib/volt/console.rb +19 -0
  18. data/lib/volt/controllers/model_controller.rb +29 -0
  19. data/lib/volt/extra_core/array.rb +10 -0
  20. data/lib/volt/extra_core/blank.rb +88 -0
  21. data/lib/volt/extra_core/extra_core.rb +7 -0
  22. data/lib/volt/extra_core/numeric.rb +9 -0
  23. data/lib/volt/extra_core/object.rb +36 -0
  24. data/lib/volt/extra_core/string.rb +29 -0
  25. data/lib/volt/extra_core/stringify_keys.rb +7 -0
  26. data/lib/volt/extra_core/true_false.rb +44 -0
  27. data/lib/volt/extra_core/try.rb +31 -0
  28. data/lib/volt/models.rb +5 -0
  29. data/lib/volt/models/array_model.rb +37 -0
  30. data/lib/volt/models/model.rb +210 -0
  31. data/lib/volt/models/model_wrapper.rb +23 -0
  32. data/lib/volt/models/params.rb +67 -0
  33. data/lib/volt/models/url.rb +192 -0
  34. data/lib/volt/page/url_tracker.rb +36 -0
  35. data/lib/volt/reactive/array_extensions.rb +13 -0
  36. data/lib/volt/reactive/event_chain.rb +126 -0
  37. data/lib/volt/reactive/events.rb +283 -0
  38. data/lib/volt/reactive/object_tracker.rb +99 -0
  39. data/lib/volt/reactive/object_tracking.rb +15 -0
  40. data/lib/volt/reactive/reactive_array.rb +222 -0
  41. data/lib/volt/reactive/reactive_tags.rb +64 -0
  42. data/lib/volt/reactive/reactive_value.rb +368 -0
  43. data/lib/volt/reactive/string_extensions.rb +34 -0
  44. data/lib/volt/router/routes.rb +83 -0
  45. data/lib/volt/server.rb +121 -0
  46. data/lib/volt/server/binding_setup.rb +2 -0
  47. data/lib/volt/server/channel_handler.rb +31 -0
  48. data/lib/volt/server/component_handler.rb +88 -0
  49. data/lib/volt/server/if_binding_setup.rb +29 -0
  50. data/lib/volt/server/request_handler.rb +16 -0
  51. data/lib/volt/server/scope.rb +43 -0
  52. data/lib/volt/server/source_map_server.rb +31 -0
  53. data/lib/volt/server/template_parser.rb +452 -0
  54. data/lib/volt/store/mongo.rb +5 -0
  55. data/lib/volt/templates/attribute_binding.rb +110 -0
  56. data/lib/volt/templates/base_binding.rb +37 -0
  57. data/lib/volt/templates/channel.rb +48 -0
  58. data/lib/volt/templates/content_binding.rb +35 -0
  59. data/lib/volt/templates/document_events.rb +80 -0
  60. data/lib/volt/templates/each_binding.rb +115 -0
  61. data/lib/volt/templates/event_binding.rb +51 -0
  62. data/lib/volt/templates/if_binding.rb +74 -0
  63. data/lib/volt/templates/memory_test.rb +26 -0
  64. data/lib/volt/templates/page.rb +146 -0
  65. data/lib/volt/templates/reactive_template.rb +38 -0
  66. data/lib/volt/templates/render_queue.rb +5 -0
  67. data/lib/volt/templates/sub_context.rb +23 -0
  68. data/lib/volt/templates/targets/attribute_section.rb +33 -0
  69. data/lib/volt/templates/targets/attribute_target.rb +18 -0
  70. data/lib/volt/templates/targets/base_section.rb +14 -0
  71. data/lib/volt/templates/targets/binding_document/base_node.rb +3 -0
  72. data/lib/volt/templates/targets/binding_document/component_node.rb +112 -0
  73. data/lib/volt/templates/targets/binding_document/html_node.rb +11 -0
  74. data/lib/volt/templates/targets/dom_section.rb +147 -0
  75. data/lib/volt/templates/targets/dom_target.rb +11 -0
  76. data/lib/volt/templates/template_binding.rb +159 -0
  77. data/lib/volt/templates/template_renderer.rb +50 -0
  78. data/spec/models/event_chain_spec.rb +129 -0
  79. data/spec/models/model_spec.rb +340 -0
  80. data/spec/models/old_model_spec.rb +109 -0
  81. data/spec/models/reactive_array_spec.rb +262 -0
  82. data/spec/models/reactive_tags_spec.rb +35 -0
  83. data/spec/models/reactive_value_spec.rb +336 -0
  84. data/spec/models/string_extensions_spec.rb +57 -0
  85. data/spec/router/routes_spec.rb +24 -0
  86. data/spec/server/template_parser_spec.rb +50 -0
  87. data/spec/spec_helper.rb +20 -0
  88. data/spec/store/mongo_spec.rb +4 -0
  89. data/spec/templates/targets/binding_document/component_node_spec.rb +18 -0
  90. data/spec/templates/template_binding_spec.rb +98 -0
  91. data/templates/.gitignore +12 -0
  92. data/templates/Gemfile.tt +8 -0
  93. data/templates/app/.empty_directory +0 -0
  94. data/templates/app/home/config/routes.rb +1 -0
  95. data/templates/app/home/controllers/index_controller.rb +5 -0
  96. data/templates/app/home/css/.empty_directory +0 -0
  97. data/templates/app/home/models/.empty_directory +0 -0
  98. data/templates/app/home/views/index/about.html +9 -0
  99. data/templates/app/home/views/index/home.html +7 -0
  100. data/templates/app/home/views/index/index.html +28 -0
  101. data/templates/config.ru +4 -0
  102. data/templates/public/css/ansi.css +0 -0
  103. data/templates/public/css/bootstrap-theme.css +459 -0
  104. data/templates/public/css/bootstrap.css +7098 -0
  105. data/templates/public/css/jumbotron.css +79 -0
  106. data/templates/public/fonts/glyphicons-halflings-regular.eot +0 -0
  107. data/templates/public/fonts/glyphicons-halflings-regular.svg +229 -0
  108. data/templates/public/fonts/glyphicons-halflings-regular.ttf +0 -0
  109. data/templates/public/fonts/glyphicons-halflings-regular.woff +0 -0
  110. data/templates/public/index.html +25 -0
  111. data/templates/public/js/bootstrap.js +0 -0
  112. data/templates/public/js/jquery-2.0.3.js +8829 -0
  113. data/templates/public/js/sockjs-0.2.1.min.js +27 -0
  114. data/templates/spec/spec_helper.rb +20 -0
  115. data/volt.gemspec +41 -0
  116. 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