hyper-react 0.10.0

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/.codeclimate.yml +27 -0
  3. data/.gitignore +36 -0
  4. data/.rubocop.yml +1159 -0
  5. data/.travis.yml +29 -0
  6. data/Appraisals +20 -0
  7. data/CHANGELOG.md +93 -0
  8. data/Gemfile +6 -0
  9. data/LICENSE +19 -0
  10. data/README.md +121 -0
  11. data/Rakefile +33 -0
  12. data/UPGRADING.md +24 -0
  13. data/component-name-lookup.md +145 -0
  14. data/config.ru +25 -0
  15. data/gemfiles/opal_0.8_react_13.gemfile +13 -0
  16. data/gemfiles/opal_0.8_react_14.gemfile +13 -0
  17. data/gemfiles/opal_0.8_react_15.gemfile +13 -0
  18. data/gemfiles/opal_0.9_react_13.gemfile +13 -0
  19. data/gemfiles/opal_0.9_react_14.gemfile +13 -0
  20. data/gemfiles/opal_0.9_react_15.gemfile +13 -0
  21. data/hyper-react.gemspec +43 -0
  22. data/lib/generators/reactive_ruby/test_app/templates/assets/javascripts/components.rb +4 -0
  23. data/lib/generators/reactive_ruby/test_app/templates/assets/javascripts/test_application.rb +2 -0
  24. data/lib/generators/reactive_ruby/test_app/templates/boot.rb.erb +6 -0
  25. data/lib/generators/reactive_ruby/test_app/templates/script/rails +5 -0
  26. data/lib/generators/reactive_ruby/test_app/templates/test_application.rb.erb +13 -0
  27. data/lib/generators/reactive_ruby/test_app/templates/views/components/hello_world.rb +11 -0
  28. data/lib/generators/reactive_ruby/test_app/templates/views/components/todo.rb +14 -0
  29. data/lib/generators/reactive_ruby/test_app/templates/views/layouts/test_layout.html.erb +0 -0
  30. data/lib/generators/reactive_ruby/test_app/test_app_generator.rb +109 -0
  31. data/lib/hyper-react.rb +52 -0
  32. data/lib/rails-helpers/top_level_rails_component.rb +54 -0
  33. data/lib/react-sources/react-server.js +2 -0
  34. data/lib/react/api.rb +162 -0
  35. data/lib/react/callbacks.rb +42 -0
  36. data/lib/react/children.rb +30 -0
  37. data/lib/react/component.rb +139 -0
  38. data/lib/react/component/api.rb +50 -0
  39. data/lib/react/component/base.rb +9 -0
  40. data/lib/react/component/class_methods.rb +214 -0
  41. data/lib/react/component/dsl_instance_methods.rb +27 -0
  42. data/lib/react/component/params.rb +6 -0
  43. data/lib/react/component/props_wrapper.rb +83 -0
  44. data/lib/react/component/should_component_update.rb +98 -0
  45. data/lib/react/component/tags.rb +144 -0
  46. data/lib/react/element.rb +168 -0
  47. data/lib/react/event.rb +76 -0
  48. data/lib/react/ext/hash.rb +9 -0
  49. data/lib/react/ext/string.rb +8 -0
  50. data/lib/react/hash.rb +13 -0
  51. data/lib/react/native_library.rb +92 -0
  52. data/lib/react/object.rb +15 -0
  53. data/lib/react/observable.rb +29 -0
  54. data/lib/react/react-source.rb +9 -0
  55. data/lib/react/rendering_context.rb +142 -0
  56. data/lib/react/state.rb +190 -0
  57. data/lib/react/test.rb +16 -0
  58. data/lib/react/test/dsl.rb +17 -0
  59. data/lib/react/test/matchers/render_html_matcher.rb +49 -0
  60. data/lib/react/test/rspec.rb +15 -0
  61. data/lib/react/test/session.rb +46 -0
  62. data/lib/react/top_level.rb +132 -0
  63. data/lib/react/validator.rb +136 -0
  64. data/lib/reactive-ruby/component_loader.rb +49 -0
  65. data/lib/reactive-ruby/isomorphic_helpers.rb +197 -0
  66. data/lib/reactive-ruby/rails.rb +7 -0
  67. data/lib/reactive-ruby/rails/component_mount.rb +46 -0
  68. data/lib/reactive-ruby/rails/controller_helper.rb +15 -0
  69. data/lib/reactive-ruby/rails/railtie.rb +14 -0
  70. data/lib/reactive-ruby/serializers.rb +15 -0
  71. data/lib/reactive-ruby/server_rendering/contextual_renderer.rb +42 -0
  72. data/lib/reactive-ruby/version.rb +3 -0
  73. data/lib/reactrb/auto-import.rb +32 -0
  74. data/lib/reactrb/deep-compare.rb +24 -0
  75. data/lib/reactrb/new-event-name-convention.rb +11 -0
  76. data/lib/sources/react-latest.js +21169 -0
  77. data/lib/sources/react-v13.js +21645 -0
  78. data/lib/sources/react-v14.js +20821 -0
  79. data/lib/sources/react-v15.js +21170 -0
  80. data/logo1.png +0 -0
  81. data/logo2.png +0 -0
  82. data/logo3.png +0 -0
  83. data/path_release_steps.md +9 -0
  84. data/spec/controller_helper_spec.rb +34 -0
  85. data/spec/index.html.erb +10 -0
  86. data/spec/react/callbacks_spec.rb +106 -0
  87. data/spec/react/children_spec.rb +76 -0
  88. data/spec/react/component/base_spec.rb +32 -0
  89. data/spec/react/component_spec.rb +872 -0
  90. data/spec/react/dsl_spec.rb +296 -0
  91. data/spec/react/element_spec.rb +136 -0
  92. data/spec/react/event_spec.rb +24 -0
  93. data/spec/react/native_library_spec.rb +344 -0
  94. data/spec/react/observable_spec.rb +7 -0
  95. data/spec/react/opal_jquery_extensions_spec.rb +66 -0
  96. data/spec/react/param_declaration_spec.rb +258 -0
  97. data/spec/react/react_spec.rb +209 -0
  98. data/spec/react/state_spec.rb +55 -0
  99. data/spec/react/test/dsl_spec.rb +43 -0
  100. data/spec/react/test/matchers/render_html_matcher_spec.rb +83 -0
  101. data/spec/react/test/rspec_spec.rb +62 -0
  102. data/spec/react/test/session_spec.rb +100 -0
  103. data/spec/react/test/utils_spec.rb +45 -0
  104. data/spec/react/top_level_component_spec.rb +96 -0
  105. data/spec/react/tutorial/tutorial_spec.rb +36 -0
  106. data/spec/react/validator_spec.rb +124 -0
  107. data/spec/reactive-ruby/component_loader_spec.rb +71 -0
  108. data/spec/reactive-ruby/isomorphic_helpers_spec.rb +155 -0
  109. data/spec/reactive-ruby/rails/asset_pipeline_spec.rb +10 -0
  110. data/spec/reactive-ruby/rails/component_mount_spec.rb +66 -0
  111. data/spec/reactive-ruby/server_rendering/contextual_renderer_spec.rb +35 -0
  112. data/spec/spec_helper.rb +115 -0
  113. data/spec/support/react/spec_helpers.rb +64 -0
  114. data/spec/vendor/es5-shim.min.js +6 -0
  115. data/spec/vendor/jquery-2.2.4.min.js +4 -0
  116. metadata +387 -0
Binary file
Binary file
Binary file
@@ -0,0 +1,9 @@
1
+
2
+ For example assuming you are releasing fix to 0.8.18
3
+
4
+ 1. Checkout 0-8-stable
5
+ 2. Update tests, fix the bug and commit the changes.
6
+ 3. Build & Release to RubyGems (Remember the version in version.rb should already be 0.8.19)
7
+ 4. Create a tag 'v0.8.19' pointing to that commit.
8
+ 5. Bump the version in 0-8-stable to 0.8.20 so it will be ready for the next patch level release.
9
+ 6. Commit the version bump, and do a `git push --tags` so the new tag goes up
@@ -0,0 +1,34 @@
1
+ require 'spec_helper'
2
+
3
+ if ruby?
4
+ class TestController < ActionController::Base; end
5
+
6
+ RSpec.describe TestController, type: :controller do
7
+ render_views
8
+
9
+ describe '#render_component' do
10
+ controller do
11
+
12
+ layout "test_layout"
13
+
14
+ def index
15
+ render_component
16
+ end
17
+
18
+ def new
19
+ render_component "Index", {}, layout: :explicit_layout
20
+ end
21
+ end
22
+
23
+ it 'renders with the default layout' do
24
+ get :index, no_prerender: true
25
+ expect(response).to render_template(layout: :test_layout)
26
+ end
27
+
28
+ it "renders with a specified layout" do
29
+ get :new, no_prerender: true
30
+ expect(response).to render_template(layout: :explicit_layout)
31
+ end
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,10 @@
1
+ <!DOCTYPE html>
2
+ <html>
3
+ <head>
4
+ </head>
5
+ <body>
6
+ <%= javascript_include_tag 'vendor/es5-shim.min' %>
7
+ <%= javascript_include_tag 'vendor/jquery-2.2.4.min' %>
8
+ <%= javascript_include_tag @server.main %>
9
+ </body>
10
+ </html>
@@ -0,0 +1,106 @@
1
+ require 'spec_helper'
2
+
3
+ if opal?
4
+ describe React::Callbacks do
5
+ it 'defines callback' do
6
+ stub_const 'Foo', Class.new
7
+ Foo.class_eval do
8
+ include React::Callbacks
9
+ define_callback :before_dinner
10
+ before_dinner :wash_hand
11
+
12
+ def wash_hand
13
+ end
14
+ end
15
+
16
+ expect_any_instance_of(Foo).to receive(:wash_hand)
17
+ Foo.new.run_callback(:before_dinner)
18
+ end
19
+
20
+ it 'defines multiple callbacks' do
21
+ stub_const 'Foo', Class.new
22
+ Foo.class_eval do
23
+ include React::Callbacks
24
+ define_callback :before_dinner
25
+
26
+ before_dinner :wash_hand, :turn_of_laptop
27
+
28
+ def wash_hand;end
29
+
30
+ def turn_of_laptop;end
31
+ end
32
+
33
+ expect_any_instance_of(Foo).to receive(:wash_hand)
34
+ expect_any_instance_of(Foo).to receive(:turn_of_laptop)
35
+ Foo.new.run_callback(:before_dinner)
36
+ end
37
+
38
+ it 'defines block callback' do
39
+ stub_const 'Foo', Class.new
40
+ Foo.class_eval do
41
+ include React::Callbacks
42
+ attr_accessor :a
43
+ attr_accessor :b
44
+
45
+ define_callback :before_dinner
46
+
47
+ before_dinner do
48
+ self.a = 10
49
+ end
50
+ before_dinner do
51
+ self.b = 20
52
+ end
53
+ end
54
+
55
+ foo = Foo.new
56
+ foo.run_callback(:before_dinner)
57
+ expect(foo.a).to eq(10)
58
+ expect(foo.b).to eq(20)
59
+ end
60
+
61
+ it 'defines multiple callback group' do
62
+ stub_const 'Foo', Class.new
63
+ Foo.class_eval do
64
+ include React::Callbacks
65
+ define_callback :before_dinner
66
+ define_callback :after_dinner
67
+ attr_accessor :a
68
+
69
+ before_dinner do
70
+ self.a = 10
71
+ end
72
+ end
73
+
74
+ foo = Foo.new
75
+ foo.run_callback(:before_dinner)
76
+ foo.run_callback(:after_dinner)
77
+
78
+ expect(foo.a).to eq(10)
79
+ end
80
+
81
+ it 'receives args as callback' do
82
+ stub_const 'Foo', Class.new
83
+ Foo.class_eval do
84
+ include React::Callbacks
85
+ define_callback :before_dinner
86
+ define_callback :after_dinner
87
+
88
+ attr_accessor :lorem
89
+
90
+ before_dinner do |a, b|
91
+ self.lorem = "#{a}-#{b}"
92
+ end
93
+
94
+ after_dinner :eat_ice_cream
95
+ def eat_ice_cream(a,b,c); end
96
+ end
97
+
98
+ expect_any_instance_of(Foo).to receive(:eat_ice_cream).with(4,5,6)
99
+
100
+ foo = Foo.new
101
+ foo.run_callback(:before_dinner, 1, 2)
102
+ foo.run_callback(:after_dinner, 4, 5, 6)
103
+ expect(foo.lorem).to eq('1-2')
104
+ end
105
+ end
106
+ end
@@ -0,0 +1,76 @@
1
+ require 'spec_helper'
2
+
3
+ if opal?
4
+ describe React::Children do
5
+ let(:component) {
6
+ Class.new do
7
+ include React::Component
8
+ def render
9
+ div { 'lorem' }
10
+ end
11
+ end
12
+ }
13
+ let(:childs) { [React.create_element('a'), React.create_element('li')] }
14
+ let(:element) { React.create_element(component) { childs } }
15
+ let(:children) { described_class.new(`#{element.to_n}.props.children`) }
16
+
17
+ before(:each) do
18
+ renderElementToDocument(element)
19
+ end
20
+
21
+ it 'is enumerable' do
22
+ nodes = children.map { |elem| elem.element_type }
23
+ expect(nodes).to eq(['a', 'li'])
24
+ end
25
+
26
+ it 'returns an Enumerator when not providing a block' do
27
+ nodes = children.each
28
+ expect(nodes).to be_a(Enumerator)
29
+ expect(nodes.size).to eq(2)
30
+ end
31
+
32
+ describe '#each' do
33
+ it 'returns an array of elements' do
34
+ nodes = children.each { |elem| elem.element_type }
35
+ expect(nodes).to be_a(Array)
36
+ expect(nodes.map(&:class)).to eq(childs.map(&:class))
37
+ end
38
+ end
39
+
40
+ describe '#length' do
41
+ it 'returns the number of child elements' do
42
+ expect(children.length).to eq(2)
43
+ end
44
+ end
45
+
46
+ describe 'with single child element' do
47
+ let(:childs) { [React.create_element('a')] }
48
+
49
+ it 'is enumerable containing single element' do
50
+ nodes = children.map { |elem| elem.element_type }
51
+ expect(nodes).to eq(['a'])
52
+ end
53
+
54
+ describe '#length' do
55
+ it 'returns the number of child elements' do
56
+ expect(children.length).to eq(1)
57
+ end
58
+ end
59
+ end
60
+
61
+ describe 'with no child element' do
62
+ let(:element) { React.create_element(component) }
63
+
64
+ it 'is enumerable containing no elements' do
65
+ nodes = children.map { |elem| elem.element_type }
66
+ expect(nodes).to eq([])
67
+ end
68
+
69
+ describe '#length' do
70
+ it 'returns the number of child elements' do
71
+ expect(children.length).to eq(0)
72
+ end
73
+ end
74
+ end
75
+ end
76
+ end
@@ -0,0 +1,32 @@
1
+ require 'spec_helper'
2
+
3
+ if opal?
4
+ RSpec.describe React::Component::Base, type: :component do
5
+ after(:each) do
6
+ React::API.clear_component_class_cache
7
+ end
8
+
9
+ it 'can be inherited to create a component class' do
10
+ stub_const 'Foo', Class.new(React::Component::Base)
11
+ Foo.class_eval do
12
+ before_mount do
13
+ @instance_data = ["working"]
14
+ end
15
+ def render
16
+ @instance_data.first
17
+ end
18
+ end
19
+ stub_const 'Bar', Class.new(Foo)
20
+ Bar.class_eval do
21
+ before_mount do
22
+ @instance_data << "well"
23
+ end
24
+ def render
25
+ @instance_data.join(" ")
26
+ end
27
+ end
28
+ expect(Foo).to render("<span>working</span>")
29
+ expect(Bar).to render("<span>working well</span>")
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,872 @@
1
+ require 'spec_helper'
2
+
3
+ if opal?
4
+ describe React::Component, type: :component do
5
+ after(:each) do
6
+ React::API.clear_component_class_cache
7
+ end
8
+
9
+ it 'defines component spec methods' do
10
+ stub_const 'Foo', Class.new
11
+ Foo.class_eval do
12
+ include React::Component
13
+ def render
14
+ React.create_element('div')
15
+ end
16
+ end
17
+
18
+ # Class Methods
19
+ expect(Foo).to respond_to('initial_state')
20
+ expect(Foo).to respond_to('default_props')
21
+ expect(Foo).to respond_to('prop_types')
22
+
23
+ # Instance method
24
+ expect(Foo.new).to respond_to('component_will_mount')
25
+ expect(Foo.new).to respond_to('component_did_mount')
26
+ expect(Foo.new).to respond_to('component_will_receive_props')
27
+ expect(Foo.new).to respond_to('should_component_update?')
28
+ expect(Foo.new).to respond_to('component_will_update')
29
+ expect(Foo.new).to respond_to('component_did_update')
30
+ expect(Foo.new).to respond_to('component_will_unmount')
31
+ end
32
+
33
+ describe 'Life Cycle' do
34
+ before(:each) do
35
+ stub_const 'Foo', Class.new
36
+ Foo.class_eval do
37
+ include React::Component
38
+ def render
39
+ React.create_element('div') { 'lorem' }
40
+ end
41
+ end
42
+ end
43
+
44
+ it 'invokes `before_mount` registered methods when `componentWillMount()`' do
45
+ Foo.class_eval do
46
+ before_mount :bar, :bar2
47
+ def bar; end
48
+ def bar2; end
49
+ end
50
+
51
+ expect_any_instance_of(Foo).to receive(:bar)
52
+ expect_any_instance_of(Foo).to receive(:bar2)
53
+
54
+ renderToDocument(Foo)
55
+ end
56
+
57
+ it 'invokes `after_mount` registered methods when `componentDidMount()`' do
58
+ Foo.class_eval do
59
+ after_mount :bar3, :bar4
60
+ def bar3; end
61
+ def bar4; end
62
+ end
63
+
64
+ expect_any_instance_of(Foo).to receive(:bar3)
65
+ expect_any_instance_of(Foo).to receive(:bar4)
66
+
67
+ renderToDocument(Foo)
68
+ end
69
+
70
+ it 'allows multiple class declared life cycle hooker' do
71
+ stub_const 'FooBar', Class.new
72
+ Foo.class_eval do
73
+ before_mount :bar
74
+ def bar; end
75
+ end
76
+
77
+ FooBar.class_eval do
78
+ include React::Component
79
+ after_mount :bar2
80
+ def bar2; end
81
+ def render
82
+ React.create_element('div') { 'lorem' }
83
+ end
84
+ end
85
+
86
+ expect_any_instance_of(Foo).to receive(:bar)
87
+
88
+ renderToDocument(Foo)
89
+ end
90
+
91
+ it 'allows block for life cycle callback' do
92
+ Foo.class_eval do
93
+ export_state(:foo)
94
+
95
+ before_mount do
96
+ foo! 'bar'
97
+ end
98
+ end
99
+
100
+ element = renderToDocument(Foo)
101
+ expect(Foo.foo).to be('bar')
102
+ end
103
+ end
104
+
105
+ describe 'New style setter & getter' do
106
+ before(:each) do
107
+ stub_const 'Foo', Class.new
108
+ Foo.class_eval do
109
+ include React::Component
110
+ def render
111
+ div { state.foo }
112
+ end
113
+ end
114
+ end
115
+
116
+ it 'implicitly will create a state variable when first written' do
117
+ Foo.class_eval do
118
+ before_mount do
119
+ state.foo! 'bar'
120
+ end
121
+ end
122
+
123
+ expect(Foo).to render('<div>bar</div>')
124
+ end
125
+
126
+ it 'allows kernal method names like "format" to be used as state variable names' do
127
+ Foo.class_eval do
128
+ before_mount do
129
+ state.format! 'yes'
130
+ state.foo! state.format
131
+ end
132
+ end
133
+
134
+ expect(Foo).to render('<div>yes</div>')
135
+ end
136
+
137
+ it 'returns an observer with the bang method and no arguments' do
138
+ Foo.class_eval do
139
+ before_mount do
140
+ state.foo!(state.baz!.class.name)
141
+ end
142
+ end
143
+
144
+ expect(Foo).to render('<div>React::Observable</div>')
145
+ end
146
+
147
+ it 'returns the current value of a state when written' do
148
+ Foo.class_eval do
149
+ before_mount do
150
+ state.baz! 'bar'
151
+ state.foo!(state.baz!('pow'))
152
+ end
153
+ end
154
+
155
+ expect(Foo).to render('<div>bar</div>')
156
+ end
157
+
158
+ it 'can access an explicitly defined state`' do
159
+ Foo.class_eval do
160
+ define_state foo: :bar
161
+ end
162
+
163
+ expect(Foo).to render('<div>bar</div>')
164
+ end
165
+
166
+ end
167
+
168
+ describe 'State setter & getter' do
169
+ before(:each) do
170
+ stub_const 'Foo', Class.new
171
+ Foo.class_eval do
172
+ include React::Component
173
+ def render
174
+ React.create_element('div') { 'lorem' }
175
+ end
176
+ end
177
+ end
178
+
179
+ it 'defines setter using `define_state`' do
180
+ Foo.class_eval do
181
+ define_state :foo
182
+ before_mount :set_up
183
+ def set_up
184
+ self.foo = 'bar'
185
+ end
186
+ end
187
+
188
+ element = renderToDocument(Foo)
189
+ expect(element.state.foo).to be('bar')
190
+ end
191
+
192
+ it 'defines init state by passing a block to `define_state`' do
193
+ Foo.class_eval do
194
+ define_state(:foo) { 10 }
195
+ end
196
+
197
+ element = renderToDocument(Foo)
198
+ expect(element.state.foo).to be(10)
199
+ end
200
+
201
+ it 'defines getter using `define_state`' do
202
+ Foo.class_eval do
203
+ define_state(:foo) { 10 }
204
+ before_mount :bump
205
+ def bump
206
+ self.foo = self.foo + 20
207
+ end
208
+ end
209
+
210
+ element = renderToDocument(Foo)
211
+ expect(element.state.foo).to be(30)
212
+ end
213
+
214
+ it 'defines multiple state accessors by passing array to `define_state`' do
215
+ Foo.class_eval do
216
+ define_state :foo, :foo2
217
+ before_mount :set_up
218
+ def set_up
219
+ self.foo = 10
220
+ self.foo2 = 20
221
+ end
222
+ end
223
+
224
+ element = renderToDocument(Foo)
225
+ expect(element.state.foo).to be(10)
226
+ expect(element.state.foo2).to be(20)
227
+ end
228
+
229
+ it 'invokes `define_state` multiple times to define states' do
230
+ Foo.class_eval do
231
+ define_state(:foo) { 30 }
232
+ define_state(:foo2) { 40 }
233
+ end
234
+
235
+ element = renderToDocument(Foo)
236
+ expect(element.state.foo).to be(30)
237
+ expect(element.state.foo2).to be(40)
238
+ end
239
+
240
+ it 'can initialize multiple state variables with a block' do
241
+ Foo.class_eval do
242
+ define_state(:foo, :foo2) { 30 }
243
+ end
244
+ element = renderToDocument(Foo)
245
+ expect(element.state.foo).to be(30)
246
+ expect(element.state.foo2).to be(30)
247
+ end
248
+
249
+ it 'can mix multiple state variables with initializers and a block' do
250
+ Foo.class_eval do
251
+ define_state(:x, :y, foo: 1, bar: 2) {3}
252
+ end
253
+ element = renderToDocument(Foo)
254
+ expect(element.state.x).to be(3)
255
+ expect(element.state.y).to be(3)
256
+ expect(element.state.foo).to be(1)
257
+ expect(element.state.bar).to be(2)
258
+ end
259
+
260
+ it 'gets state in render method' do
261
+ Foo.class_eval do
262
+ define_state(:foo) { 10 }
263
+ def render
264
+ React.create_element('div') { self.foo }
265
+ end
266
+ end
267
+
268
+ element = renderToDocument(Foo)
269
+ expect(Element[element].text).to eq('10')
270
+ end
271
+
272
+ it 'supports original `setState` as `set_state` method' do
273
+ Foo.class_eval do
274
+ before_mount do
275
+ self.set_state(foo: 'bar')
276
+ end
277
+ end
278
+
279
+ element = renderToDocument(Foo)
280
+ expect(element.state.foo).to be('bar')
281
+ end
282
+
283
+ it 'supports original `replaceState` as `set_state!` method' do
284
+ Foo.class_eval do
285
+ before_mount do
286
+ self.set_state(foo: 'bar')
287
+ self.set_state!(bar: 'lorem')
288
+ end
289
+ end
290
+
291
+ element = renderToDocument(Foo)
292
+ expect(element.state.foo).to be_nil
293
+ expect(element.state.bar).to eq('lorem')
294
+ end
295
+
296
+ it 'supports original `state` method' do
297
+ Foo.class_eval do
298
+ before_mount do
299
+ self.set_state(foo: 'bar')
300
+ end
301
+
302
+ def render
303
+ div { self.state[:foo] }
304
+ end
305
+ end
306
+
307
+ expect(Foo).to render('<div>bar</div>')
308
+ end
309
+
310
+ it 'transforms state getter to Ruby object' do
311
+ Foo.class_eval do
312
+ define_state :foo
313
+
314
+ before_mount do
315
+ self.foo = [{a: "Hello"}]
316
+ end
317
+
318
+ def render
319
+ div { self.foo[0][:a] }
320
+ end
321
+ end
322
+
323
+ expect(Foo).to render('<div>Hello</div>')
324
+ end
325
+ end
326
+
327
+ describe 'Props' do
328
+ describe 'this.props could be accessed through `params` method' do
329
+ before do
330
+ stub_const 'Foo', Class.new
331
+ Foo.class_eval do
332
+ include React::Component
333
+ end
334
+ end
335
+
336
+ it 'reads from parent passed properties through `params`' do
337
+ Foo.class_eval do
338
+ def render
339
+ React.create_element('div') { params[:prop] }
340
+ end
341
+ end
342
+
343
+ element = renderToDocument(Foo, prop: 'foobar')
344
+ expect(Element[element].text).to eq('foobar')
345
+ end
346
+
347
+ it 'accesses nested params as orignal Ruby object' do
348
+ Foo.class_eval do
349
+ def render
350
+ React.create_element('div') { params[:prop][0][:foo] }
351
+ end
352
+ end
353
+
354
+ element = renderToDocument(Foo, prop: [{foo: 10}])
355
+ expect(Element[element].text).to eq('10')
356
+ end
357
+ end
358
+
359
+ describe 'Props Updating', v13_only: true do
360
+ before do
361
+ stub_const 'Foo', Class.new
362
+ Foo.class_eval do
363
+ include React::Component
364
+ end
365
+ end
366
+
367
+ it 'supports original `setProps` as method `set_props`' do
368
+ Foo.class_eval do
369
+ def render
370
+ React.create_element('div') { params[:foo] }
371
+ end
372
+ end
373
+
374
+ element = renderToDocument(Foo, {foo: 10})
375
+ element.set_props(foo: 20)
376
+ expect(`#{element.dom_node}.innerHTML`).to eq('20')
377
+ end
378
+
379
+ it 'supports original `replaceProps` as method `set_props!`' do
380
+ Foo.class_eval do
381
+ def render
382
+ React.create_element('div') { params[:foo] ? 'exist' : 'null' }
383
+ end
384
+ end
385
+
386
+ element = renderToDocument(Foo, {foo: 10})
387
+ element.set_props!(bar: 20)
388
+ expect(element.getDOMNode.innerHTML).to eq('null')
389
+ end
390
+ end
391
+
392
+ describe 'Prop validation' do
393
+ before do
394
+ stub_const 'Foo', Class.new
395
+ Foo.class_eval do
396
+ include React::Component
397
+ end
398
+ end
399
+
400
+ it 'specifies validation rules using `params` class method' do
401
+ Foo.class_eval do
402
+ params do
403
+ requires :foo, type: String
404
+ optional :bar
405
+ end
406
+ end
407
+
408
+ expect(Foo.prop_types).to have_key(:_componentValidator)
409
+ end
410
+
411
+ it 'logs error in warning if validation failed' do
412
+ stub_const 'Lorem', Class.new
413
+ Foo.class_eval do
414
+ params do
415
+ requires :foo
416
+ requires :lorem, type: Lorem
417
+ optional :bar, type: String
418
+ end
419
+
420
+ def render; div; end
421
+ end
422
+
423
+ %x{
424
+ var log = [];
425
+ var org_warn_console = window.console.warn;
426
+ var org_error_console = window.console.error;
427
+ window.console.warn = window.console.error = function(str){log.push(str)}
428
+ }
429
+ renderToDocument(Foo, bar: 10, lorem: Lorem.new)
430
+ `window.console.warn = org_warn_console; window.console.error = org_error_console;`
431
+ expect(`log[0]`).to match(/Warning: Failed prop( type|Type): In component `Foo`\nRequired prop `foo` was not specified\nProvided prop `bar` could not be converted to String/)
432
+ end
433
+
434
+ it 'should not log anything if validation pass' do
435
+ stub_const 'Lorem', Class.new
436
+ Foo.class_eval do
437
+ params do
438
+ requires :foo
439
+ requires :lorem, type: Lorem
440
+ optional :bar, type: String
441
+ end
442
+
443
+ def render; div; end
444
+ end
445
+
446
+ %x{
447
+ var log = [];
448
+ var org_warn_console = window.console.warn;
449
+ var org_error_console = window.console.error
450
+ window.console.warn = window.console.error = function(str){log.push(str)}
451
+ }
452
+ renderToDocument(Foo, foo: 10, bar: '10', lorem: Lorem.new)
453
+ `window.console.warn = org_warn_console; window.console.error = org_error_console;`
454
+ expect(`log`).to eq([])
455
+ end
456
+ end
457
+
458
+ describe 'Default props' do
459
+ it 'sets default props using validation helper' do
460
+ stub_const 'Foo', Class.new
461
+ Foo.class_eval do
462
+ include React::Component
463
+ params do
464
+ optional :foo, default: 'foo'
465
+ optional :bar, default: 'bar'
466
+ end
467
+
468
+ def render
469
+ div { params[:foo] + '-' + params[:bar]}
470
+ end
471
+ end
472
+
473
+ expect(Foo).to render('<div>lorem-bar</div>').with_params(foo: 'lorem')
474
+ expect(Foo).to render('<div>foo-bar</div>')
475
+ end
476
+ end
477
+ end
478
+
479
+ describe 'Anonymous Component' do
480
+ it "will not generate spurious warning messages" do
481
+ foo = Class.new(React::Component::Base)
482
+ foo.class_eval do
483
+ def render; "hello" end
484
+ end
485
+
486
+ %x{
487
+ var log = [];
488
+ var org_warn_console = window.console.warn;
489
+ var org_error_console = window.console.error
490
+ window.console.warn = window.console.error = function(str){log.push(str)}
491
+ }
492
+ renderToDocument(foo)
493
+ `window.console.warn = org_warn_console; window.console.error = org_error_console;`
494
+ expect(`log`).to eq([])
495
+ end
496
+ end
497
+
498
+ describe 'Render Error Handling' do
499
+ before(:each) do
500
+ %x{
501
+ window.test_log = [];
502
+ window.org_warn_console = window.console.warn;
503
+ window.org_error_console = window.console.error
504
+ window.console.warn = window.console.error = function(str){window.test_log.push(str)}
505
+ }
506
+ end
507
+ it "will generate a message if render returns something other than an Element or a String" do
508
+ foo = Class.new(React::Component::Base)
509
+ foo.class_eval do
510
+ def render; Hash.new; end
511
+ end
512
+
513
+ renderToDocument(foo)
514
+ `window.console.warn = window.org_warn_console; window.console.error = window.org_error_console;`
515
+ expect(`test_log`.first).to match /Instead the Hash \{\} was returned/
516
+ end
517
+ it "will generate a message if render returns a Component class" do
518
+ stub_const 'Foo', Class.new(React::Component::Base)
519
+ foo = Class.new(React::Component::Base)
520
+ foo.class_eval do
521
+ def render; Foo; end
522
+ end
523
+
524
+ renderToDocument(foo)
525
+ `window.console.warn = window.org_warn_console; window.console.error = window.org_error_console;`
526
+ expect(`test_log`.first).to match /Did you mean Foo()/
527
+ end
528
+ it "will generate a message if more than 1 element is generated" do
529
+ foo = Class.new(React::Component::Base)
530
+ foo.class_eval do
531
+ def render; "hello".span; "goodby".span; end
532
+ end
533
+
534
+ renderToDocument(foo)
535
+ `window.console.warn = window.org_warn_console; window.console.error = window.org_error_console;`
536
+ expect(`test_log`.first).to match /Instead 2 elements were generated/
537
+ end
538
+ it "will generate a message if the element generated is not the element returned" do
539
+ foo = Class.new(React::Component::Base)
540
+ foo.class_eval do
541
+ def render; "hello".span; "goodby".span.delete; end
542
+ end
543
+
544
+ renderToDocument(foo)
545
+ `window.console.warn = window.org_warn_console; window.console.error = window.org_error_console;`
546
+ expect(`test_log`.first).to match /A different element was returned than was generated within the DSL/
547
+ end
548
+ end
549
+
550
+ describe 'Event handling' do
551
+ before do
552
+ stub_const 'Foo', Class.new
553
+ Foo.class_eval do
554
+ include React::Component
555
+ end
556
+ end
557
+
558
+ it 'works in render method' do
559
+ Foo.class_eval do
560
+ define_state(:clicked) { false }
561
+
562
+ def render
563
+ React.create_element('div').on(:click) do
564
+ self.clicked = true
565
+ end
566
+ end
567
+ end
568
+
569
+ element = React.create_element(Foo)
570
+ instance = renderElementToDocument(element)
571
+ simulateEvent(:click, instance)
572
+ expect(instance.state.clicked).to eq(true)
573
+ end
574
+
575
+ it 'invokes handler on `this.props` using emit' do
576
+ Foo.class_eval do
577
+ after_mount :setup
578
+
579
+ def setup
580
+ self.emit(:foo_submit, 'bar')
581
+ end
582
+
583
+ def render
584
+ React.create_element('div')
585
+ end
586
+ end
587
+
588
+ expect { |b|
589
+ element = React.create_element(Foo).on(:foo_submit, &b)
590
+ renderElementToDocument(element)
591
+ }.to yield_with_args('bar')
592
+ end
593
+
594
+ it 'invokes handler with multiple params using emit' do
595
+ Foo.class_eval do
596
+ after_mount :setup
597
+
598
+ def setup
599
+ self.emit(:foo_invoked, [1,2,3], 'bar')
600
+ end
601
+
602
+ def render
603
+ React.create_element('div')
604
+ end
605
+ end
606
+
607
+ expect { |b|
608
+ element = React.create_element(Foo).on(:foo_invoked, &b)
609
+ renderElementToDocument(element)
610
+ }.to yield_with_args([1,2,3], 'bar')
611
+ end
612
+ end
613
+
614
+ describe '#refs' do
615
+ before do
616
+ stub_const 'Foo', Class.new
617
+ Foo.class_eval do
618
+ include React::Component
619
+ end
620
+ end
621
+
622
+ it 'correctly assigns refs' do
623
+ Foo.class_eval do
624
+ def render
625
+ React.create_element('input', type: :text, ref: :field)
626
+ end
627
+ end
628
+
629
+ element = renderToDocument(Foo)
630
+ expect(element.refs.field).not_to be_nil
631
+ end
632
+
633
+ it 'accesses refs through `refs` method' do
634
+ Foo.class_eval do
635
+ def render
636
+ React.create_element('input', type: :text, ref: :field).on(:click) do
637
+ refs[:field].value = 'some_stuff'
638
+ end
639
+ end
640
+ end
641
+
642
+ element = renderToDocument(Foo)
643
+ simulateEvent(:click, element)
644
+
645
+ expect(element.refs.field.value).to eq('some_stuff')
646
+ end
647
+
648
+ it "allows access the actual DOM node" do
649
+ Foo.class_eval do
650
+ after_mount do
651
+ dom = refs[:my_div].dom_node
652
+ `dom.innerHTML = 'Modified'`
653
+ end
654
+
655
+ def render
656
+ React.create_element('div', ref: :my_div) { "Original Content" }
657
+ end
658
+ end
659
+
660
+ instance = renderToDocument(Foo)
661
+ expect(`#{instance.dom_node}.innerHTML`).to eq('Modified')
662
+ end
663
+ end
664
+
665
+ describe '#render' do
666
+ it 'supports element building helpers' do
667
+ stub_const 'Foo', Class.new
668
+ Foo.class_eval do
669
+ include React::Component
670
+
671
+ def render
672
+ div do
673
+ span { params[:foo] }
674
+ end
675
+ end
676
+ end
677
+
678
+ stub_const 'Bar', Class.new
679
+ Bar.class_eval do
680
+ include React::Component
681
+ def render
682
+ div do
683
+ present Foo, foo: 'astring'
684
+ end
685
+ end
686
+ end
687
+
688
+ expect(Bar).to render('<div><div><span>astring</span></div></div>')
689
+ end
690
+
691
+ it 'builds single node in top-level render without providing a block' do
692
+ stub_const 'Foo', Class.new
693
+ Foo.class_eval do
694
+ include React::Component
695
+
696
+ def render
697
+ div
698
+ end
699
+ end
700
+
701
+ expect(Foo).to render('<div></div>')
702
+ end
703
+
704
+ it 'redefines `p` to make method missing work' do
705
+ stub_const 'Foo', Class.new
706
+ Foo.class_eval do
707
+ include React::Component
708
+
709
+ def render
710
+ p(class_name: 'foo') do
711
+ p
712
+ div { 'lorem ipsum' }
713
+ p(id: '10')
714
+ end
715
+ end
716
+ end
717
+
718
+ markup = '<p class="foo"><p></p><div>lorem ipsum</div><p id="10"></p></p>'
719
+ expect(Foo).to render(markup)
720
+ end
721
+
722
+ it 'only overrides `p` in render context' do
723
+ stub_const 'Foo', Class.new
724
+ Foo.class_eval do
725
+ include React::Component
726
+
727
+ before_mount do
728
+ p 'first'
729
+ end
730
+
731
+ after_mount do
732
+ p 'second'
733
+ end
734
+
735
+ def render
736
+ div
737
+ end
738
+ end
739
+
740
+ expect(Kernel).to receive(:p).with('first')
741
+ expect(Kernel).to receive(:p).with('second')
742
+ renderToDocument(Foo)
743
+ end
744
+ end
745
+
746
+ describe 'isMounted()' do
747
+ it 'returns true if after mounted' do
748
+ stub_const 'Foo', Class.new
749
+ Foo.class_eval do
750
+ include React::Component
751
+
752
+ def render
753
+ React.create_element('div')
754
+ end
755
+ end
756
+
757
+ component = renderToDocument(Foo)
758
+ expect(component.mounted?).to eq(true)
759
+ end
760
+ end
761
+
762
+ describe '.params_changed?' do
763
+
764
+ before(:each) do
765
+ stub_const 'Foo', Class.new(React::Component::Base)
766
+ Foo.define_method :needs_update? do |next_params, next_state|
767
+ next_params.changed?
768
+ end
769
+ @foo = Foo.new
770
+ end
771
+
772
+ it "returns false if new and old params are the same" do
773
+ @foo.instance_variable_set("@native", `{props: {value1: 1, value2: 2}}`)
774
+ expect(@foo.should_component_update?(`{value2: 2, value1: 1}`, `null`)).to be_falsy
775
+ end
776
+
777
+ it "returns true if new and old params are have different values" do
778
+ @foo.instance_variable_set("@native", `{props: {value1: 1, value2: 2}}`)
779
+ expect(@foo.should_component_update?(`{value2: 2, value1: 2}`, `null`)).to be_truthy
780
+ end
781
+
782
+ it "returns true if new and old params are have different keys" do
783
+ @foo.instance_variable_set("@native", `{props: {value1: 1, value2: 2}}`)
784
+ expect(@foo.should_component_update?(`{value2: 2, value1: 1, value3: 3}`, `null`)).to be_truthy
785
+ end
786
+ end
787
+
788
+ describe '#state_changed?' do
789
+
790
+ empties = [`{}`, `undefined`, `null`, `false`]
791
+
792
+ before(:each) do
793
+ stub_const 'Foo', Class.new(React::Component::Base)
794
+ Foo.define_method :needs_update? do |next_params, next_state|
795
+ next_state.changed?
796
+ end
797
+ @foo = Foo.new
798
+ end
799
+
800
+ it "returns false if both new and old states are empty" do
801
+ empties.each do |empty1|
802
+ empties.each do |empty2|
803
+ @foo.instance_variable_set("@native", `{state: #{empty1}}`)
804
+ expect(@foo.should_component_update?(`{}`, empty2)).to be_falsy
805
+ end
806
+ end
807
+ end
808
+
809
+ it "returns true if old state is empty, but new state is not" do
810
+ empties.each do |empty|
811
+ @foo.instance_variable_set("@native", `{state: #{empty}}`)
812
+ expect(@foo.should_component_update?(`{}`, `{foo: 12}`)).to be_truthy
813
+ end
814
+ end
815
+
816
+ it "returns true if new state is empty, but old state is not" do
817
+ empties.each do |empty|
818
+ @foo.instance_variable_set("@native", `{state: {foo: 12}}`)
819
+ expect(@foo.should_component_update?(`{}`, empty)).to be_truthy
820
+ end
821
+ end
822
+
823
+ it "returns true if new state and old state have different time stamps" do
824
+ @foo.instance_variable_set("@native", `{state: {'***_state_updated_at-***': 12}}`)
825
+ expect(@foo.should_component_update?(`{}`, `{'***_state_updated_at-***': 13}`)).to be_truthy
826
+ end
827
+
828
+ it "returns false if new state and old state have the same time stamps" do
829
+ @foo.instance_variable_set("@native", `{state: {'***_state_updated_at-***': 12}}`)
830
+ expect(@foo.should_component_update?(`{}`, `{'***_state_updated_at-***': 12}`)).to be_falsy
831
+ end
832
+
833
+ end
834
+
835
+ describe '#children' do
836
+ before(:each) do
837
+ stub_const 'Foo', Class.new
838
+ Foo.class_eval do
839
+ include React::Component
840
+ export_state :the_children
841
+ before_mount do
842
+ the_children! children
843
+ end
844
+ def render
845
+ React.create_element('div') { 'lorem' }
846
+ end
847
+ end
848
+ end
849
+
850
+ it 'returns React::Children collection with child elements' do
851
+ ele = React.create_element(Foo) {
852
+ [React.create_element('a'), React.create_element('li')]
853
+ }
854
+ renderElementToDocument(ele)
855
+
856
+ children = Foo.the_children
857
+
858
+ expect(children).to be_a(React::Children)
859
+ expect(children.count).to eq(2)
860
+ expect(children.map(&:element_type)).to eq(['a', 'li'])
861
+ end
862
+
863
+ it 'returns an empty Enumerator if there are no children' do
864
+ ele = React.create_element(Foo)
865
+ renderElementToDocument(ele)
866
+ nodes = Foo.the_children.each
867
+ expect(nodes.size).to eq(0)
868
+ expect(nodes.count).to eq(0)
869
+ end
870
+ end
871
+ end
872
+ end