volt 0.2.3

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