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,15 @@
1
+ [Bignum, FalseClass, Fixnum, Float, Integer, NilClass, String, Symbol, Time, TrueClass].each do |klass|
2
+ klass.send(:define_method, :react_serializer) do
3
+ as_json
4
+ end
5
+ end
6
+
7
+ BigDecimal.send(:define_method, :react_serializer) { as_json } rescue nil
8
+
9
+ Array.send(:define_method, :react_serializer) do
10
+ self.collect { |e| e.react_serializer }.as_json
11
+ end
12
+
13
+ Hash.send(:define_method, :react_serializer) do
14
+ Hash[*self.collect { |key, value| [key, value.react_serializer] }.flatten(1)].as_json
15
+ end
@@ -0,0 +1,90 @@
1
+ module React
2
+
3
+ class State
4
+
5
+ class << self
6
+
7
+ attr_reader :current_observer
8
+
9
+ def initialize_states(object, initial_values) # initialize objects' name/value pairs
10
+ states[object].merge!(initial_values || {})
11
+ end
12
+
13
+ def get_state(object, name, current_observer = @current_observer)
14
+ # get current value of name for object, remember that the current object depends on this state, current observer can be overriden with last param
15
+ new_observers[current_observer][object] << name if current_observer and !new_observers[current_observer][object].include? name
16
+ states[object][name]
17
+ end
18
+
19
+ def set_state(object, name, value) # set object's name state to value, tell all observers it has changed. Observers must implement update_react_js_state
20
+ states[object][name] = value
21
+ observers_by_name[object][name].dup.each do |observer|
22
+ observer.update_react_js_state(object, name, value)
23
+ end
24
+ value
25
+ end
26
+
27
+ def will_be_observing?(object, name, current_observer)
28
+ current_observer and new_observers[current_observer][object].include?(name)
29
+ end
30
+
31
+ def is_observing?(object, name, current_observer)
32
+ current_observer and observers_by_name[object][name].include?(current_observer)
33
+ end
34
+
35
+ def update_states_to_observe(current_observer = @current_observer) # should be called after the last after_render callback, currently called after components render method
36
+ raise "update_states_to_observer called outside of watch block" unless current_observer
37
+ current_observers[current_observer].each do |object, names|
38
+ names.each do |name|
39
+ observers_by_name[object][name].delete(current_observer)
40
+ end
41
+ end
42
+ observers = current_observers[current_observer] = new_observers[current_observer]
43
+ new_observers.delete(current_observer)
44
+ observers.each do |object, names|
45
+ names.each do |name|
46
+ observers_by_name[object][name] << current_observer
47
+ end
48
+ end
49
+ end
50
+
51
+ def remove # call after component is unmounted
52
+ raise "remove called outside of watch block" unless @current_observer
53
+ current_observers[@current_observer].each do |object, names|
54
+ names.each do |name|
55
+ observers_by_name[object][name].delete(@current_observer)
56
+ end
57
+ end
58
+ current_observers.delete(@current_observer)
59
+ end
60
+
61
+ def set_state_context_to(observer) # wrap all execution that may set or get states in a block so we know which observer is executing
62
+ saved_current_observer = @current_observer
63
+ @current_observer = observer
64
+ return_value = yield
65
+ ensure
66
+ @current_observer = saved_current_observer
67
+ return_value
68
+ end
69
+
70
+ def states
71
+ @states ||= Hash.new { |h, k| h[k] = {} }
72
+ end
73
+
74
+ def new_observers
75
+ @new_observers ||= Hash.new { |h, k| h[k] = Hash.new { |h, k| h[k] = [] } }
76
+ end
77
+
78
+ def current_observers
79
+ @current_observers ||= Hash.new { |h, k| h[k] = Hash.new { |h, k| h[k] = [] } }
80
+ end
81
+
82
+ def observers_by_name
83
+ @observers_by_name ||= Hash.new { |h, k| h[k] = Hash.new { |h, k| h[k] = [] } }
84
+ end
85
+
86
+ end
87
+
88
+ end
89
+
90
+ end
@@ -0,0 +1,53 @@
1
+ require "native"
2
+ require 'active_support'
3
+ require 'reactive-ruby/component'
4
+
5
+ module React
6
+ HTML_TAGS = %w(a abbr address area article aside audio b base bdi bdo big blockquote body br
7
+ button canvas caption cite code col colgroup data datalist dd del details dfn
8
+ dialog div dl dt em embed fieldset figcaption figure footer form h1 h2 h3 h4 h5
9
+ h6 head header hr html i iframe img input ins kbd keygen label legend li link
10
+ main map mark menu menuitem meta meter nav noscript object ol optgroup option
11
+ output p param picture pre progress q rp rt ruby s samp script section select
12
+ small source span strong style sub summary sup table tbody td textarea tfoot th
13
+ thead time title tr track u ul var video wbr)
14
+ ATTRIBUTES = %w(accept acceptCharset accessKey action allowFullScreen allowTransparency alt
15
+ async autoComplete autoPlay cellPadding cellSpacing charSet checked classID
16
+ className cols colSpan content contentEditable contextMenu controls coords
17
+ crossOrigin data dateTime defer dir disabled download draggable encType form
18
+ formAction formEncType formMethod formNoValidate formTarget frameBorder height
19
+ hidden href hrefLang htmlFor httpEquiv icon id label lang list loop manifest
20
+ marginHeight marginWidth max maxLength media mediaGroup method min multiple
21
+ muted name noValidate open pattern placeholder poster preload radioGroup
22
+ readOnly rel required role rows rowSpan sandbox scope scrolling seamless
23
+ selected shape size sizes span spellCheck src srcDoc srcSet start step style
24
+ tabIndex target title type useMap value width wmode dangerouslySetInnerHTML)
25
+
26
+ def self.create_element(type, properties = {}, &block)
27
+ React::API.create_element(type, properties, &block)
28
+ end
29
+
30
+ def self.render(element, container)
31
+ container = `container.$$class ? container[0] : container`
32
+ component = Native(`React.render(#{element.to_n}, container, function(){#{yield if block_given?}})`)
33
+ component.class.include(React::Component::API)
34
+ component
35
+ end
36
+
37
+ def self.is_valid_element(element)
38
+ element.kind_of?(React::Element) && `React.isValidElement(#{element.to_n})`
39
+ end
40
+
41
+ def self.render_to_string(element)
42
+ React::RenderingContext.build { `React.renderToString(#{element.to_n})` }
43
+ end
44
+
45
+ def self.render_to_static_markup(element)
46
+ React::RenderingContext.build { `React.renderToStaticMarkup(#{element.to_n})` }
47
+ end
48
+
49
+ def self.unmount_component_at_node(node)
50
+ `React.unmountComponentAtNode(node.$$class ? node[0] : node)`
51
+ end
52
+
53
+ end
@@ -0,0 +1,83 @@
1
+ module React
2
+ class Validator
3
+
4
+ def self.build(&block)
5
+ self.new.build(&block)
6
+ end
7
+
8
+ def build(&block)
9
+ instance_eval(&block)
10
+ self
11
+ end
12
+
13
+ def initialize
14
+ @rules = {}
15
+ end
16
+
17
+ def requires(prop_name, options = {})
18
+ rule = options
19
+ options[:required] = true
20
+ @rules[prop_name] = options
21
+ end
22
+
23
+ def optional(prop_name, options = {})
24
+ rule = options
25
+ options[:required] = false
26
+ @rules[prop_name] = options
27
+ end
28
+
29
+ def type_check(errors, error_prefix, object, klass)
30
+ is_native = !object.respond_to?(:is_a?) rescue true
31
+ if is_native or !object.is_a? klass
32
+ unless klass.respond_to? :_react_param_conversion and klass._react_param_conversion object, :validate_only
33
+ errors << "#{error_prefix} could not be converted to #{klass}" unless klass._react_param_conversion object, :validate_only
34
+ end
35
+ end
36
+ end
37
+
38
+ def validate(props)
39
+ errors = []
40
+ props.keys.each do |prop_name|
41
+ errors << "Provided prop `#{prop_name}` not specified in spec" if @rules[prop_name] == nil
42
+ end
43
+
44
+ props = props.select {|key| @rules.keys.include?(key) }
45
+
46
+ # requires or not
47
+ (@rules.keys - props.keys).each do |prop_name|
48
+ errors << "Required prop `#{prop_name}` was not specified" if @rules[prop_name][:required]
49
+ end
50
+ # type checking
51
+ props.each do |prop_name, value|
52
+ if klass = @rules[prop_name][:type]
53
+ is_klass_array = klass.is_a?(Array) and klass.length > 0 rescue nil
54
+ if is_klass_array
55
+ value_is_array_like = value.respond_to?(:each_with_index) rescue nil
56
+ if value_is_array_like
57
+ value.each_with_index { |ele, i| type_check(errors, "Provided prop `#{prop_name}`[#{i}]", ele, klass[0]) }
58
+ else
59
+ errors << "Provided prop `#{prop_name}` was not an Array"
60
+ end
61
+ else
62
+ type_check(errors, "Provided prop `#{prop_name}`", value, klass)
63
+ end
64
+ end
65
+ end
66
+
67
+ # values
68
+ props.each do |prop_name, value|
69
+ if values = @rules[prop_name][:values]
70
+ errors << "Value `#{value}` for prop `#{prop_name}` is not an allowed value" unless values.include?(value)
71
+ end
72
+ end
73
+
74
+ errors
75
+ end
76
+
77
+ def default_props
78
+ @rules
79
+ .select {|key, value| value.keys.include?("default") }
80
+ .inject({}) {|memo, (k,v)| memo[k] = v[:default]; memo}
81
+ end
82
+ end
83
+ end
@@ -0,0 +1,3 @@
1
+ module React
2
+ VERSION = "0.7.3"
3
+ end
data/logo1.png ADDED
Binary file
data/logo2.png ADDED
Binary file
data/logo3.png ADDED
Binary file
@@ -0,0 +1,25 @@
1
+ # -*- encoding: utf-8 -*-
2
+ require File.expand_path('../lib/reactive-ruby/version', __FILE__)
3
+
4
+ Gem::Specification.new do |s|
5
+ s.name = 'reactive-ruby'
6
+ s.version = React::VERSION
7
+ s.author = 'David Chang'
8
+ s.email = 'zeta11235813@gmail.com'
9
+ s.homepage = 'https://github.com/zetachang/react.rb'
10
+ s.summary = 'Opal Ruby wrapper of React.js library.'
11
+ s.license = 'MIT'
12
+ s.description = "Write reactive UI component with Ruby's elegancy and compiled to run in Javascript."
13
+
14
+ s.files = `git ls-files`.split("\n")
15
+ s.executables = `git ls-files -- bin/*`.split("\n").map { |f| File.basename(f) }
16
+ s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
17
+ s.require_paths = ['lib', 'vendor']
18
+
19
+ s.add_runtime_dependency 'opal'#, '~> 0.7.0'
20
+ s.add_runtime_dependency 'opal-activesupport'
21
+ s.add_development_dependency 'react-source', '~> 0.12'
22
+ s.add_development_dependency 'opal-rspec'
23
+ s.add_development_dependency 'sinatra'
24
+ s.add_development_dependency 'opal-jquery'
25
+ end
@@ -0,0 +1,107 @@
1
+ require "spec_helper"
2
+ require "react/callbacks"
3
+
4
+ describe React::Callbacks do
5
+ it "should be able to define callback" do
6
+ stub_const 'Foo', Class.new
7
+ Foo.class_eval do
8
+ include React::Callbacks
9
+ define_callback :before_dinner
10
+
11
+ before_dinner :wash_hand
12
+
13
+ def wash_hand
14
+
15
+ end
16
+ end
17
+
18
+ expect_any_instance_of(Foo).to receive(:wash_hand)
19
+ Foo.new.run_callback(:before_dinner)
20
+ end
21
+
22
+ it "should be able to define multiple callbacks" do
23
+ stub_const 'Foo', Class.new
24
+ Foo.class_eval do
25
+ include React::Callbacks
26
+ define_callback :before_dinner
27
+
28
+ before_dinner :wash_hand, :turn_of_laptop
29
+
30
+ def wash_hand;end
31
+
32
+ def turn_of_laptop;end
33
+ end
34
+
35
+ expect_any_instance_of(Foo).to receive(:wash_hand)
36
+ expect_any_instance_of(Foo).to receive(:turn_of_laptop)
37
+ Foo.new.run_callback(:before_dinner)
38
+ end
39
+
40
+ it "should be able to define block callback" do
41
+ stub_const 'Foo', Class.new
42
+ Foo.class_eval do
43
+ include React::Callbacks
44
+ attr_accessor :a
45
+ attr_accessor :b
46
+
47
+ define_callback :before_dinner
48
+
49
+ before_dinner do
50
+ self.a = 10
51
+ end
52
+ before_dinner do
53
+ self.b = 20
54
+ end
55
+ end
56
+
57
+ foo = Foo.new
58
+ foo.run_callback(:before_dinner)
59
+ expect(foo.a).to eq(10)
60
+ expect(foo.b).to eq(20)
61
+ end
62
+
63
+ it "should be able to define multiple callback group" do
64
+ stub_const 'Foo', Class.new
65
+ Foo.class_eval do
66
+ include React::Callbacks
67
+ define_callback :before_dinner
68
+ define_callback :after_dinner
69
+ attr_accessor :a
70
+
71
+ before_dinner do
72
+ self.a = 10
73
+ end
74
+ end
75
+
76
+ foo = Foo.new
77
+ foo.run_callback(:before_dinner)
78
+ foo.run_callback(:after_dinner)
79
+
80
+ expect(foo.a).to eq(10)
81
+ end
82
+
83
+ it "should be able to receive args as callback" do
84
+ stub_const 'Foo', Class.new
85
+ Foo.class_eval do
86
+ include React::Callbacks
87
+ define_callback :before_dinner
88
+ define_callback :after_dinner
89
+
90
+ attr_accessor :lorem
91
+
92
+ before_dinner do |a, b|
93
+ self.lorem = "#{a}-#{b}"
94
+ end
95
+
96
+ after_dinner :eat_ice_cream
97
+ def eat_ice_cream(a,b,c); end
98
+ end
99
+
100
+ expect_any_instance_of(Foo).to receive(:eat_ice_cream).with(4,5,6)
101
+
102
+ foo = Foo.new
103
+ foo.run_callback(:before_dinner, 1, 2)
104
+ foo.run_callback(:after_dinner, 4, 5, 6)
105
+ expect(foo.lorem).to eq('1-2')
106
+ end
107
+ end
@@ -0,0 +1,597 @@
1
+ require "spec_helper"
2
+
3
+ describe React::Component do
4
+ after(:each) do
5
+ React::API.clear_component_class_cache
6
+ end
7
+
8
+ it "should define component spec methods" do
9
+ stub_const 'Foo', Class.new
10
+ Foo.class_eval do
11
+ include React::Component
12
+ def render
13
+ React.create_element("div")
14
+ end
15
+ end
16
+
17
+ # Class Methods
18
+ expect(Foo).to respond_to("initial_state")
19
+ expect(Foo).to respond_to("default_props")
20
+ expect(Foo).to respond_to("prop_types")
21
+
22
+ # Instance method
23
+ expect(Foo.new).to respond_to("component_will_mount")
24
+ expect(Foo.new).to respond_to("component_did_mount")
25
+ expect(Foo.new).to respond_to("component_will_receive_props")
26
+ expect(Foo.new).to respond_to("should_component_update?")
27
+ expect(Foo.new).to respond_to("component_will_update")
28
+ expect(Foo.new).to respond_to("component_did_update")
29
+ expect(Foo.new).to respond_to("component_will_unmount")
30
+ end
31
+
32
+ describe "Life Cycle" do
33
+ before(:each) do
34
+ stub_const 'Foo', Class.new
35
+ Foo.class_eval do
36
+ include React::Component
37
+ def render
38
+ React.create_element("div") { "lorem" }
39
+ end
40
+ end
41
+ end
42
+
43
+ it "should invoke `before_mount` registered methods when `componentWillMount()`" do
44
+ Foo.class_eval do
45
+ before_mount :bar, :bar2
46
+ def bar; end
47
+ def bar2; end
48
+ end
49
+
50
+ expect_any_instance_of(Foo).to receive(:bar)
51
+ expect_any_instance_of(Foo).to receive(:bar2)
52
+
53
+ renderToDocument(Foo)
54
+ end
55
+
56
+ it "should invoke `after_mount` registered methods when `componentDidMount()`" do
57
+ Foo.class_eval do
58
+ after_mount :bar3, :bar4
59
+ def bar3; end
60
+ def bar4; end
61
+ end
62
+
63
+ expect_any_instance_of(Foo).to receive(:bar3)
64
+ expect_any_instance_of(Foo).to receive(:bar4)
65
+
66
+ renderToDocument(Foo)
67
+ end
68
+
69
+ it "should allow multiple class declared life cycle hooker" do
70
+ stub_const 'FooBar', Class.new
71
+ Foo.class_eval do
72
+ before_mount :bar
73
+ def bar; end
74
+ end
75
+
76
+ FooBar.class_eval do
77
+ include React::Component
78
+ after_mount :bar2
79
+ def bar2; end
80
+ def render
81
+ React.create_element("div") { "lorem" }
82
+ end
83
+ end
84
+
85
+ expect_any_instance_of(Foo).to receive(:bar)
86
+
87
+ renderToDocument(Foo)
88
+ end
89
+
90
+ it "should allow block for life cycle callback" do
91
+ Foo.class_eval do
92
+ define_state(:foo)
93
+
94
+ before_mount do
95
+ self.foo = "bar"
96
+ end
97
+ end
98
+
99
+ element = renderToDocument(Foo)
100
+ expect(element.state.foo).to be("bar")
101
+ end
102
+ end
103
+
104
+ describe "State setter & getter" do
105
+ before(:each) do
106
+ stub_const 'Foo', Class.new
107
+ Foo.class_eval do
108
+ include React::Component
109
+ def render
110
+ React.create_element("div") { "lorem" }
111
+ end
112
+ end
113
+ end
114
+
115
+ it "should define setter using `define_state`" do
116
+ Foo.class_eval do
117
+ define_state :foo
118
+ before_mount :set_up
119
+ def set_up
120
+ self.foo = "bar"
121
+ end
122
+ end
123
+
124
+ element = renderToDocument(Foo)
125
+ expect(element.state.foo).to be("bar")
126
+ end
127
+
128
+ it "should define init state by passing a block to `define_state`" do
129
+ Foo.class_eval do
130
+ define_state(:foo) { 10 }
131
+ end
132
+
133
+ element = renderToDocument(Foo)
134
+ expect(element.state.foo).to be(10)
135
+ end
136
+
137
+ it "should define getter using `define_state`" do
138
+ Foo.class_eval do
139
+ define_state(:foo) { 10 }
140
+ before_mount :bump
141
+ def bump
142
+ self.foo = self.foo + 20
143
+ end
144
+ end
145
+
146
+ element = renderToDocument(Foo)
147
+ expect(element.state.foo).to be(30)
148
+ end
149
+
150
+ it "should define multiple state accessor by passing symols array to `define_state`" do
151
+ Foo.class_eval do
152
+ define_state :foo, :foo2
153
+ before_mount :set_up
154
+ def set_up
155
+ self.foo = 10
156
+ self.foo2 = 20
157
+ end
158
+ end
159
+
160
+ element = renderToDocument(Foo)
161
+ expect(element.state.foo).to be(10)
162
+ expect(element.state.foo2).to be(20)
163
+ end
164
+
165
+ it "should invoke `define_state` multiple times to define states" do
166
+ Foo.class_eval do
167
+ define_state(:foo) { 30 }
168
+ define_state(:foo2) { 40 }
169
+ end
170
+
171
+ element = renderToDocument(Foo)
172
+ expect(element.state.foo).to be(30)
173
+ expect(element.state.foo2).to be(40)
174
+ end
175
+
176
+ it "should raise error if multiple states and block given at the same time" do
177
+ expect {
178
+ Foo.class_eval do
179
+ define_state(:foo, :foo2) { 30 }
180
+ end
181
+ }.to raise_error
182
+ end
183
+
184
+ it "should get state in render method" do
185
+ Foo.class_eval do
186
+ define_state(:foo) { 10 }
187
+ def render
188
+ React.create_element("div") { self.foo }
189
+ end
190
+ end
191
+
192
+ element = renderToDocument(Foo)
193
+ expect(element.getDOMNode.textContent).to eq("10")
194
+ end
195
+
196
+ it "should support original `setState` as `set_state` method" do
197
+ Foo.class_eval do
198
+ before_mount do
199
+ self.set_state(foo: "bar")
200
+ end
201
+ end
202
+
203
+ element = renderToDocument(Foo)
204
+ expect(element.state.foo).to be("bar")
205
+ end
206
+
207
+ it "should support original `replaceState` as `set_state!` method" do
208
+ Foo.class_eval do
209
+ before_mount do
210
+ self.set_state(foo: "bar")
211
+ self.set_state!(bar: "lorem")
212
+ end
213
+ end
214
+
215
+ element = renderToDocument(Foo)
216
+ expect(element.state.foo).to be_nil
217
+ expect(element.state.bar).to eq("lorem")
218
+ end
219
+
220
+ it "should support originl `state` method" do
221
+ Foo.class_eval do
222
+ before_mount do
223
+ self.set_state(foo: "bar")
224
+ end
225
+
226
+ def render
227
+ div { self.state[:foo] }
228
+ end
229
+ end
230
+
231
+ expect(React.render_to_static_markup(React.create_element(Foo))).to eq("<div>bar</div>")
232
+ end
233
+
234
+ it "should transform state getter to Ruby object" do
235
+ Foo.class_eval do
236
+ define_state :foo
237
+
238
+ before_mount do
239
+ self.foo = [{a: 10}]
240
+ end
241
+
242
+ def render
243
+ div { self.foo[0][:a] }
244
+ end
245
+ end
246
+
247
+ expect(React.render_to_static_markup(React.create_element(Foo))).to eq("<div>10</div>")
248
+ end
249
+ end
250
+
251
+ describe "Props" do
252
+ describe "this.props could be accessed through `params` method" do
253
+ before do
254
+ stub_const 'Foo', Class.new
255
+ Foo.class_eval do
256
+ include React::Component
257
+ end
258
+ end
259
+
260
+ it "should read from parent passed properties through `params`" do
261
+ Foo.class_eval do
262
+ def render
263
+ React.create_element("div") { params[:prop] }
264
+ end
265
+ end
266
+
267
+ element = renderToDocument(Foo, prop: "foobar")
268
+ expect(element.getDOMNode.textContent).to eq("foobar")
269
+ end
270
+
271
+ it "should access nested params as orignal Ruby object" do
272
+ Foo.class_eval do
273
+ def render
274
+ React.create_element("div") { params[:prop][0][:foo] }
275
+ end
276
+ end
277
+
278
+ element = renderToDocument(Foo, prop: [{foo: 10}])
279
+ expect(element.getDOMNode.textContent).to eq("10")
280
+ end
281
+ end
282
+
283
+ describe "Props Updating" do
284
+ before do
285
+ stub_const 'Foo', Class.new
286
+ Foo.class_eval do
287
+ include React::Component
288
+ end
289
+ end
290
+
291
+ it "should support original `setProps` as method `set_props`" do
292
+ Foo.class_eval do
293
+ def render
294
+ React.create_element("div") { params[:foo] }
295
+ end
296
+ end
297
+
298
+ element = renderToDocument(Foo, {foo: 10})
299
+ element.set_props(foo: 20)
300
+ expect(element.dom_node.innerHTML).to eq('20')
301
+ end
302
+
303
+ it "should support original `replaceProps` as method `set_props!`" do
304
+ Foo.class_eval do
305
+ def render
306
+ React.create_element("div") { params[:foo] ? "exist" : "null" }
307
+ end
308
+ end
309
+
310
+ element = renderToDocument(Foo, {foo: 10})
311
+ element.set_props!(bar: 20)
312
+ expect(element.dom_node.innerHTML).to eq('null')
313
+ end
314
+ end
315
+
316
+ describe "Prop validation" do
317
+ before do
318
+ stub_const 'Foo', Class.new
319
+ Foo.class_eval do
320
+ include React::Component
321
+ end
322
+ end
323
+
324
+ it "should specify validation rules using `params` class method" do
325
+ Foo.class_eval do
326
+ params do
327
+ requires :foo, type: String
328
+ optional :bar
329
+ end
330
+ end
331
+
332
+ expect(Foo.prop_types).to have_key(:_componentValidator)
333
+ end
334
+
335
+ it "should log error in warning if validation failed" do
336
+ stub_const 'Lorem', Class.new
337
+ Foo.class_eval do
338
+ params do
339
+ requires :foo
340
+ requires :lorem, type: Lorem
341
+ optional :bar, type: String
342
+ end
343
+
344
+ def render; div; end
345
+ end
346
+
347
+ %x{
348
+ var log = [];
349
+ var org_console = window.console;
350
+ window.console = {warn: function(str){log.push(str)}}
351
+ }
352
+ renderToDocument(Foo, bar: 10, lorem: Lorem.new)
353
+ `window.console = org_console;`
354
+ expect(`log`).to eq(["Warning: Failed propType: In component `Foo`\nRequired prop `foo` was not specified\nProvided prop `bar` was not the specified type `String`"])
355
+ end
356
+
357
+ it "should not log anything if validation pass" do
358
+ stub_const 'Lorem', Class.new
359
+ Foo.class_eval do
360
+ params do
361
+ requires :foo
362
+ requires :lorem, type: Lorem
363
+ optional :bar, type: String
364
+ end
365
+
366
+ def render; div; end
367
+ end
368
+
369
+ %x{
370
+ var log = [];
371
+ var org_console = window.console;
372
+ window.console = {warn: function(str){log.push(str)}}
373
+ }
374
+ renderToDocument(Foo, foo: 10, bar: "10", lorem: Lorem.new)
375
+ `window.console = org_console;`
376
+ expect(`log`).to eq([])
377
+ end
378
+ end
379
+
380
+ describe "Default props" do
381
+ it "should set default props using validation helper" do
382
+ stub_const 'Foo', Class.new
383
+ Foo.class_eval do
384
+ include React::Component
385
+ params do
386
+ optional :foo, default: "foo"
387
+ optional :bar, default: "bar"
388
+ end
389
+
390
+ def render
391
+ div { params[:foo] + "-" + params[:bar]}
392
+ end
393
+ end
394
+
395
+ expect(React.render_to_static_markup(React.create_element(Foo, foo: "lorem"))).to eq("<div>lorem-bar</div>")
396
+ expect(React.render_to_static_markup(React.create_element(Foo))).to eq("<div>foo-bar</div>")
397
+ end
398
+ end
399
+ end
400
+
401
+ describe "Event handling" do
402
+ before do
403
+ stub_const 'Foo', Class.new
404
+ Foo.class_eval do
405
+ include React::Component
406
+ end
407
+ end
408
+
409
+ it "should work in render method" do
410
+ Foo.class_eval do
411
+ define_state(:clicked) { false }
412
+
413
+ def render
414
+ React.create_element("div").on(:click) do
415
+ self.clicked = true
416
+ end
417
+ end
418
+ end
419
+
420
+ element = React.create_element(Foo)
421
+ instance = renderElementToDocument(element)
422
+ simulateEvent(:click, instance)
423
+ expect(instance.state.clicked).to eq(true)
424
+ end
425
+
426
+ it "should invoke handler on `this.props` using emit" do
427
+ Foo.class_eval do
428
+ after_mount :setup
429
+
430
+ def setup
431
+ self.emit(:foo_submit, "bar")
432
+ end
433
+
434
+ def render
435
+ React.create_element("div")
436
+ end
437
+ end
438
+
439
+ expect { |b|
440
+ element = React.create_element(Foo).on(:foo_submit, &b)
441
+ renderElementToDocument(element)
442
+ }.to yield_with_args("bar")
443
+ end
444
+
445
+ it "should invoke handler with multiple params using emit" do
446
+ Foo.class_eval do
447
+ after_mount :setup
448
+
449
+ def setup
450
+ self.emit(:foo_invoked, [1,2,3], "bar")
451
+ end
452
+
453
+ def render
454
+ React.create_element("div")
455
+ end
456
+ end
457
+
458
+ expect { |b|
459
+ element = React.create_element(Foo).on(:foo_invoked, &b)
460
+ renderElementToDocument(element)
461
+ }.to yield_with_args([1,2,3], "bar")
462
+ end
463
+ end
464
+
465
+ describe "Refs" do
466
+ before do
467
+ stub_const 'Foo', Class.new
468
+ Foo.class_eval do
469
+ include React::Component
470
+ end
471
+ end
472
+
473
+ it "should correctly assign refs" do
474
+ Foo.class_eval do
475
+ def render
476
+ React.create_element("input", type: :text, ref: :field)
477
+ end
478
+ end
479
+
480
+ element = renderToDocument(Foo)
481
+ expect(element.refs.field).not_to be_nil
482
+ end
483
+
484
+ it "should access refs through `refs` method" do
485
+ Foo.class_eval do
486
+ def render
487
+ React.create_element("input", type: :text, ref: :field).on(:click) do
488
+ refs[:field].value = "some_stuff"
489
+ end
490
+ end
491
+ end
492
+
493
+ element = renderToDocument(Foo)
494
+ simulateEvent(:click, element)
495
+
496
+ expect(element.refs.field.value).to eq("some_stuff")
497
+ end
498
+ end
499
+
500
+ describe "Render" do
501
+ it "should support element building helpers" do
502
+ stub_const 'Foo', Class.new
503
+ Foo.class_eval do
504
+ include React::Component
505
+
506
+ def render
507
+ div do
508
+ span { params[:foo] }
509
+ end
510
+ end
511
+ end
512
+
513
+ stub_const 'Bar', Class.new
514
+ Bar.class_eval do
515
+ include React::Component
516
+ def render
517
+ div do
518
+ present Foo, foo: "astring"
519
+ end
520
+ end
521
+ end
522
+
523
+ expect(React.render_to_static_markup(React.create_element(Bar))).to eq("<div><div><span>astring</span></div></div>")
524
+ end
525
+
526
+ it "should build single node in top-level render without providing a block" do
527
+ stub_const 'Foo', Class.new
528
+ Foo.class_eval do
529
+ include React::Component
530
+
531
+ def render
532
+ div
533
+ end
534
+ end
535
+
536
+ element = React.create_element(Foo)
537
+ expect(React.render_to_static_markup(element)).to eq("<div></div>")
538
+ end
539
+
540
+ it "should redefine `p` to make method missing work" do
541
+ stub_const 'Foo', Class.new
542
+ Foo.class_eval do
543
+ include React::Component
544
+
545
+ def render
546
+ p(class_name: "foo") do
547
+ p
548
+ div { "lorem ipsum" }
549
+ p(id: "10")
550
+ end
551
+ end
552
+ end
553
+
554
+ element = React.create_element(Foo)
555
+ expect(React.render_to_static_markup(element)).to eq("<p class=\"foo\"><p></p><div>lorem ipsum</div><p id=\"10\"></p></p>")
556
+ end
557
+
558
+ it "should only override `p` in render context" do
559
+ stub_const 'Foo', Class.new
560
+ Foo.class_eval do
561
+ include React::Component
562
+
563
+ before_mount do
564
+ p "first"
565
+ end
566
+
567
+ after_mount do
568
+ p "second"
569
+ end
570
+
571
+ def render
572
+ div
573
+ end
574
+ end
575
+
576
+ expect(Kernel).to receive(:p).with("first")
577
+ expect(Kernel).to receive(:p).with("second")
578
+ renderToDocument(Foo)
579
+ end
580
+ end
581
+
582
+ describe "isMounted()" do
583
+ it "should return true if after mounted" do
584
+ stub_const 'Foo', Class.new
585
+ Foo.class_eval do
586
+ include React::Component
587
+
588
+ def render
589
+ React.create_element("div")
590
+ end
591
+ end
592
+
593
+ component = renderToDocument(Foo)
594
+ expect(component.mounted?).to eq(true)
595
+ end
596
+ end
597
+ end