reactive-ruby 0.7.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 (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