reactive-ruby 0.7.3

Sign up to get free protection for your applications and to get access to all the features.
Files changed (69) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +30 -0
  3. data/Gemfile +3 -0
  4. data/Gemfile.lock +53 -0
  5. data/LICENSE +19 -0
  6. data/README.md +303 -0
  7. data/config.ru +15 -0
  8. data/example/examples/Gemfile +7 -0
  9. data/example/examples/Gemfile.lock +45 -0
  10. data/example/examples/config.ru +44 -0
  11. data/example/examples/hello.js.rb +43 -0
  12. data/example/react-tutorial/Gemfile +7 -0
  13. data/example/react-tutorial/Gemfile.lock +49 -0
  14. data/example/react-tutorial/README.md +8 -0
  15. data/example/react-tutorial/_comments.json +14 -0
  16. data/example/react-tutorial/config.ru +63 -0
  17. data/example/react-tutorial/example.js.rb +290 -0
  18. data/example/react-tutorial/public/base.css +62 -0
  19. data/example/todos/Gemfile +11 -0
  20. data/example/todos/Gemfile.lock +84 -0
  21. data/example/todos/README.md +37 -0
  22. data/example/todos/Rakefile +8 -0
  23. data/example/todos/app/application.rb +22 -0
  24. data/example/todos/app/components/app.react.rb +61 -0
  25. data/example/todos/app/components/footer.react.rb +31 -0
  26. data/example/todos/app/components/todo_item.react.rb +46 -0
  27. data/example/todos/app/components/todo_list.react.rb +25 -0
  28. data/example/todos/app/models/todo.rb +19 -0
  29. data/example/todos/config.ru +14 -0
  30. data/example/todos/index.html.haml +16 -0
  31. data/example/todos/spec/todo_spec.rb +28 -0
  32. data/example/todos/vendor/base.css +410 -0
  33. data/example/todos/vendor/bg.png +0 -0
  34. data/example/todos/vendor/jquery.js +4 -0
  35. data/lib/rails-helpers/react_component.rb +32 -0
  36. data/lib/reactive-ruby.rb +23 -0
  37. data/lib/reactive-ruby/api.rb +177 -0
  38. data/lib/reactive-ruby/callbacks.rb +35 -0
  39. data/lib/reactive-ruby/component.rb +411 -0
  40. data/lib/reactive-ruby/element.rb +87 -0
  41. data/lib/reactive-ruby/event.rb +76 -0
  42. data/lib/reactive-ruby/ext/hash.rb +9 -0
  43. data/lib/reactive-ruby/ext/string.rb +8 -0
  44. data/lib/reactive-ruby/isomorphic_helpers.rb +223 -0
  45. data/lib/reactive-ruby/observable.rb +33 -0
  46. data/lib/reactive-ruby/rendering_context.rb +91 -0
  47. data/lib/reactive-ruby/serializers.rb +15 -0
  48. data/lib/reactive-ruby/state.rb +90 -0
  49. data/lib/reactive-ruby/top_level.rb +53 -0
  50. data/lib/reactive-ruby/validator.rb +83 -0
  51. data/lib/reactive-ruby/version.rb +3 -0
  52. data/logo1.png +0 -0
  53. data/logo2.png +0 -0
  54. data/logo3.png +0 -0
  55. data/reactive-ruby.gemspec +25 -0
  56. data/spec/callbacks_spec.rb +107 -0
  57. data/spec/component_spec.rb +597 -0
  58. data/spec/element_spec.rb +60 -0
  59. data/spec/event_spec.rb +22 -0
  60. data/spec/react_spec.rb +209 -0
  61. data/spec/reactjs/index.html.erb +11 -0
  62. data/spec/spec_helper.rb +29 -0
  63. data/spec/tutorial/tutorial_spec.rb +37 -0
  64. data/spec/validator_spec.rb +79 -0
  65. data/vendor/active_support/core_ext/array/extract_options.rb +29 -0
  66. data/vendor/active_support/core_ext/class/attribute.rb +127 -0
  67. data/vendor/active_support/core_ext/kernel/singleton_class.rb +13 -0
  68. data/vendor/active_support/core_ext/module/remove_method.rb +11 -0
  69. metadata +205 -0
@@ -0,0 +1,87 @@
1
+ require "reactive-ruby/ext/string"
2
+
3
+ module React
4
+ class Element
5
+ include Native
6
+
7
+ alias_native :element_type, :type
8
+ alias_native :props, :props
9
+
10
+ attr_reader :type
11
+ attr_reader :properties
12
+ attr_reader :block
13
+
14
+ attr_accessor :waiting_on_resources
15
+
16
+ def initialize(native_element, type, properties, block)
17
+ @type = type
18
+ @properties = properties
19
+ @block = block
20
+ @native = native_element
21
+ end
22
+
23
+ def on(event_name)
24
+ name = event_name.to_s.event_camelize
25
+ if React::Event::BUILT_IN_EVENTS.include?("on#{name}")
26
+ self.props["on#{name}"] = %x{
27
+ function(event){
28
+ #{yield React::Event.new(`event`)}
29
+ }
30
+ }
31
+ else
32
+ self.props["_on#{name}"] = %x{
33
+ function(){
34
+ #{yield *Array(`arguments`)}
35
+ }
36
+ }
37
+ end
38
+ self
39
+ end
40
+
41
+ def method_missing(class_name, args = {}, &new_block)
42
+ class_name = class_name.split("__").collect { |s| s.gsub("_", "-") }.join("_")
43
+ new_props = properties.dup
44
+ new_props["class"] = "#{new_props['class']} #{class_name} #{args.delete("class")} #{args.delete('className')}".split(" ").uniq.join(" ")
45
+ new_props.merge! args
46
+ RenderingContext.replace(
47
+ self,
48
+ React::RenderingContext.build { React::RenderingContext.render(type, new_props, &new_block) }
49
+ )
50
+ end
51
+
52
+ def delete
53
+ RenderingContext.delete(self)
54
+ end
55
+
56
+ def children
57
+ nodes = self.props.children
58
+ class << nodes
59
+ include Enumerable
60
+
61
+ def to_n
62
+ self
63
+ end
64
+
65
+ def each(&block)
66
+ if block_given?
67
+ %x{
68
+ React.Children.forEach(#{self.to_n}, function(context){
69
+ #{block.call(React::Element.new(`context`))}
70
+ })
71
+ }
72
+ else
73
+ Enumerator.new(`React.Children.count(#{self.to_n})`) do |y|
74
+ %x{
75
+ React.Children.forEach(#{self.to_n}, function(context){
76
+ #{y << React::Element.new(`context`)}
77
+ })
78
+ }
79
+ end
80
+ end
81
+ end
82
+ end
83
+
84
+ nodes
85
+ end
86
+ end
87
+ end
@@ -0,0 +1,76 @@
1
+ module React
2
+ class Event
3
+ include Native
4
+ alias_native :bubbles, :bubbles
5
+ alias_native :cancelable, :cancelable
6
+ alias_native :current_target, :currentTarget
7
+ alias_native :default_prevented, :defaultPrevented
8
+ alias_native :event_phase, :eventPhase
9
+ alias_native :is_trusted?, :isTrusted
10
+ alias_native :native_event, :nativeEvent
11
+ alias_native :target, :target
12
+ alias_native :timestamp, :timeStamp
13
+ alias_native :event_type, :type
14
+ alias_native :prevent_default, :preventDefault
15
+ alias_native :stop_propagation, :stopPropagation
16
+ # Clipboard
17
+ alias_native :clipboard_data, :clipboardData
18
+ # Keyboard
19
+ alias_native :alt_key, :altKey
20
+ alias_native :char_code, :charCode
21
+ alias_native :ctrl_key, :ctrlKey
22
+ alias_native :get_modifier_state, :getModifierState
23
+ alias_native :key, :key
24
+ alias_native :key_code, :keyCode
25
+ alias_native :locale, :locale
26
+ alias_native :location, :location
27
+ alias_native :meta_key, :metaKey
28
+ alias_native :repeat, :repeat
29
+ alias_native :shift_key, :shiftKey
30
+ alias_native :which, :which
31
+ # Focus
32
+ alias_native :related_target, :relatedTarget
33
+ # Mouse
34
+ alias_native :alt_key, :altKey
35
+ alias_native :button, :button
36
+ alias_native :buttons, :buttons
37
+ alias_native :client_x, :clientX
38
+ alias_native :client_y, :clientY
39
+ alias_native :ctrl_key, :ctrlKey
40
+ alias_native :get_modifier_state, :getModifierState
41
+ alias_native :meta_key, :metaKey
42
+ alias_native :page_x, :pageX
43
+ alias_native :page_y, :pageY
44
+ alias_native :related_target, :relatedTarget
45
+ alias_native :screen_x, :screen_x
46
+ alias_native :screen_y, :screen_y
47
+ alias_native :shift_key, :shift_key
48
+ # Touch
49
+ alias_native :alt_key, :altKey
50
+ alias_native :changed_touches, :changedTouches
51
+ alias_native :ctrl_key, :ctrlKey
52
+ alias_native :get_modifier_state, :getModifierState
53
+ alias_native :meta_key, :metaKey
54
+ alias_native :shift_key, :shiftKey
55
+ alias_native :target_touches, :targetTouches
56
+ alias_native :touches, :touches
57
+ # UI
58
+ alias_native :detail, :detail
59
+ alias_native :view, :view
60
+ # Wheel
61
+ alias_native :delta_mode, :deltaMode
62
+ alias_native :delta_x, :deltaX
63
+ alias_native :delta_y, :deltaY
64
+ alias_native :delta_z, :deltaZ
65
+
66
+ BUILT_IN_EVENTS = %w{onCopy onCut onPaste onKeyDown onKeyPress onKeyUp
67
+ onFocus onBlur onChange onInput onSubmit onClick onDoubleClick onDrag
68
+ onDragEnd onDragEnter onDragExit onDragLeave onDragOver onDragStart onDrop
69
+ onMouseDown onMouseEnter onMouseLeave onMouseMove onMouseOut onMouseOver
70
+ onMouseUp onTouchCancel onTouchEnd onTouchMove onTouchStart onScroll}
71
+
72
+ def initialize(native_element)
73
+ @native = native_element
74
+ end
75
+ end
76
+ end
@@ -0,0 +1,9 @@
1
+ class Hash
2
+ def shallow_to_n
3
+ hash = `{}`
4
+ self.map do |key, value|
5
+ `hash[#{key}] = #{value}`
6
+ end
7
+ hash
8
+ end
9
+ end
@@ -0,0 +1,8 @@
1
+ class String
2
+ def event_camelize
3
+ `#{self}.replace(/(^|_)([^_]+)/g, function(match, pre, word, index) {
4
+ var capitalize = true;
5
+ return capitalize ? word.substr(0,1).toUpperCase()+word.substr(1) : word;
6
+ })`
7
+ end
8
+ end
@@ -0,0 +1,223 @@
1
+ module React
2
+
3
+ module IsomorphicHelpers
4
+
5
+ def self.included(base)
6
+ base.extend(ClassMethods)
7
+ end
8
+
9
+ if RUBY_ENGINE != 'opal'
10
+
11
+ def self.load_context(ctx, controller)
12
+ puts "************************** React Server Context Initialized *********************************************"
13
+ @context = Context.new("#{controller.object_id}-#{Time.now.to_i}", ctx, controller)
14
+ end
15
+
16
+ else
17
+
18
+ def self.load_context(unique_id = nil) # can be called on the client to force re-initialization for testing purposes
19
+ if !unique_id or !@context or @context.unique_id != unique_id
20
+ if on_opal_server?
21
+ message = "************************ React Prerendering Context Initialized ***********************"
22
+ else
23
+ message = "************************ React Browser Context Initialized ****************************"
24
+ end
25
+ log(message)
26
+ @context = Context.new(unique_id)
27
+ end
28
+ @context
29
+ end
30
+
31
+ end
32
+
33
+ def self.log(message, message_type = :info)
34
+ message = [message] unless message.is_a? Array
35
+ if message_type == :info
36
+ if on_opal_server?
37
+ style = 'background: #00FFFF; color: red'
38
+ else
39
+ style = 'background: #222; color: #bada55'
40
+ end
41
+ message = ["%c" + message[0], style]+message[1..-1]
42
+ `console.log.apply(console, message)`
43
+ elsif message_type == :warning
44
+ `console.warn.apply(console, message)`
45
+ else
46
+ `console.error.apply(console, message)`
47
+ end
48
+ end
49
+
50
+ if RUBY_ENGINE != 'opal'
51
+
52
+ def self.on_opal_server?
53
+ false
54
+ end
55
+
56
+ def self.on_opal_client?
57
+ false
58
+ end
59
+
60
+ else
61
+
62
+ def self.on_opal_server?
63
+ `typeof window.document === 'undefined'`
64
+ end
65
+
66
+ def self.on_opal_client?
67
+ !on_opal_server?
68
+ end
69
+
70
+ end
71
+
72
+ def log(*args)
73
+ IsomorphicHelpers.log(*args)
74
+ end
75
+
76
+ def on_opal_server?
77
+ IsomorphicHelpers.on_opal_server?
78
+ end
79
+
80
+ def on_opal_client?
81
+ IsomorphicHelpers.on_opal_client?
82
+ end
83
+
84
+ def self.prerender_footers
85
+ footer = Context.prerender_footer_blocks.collect { |block| block.call }.join("\n")
86
+ if RUBY_ENGINE != 'opal'
87
+ footer = (footer + "#{@context.send_to_opal(:prerender_footers)}") if @context
88
+ footer = footer.html_safe
89
+ end
90
+ footer
91
+ end
92
+
93
+ class Context
94
+
95
+ attr_reader :controller
96
+ attr_reader :unique_id
97
+
98
+ def self.before_first_mount_blocks
99
+ @before_first_mount_blocks ||= []
100
+ end
101
+
102
+ def self.prerender_footer_blocks
103
+ @prerender_footer_blocks ||= []
104
+ end
105
+
106
+ def initialize(unique_id, ctx = nil, controller = nil)
107
+ @unique_id = unique_id
108
+ if RUBY_ENGINE != 'opal'
109
+ @controller = controller
110
+ @ctx = ctx
111
+ ctx["ServerSideIsomorphicMethods"] = self
112
+ send_to_opal(:load_context, @unique_id)
113
+ end
114
+ self.class.before_first_mount_blocks.each { |block| block.call(self) }
115
+ end
116
+
117
+ def eval(js)
118
+ @ctx.eval(js) if @ctx
119
+ end
120
+
121
+ def send_to_opal(method, *args)
122
+ args = [1] if args.length == 0
123
+ if @ctx
124
+ unless @ctx.eval('Opal.React')
125
+ @ctx.eval(Opal::Processor.load_asset_code(::Rails.application.assets, 'components')) rescue nil
126
+ raise "No opal-react components found in the components.rb file" unless @ctx.eval('Opal.React')
127
+ end
128
+ @ctx.eval("Opal.React.IsomorphicHelpers.$#{method}(#{args.join(', ')})")
129
+ end
130
+ end
131
+
132
+ def self.register_before_first_mount_block(&block)
133
+ before_first_mount_blocks << block
134
+ end
135
+
136
+ def self.register_prerender_footer_block(&block)
137
+ prerender_footer_blocks << block
138
+ end
139
+
140
+ end
141
+
142
+ class IsomorphicProcCall
143
+
144
+ def result
145
+ @result.first if @result
146
+ end
147
+
148
+ def initialize(name, block, *args)
149
+ @name = name
150
+ block.call(self, *args)
151
+ @result ||= send_to_server(*args) if IsomorphicHelpers.on_opal_server?
152
+ end
153
+
154
+ def when_on_client(&block)
155
+ @result = [block.call] if IsomorphicHelpers.on_opal_client?
156
+ end
157
+
158
+ def send_to_server(*args)
159
+ if IsomorphicHelpers.on_opal_server?
160
+ args_as_json = args.to_json
161
+ @result = [JSON.parse(`window.ServerSideIsomorphicMethods[#{@name}](#{args_as_json})`)]
162
+ end
163
+ end
164
+
165
+ def when_on_server(&block)
166
+ @result = [block.call.to_json] unless IsomorphicHelpers.on_opal_client? or IsomorphicHelpers.on_opal_server?
167
+ end
168
+
169
+ end
170
+
171
+
172
+ module ClassMethods
173
+
174
+ def on_opal_server?
175
+ IsomorphicHelpers.on_opal_server?
176
+ end
177
+
178
+ def on_opal_client?
179
+ IsomorphicHelpers.on_opal_client?
180
+ end
181
+
182
+ def log(*args)
183
+ IsomorphicHelpers.log(*args)
184
+ end
185
+
186
+ def controller
187
+ IsomorphicHelpers.context.controller
188
+ end
189
+
190
+ def before_first_mount(&block)
191
+ React::IsomorphicHelpers::Context.register_before_first_mount_block &block
192
+ end
193
+
194
+ def prerender_footer(&block)
195
+ React::IsomorphicHelpers::Context.register_prerender_footer_block &block
196
+ end
197
+
198
+ if RUBY_ENGINE != 'opal'
199
+
200
+ def isomorphic_method(name, &block)
201
+ React::IsomorphicHelpers::Context.send(:define_method, name) do |args_as_json|
202
+ React::IsomorphicHelpers::IsomorphicProcCall.new(name, block, *JSON.parse(args_as_json)).result
203
+ end
204
+ end
205
+
206
+ else
207
+
208
+ require 'json'
209
+
210
+ def isomorphic_method(name, &block)
211
+ self.class.send(:define_method, name) do | *args |
212
+ React::IsomorphicHelpers::IsomorphicProcCall.new(name, block, *args).result
213
+ end
214
+ end
215
+
216
+ end
217
+
218
+
219
+ end
220
+
221
+ end
222
+
223
+ end
@@ -0,0 +1,33 @@
1
+ module React
2
+
3
+ class Observable
4
+
5
+ def initialize(value, on_change = nil, &block)
6
+ @value = value
7
+ @on_change = on_change || block
8
+ end
9
+
10
+ def method_missing(method_sym, *args, &block)
11
+ @value.send(method_sym, *args, &block).tap { |result| @on_change.call result }
12
+ end
13
+
14
+ def respond_to?(method, *args)
15
+ if [:call, :to_proc].include? method
16
+ true
17
+ else
18
+ @value.respond_to? method, *args
19
+ end
20
+ end
21
+
22
+ def call(new_value)
23
+ @on_change.call new_value
24
+ @value = new_value
25
+ end
26
+
27
+ def to_proc
28
+ lambda { |arg = @value| @on_change.call arg }
29
+ end
30
+
31
+ end
32
+
33
+ end
@@ -0,0 +1,91 @@
1
+ module React
2
+
3
+ class RenderingContext
4
+
5
+ class << self
6
+ attr_accessor :waiting_on_resources
7
+ end
8
+
9
+ def self.render(name, *args, &block)
10
+ @buffer = [] unless @buffer
11
+ if block
12
+ element = build do
13
+ saved_waiting_on_resources = waiting_on_resources
14
+ self.waiting_on_resources = nil
15
+ result = block.call
16
+ # Todo figure out how children rendering should happen, probably should have special method that pushes children into the buffer
17
+ # i.e. render_child/render_children that takes Element/Array[Element] and does the push into the buffer
18
+ if !name and ( # !name means called from outer render so we check that it has rendered correctly
19
+ (@buffer.count > 1) or # should only render one element
20
+ (@buffer.count == 1 and @buffer.last != result) or # it should return that element
21
+ (@buffer.count == 0 and !(result.is_a? String or (result.respond_to? :acts_as_string? and result.acts_as_string?) or result.is_a? Element)) #for convience we will also convert the return value to a span if its a string
22
+ )
23
+ raise "a components render method must generate and return exactly 1 element or a string"
24
+ end
25
+
26
+ @buffer << result.to_s if result.is_a? String or (result.respond_to? :acts_as_string? and result.acts_as_string?) # For convience we push the last return value on if its a string
27
+ @buffer << result if result.is_a? Element and @buffer.count == 0
28
+ if name
29
+ buffer = @buffer.dup
30
+ React.create_element(name, *args) { buffer }.tap do |element|
31
+ element.waiting_on_resources = saved_waiting_on_resources || !!buffer.detect { |e| e.waiting_on_resources if e.respond_to? :waiting_on_resources }
32
+ end
33
+ elsif @buffer.last.is_a? React::Element
34
+ @buffer.last.tap { |element| element.waiting_on_resources ||= saved_waiting_on_resources }
35
+ else
36
+ @buffer.last.to_s.span.tap { |element| element.waiting_on_resources = saved_waiting_on_resources }
37
+ end
38
+ end
39
+ else
40
+ element = React.create_element(name, *args)
41
+ element.waiting_on_resources = waiting_on_resources
42
+ end
43
+ @buffer << element
44
+ self.waiting_on_resources = nil
45
+ element
46
+ end
47
+
48
+ def self.build(&block)
49
+ current = @buffer
50
+ @buffer = []
51
+ return_val = yield @buffer
52
+ @buffer = current
53
+ return_val
54
+ #ensure
55
+ # @buffer = current
56
+ # return_val
57
+ end
58
+
59
+ def self.as_node(element)
60
+ @buffer.delete(element)
61
+ element
62
+ end
63
+
64
+ class << self; alias_method :delete, :as_node; end
65
+
66
+ def self.replace(e1, e2)
67
+ @buffer[@buffer.index(e1)] = e2
68
+ end
69
+
70
+ end
71
+
72
+ class ::Object
73
+
74
+ alias_method :old_method_missing, :method_missing
75
+
76
+ ["span", "para", "td", "th", "while_loading"].each do |tag|
77
+ define_method(tag) do | *args, &block |
78
+ args.unshift(tag)
79
+ return self.method_missing(*args, &block) if self.is_a? React::Component
80
+ React::RenderingContext.render(*args) { self.to_s }
81
+ end
82
+ end
83
+
84
+ def br
85
+ return self.method_missing(*["br"]) if self.is_a? React::Component
86
+ React::RenderingContext.render("span") { React::RenderingContext.render(self.to_s); React::RenderingContext.render("br") }
87
+ end
88
+
89
+ end
90
+
91
+ end