react.rb 0.0.1
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.
- checksums.yaml +7 -0
- data/.gitignore +30 -0
- data/Gemfile +2 -0
- data/Gemfile.lock +48 -0
- data/LICENSE +19 -0
- data/README.md +166 -0
- data/config.ru +15 -0
- data/example/react-tutorial/Gemfile +6 -0
- data/example/react-tutorial/Gemfile.lock +45 -0
- data/example/react-tutorial/README.md +8 -0
- data/example/react-tutorial/_comments.json +6 -0
- data/example/react-tutorial/config.ru +55 -0
- data/example/react-tutorial/example.rb +104 -0
- data/example/react-tutorial/public/base.css +62 -0
- data/example/todos/Gemfile +11 -0
- data/example/todos/Gemfile.lock +84 -0
- data/example/todos/README.md +37 -0
- data/example/todos/Rakefile +8 -0
- data/example/todos/app/application.rb +22 -0
- data/example/todos/app/components/app.react.rb +61 -0
- data/example/todos/app/components/footer.react.rb +31 -0
- data/example/todos/app/components/todo_item.react.rb +46 -0
- data/example/todos/app/components/todo_list.react.rb +25 -0
- data/example/todos/app/models/todo.rb +19 -0
- data/example/todos/config.ru +14 -0
- data/example/todos/index.html.haml +16 -0
- data/example/todos/spec/todo_spec.rb +28 -0
- data/example/todos/vendor/base.css +410 -0
- data/example/todos/vendor/bg.png +0 -0
- data/example/todos/vendor/jquery.js +4 -0
- data/lib/react.rb +16 -0
- data/lib/react/api.rb +95 -0
- data/lib/react/callbacks.rb +35 -0
- data/lib/react/component.rb +197 -0
- data/lib/react/element.rb +63 -0
- data/lib/react/event.rb +76 -0
- data/lib/react/ext/hash.rb +9 -0
- data/lib/react/ext/string.rb +8 -0
- data/lib/react/top_level.rb +50 -0
- data/lib/react/validator.rb +65 -0
- data/lib/react/version.rb +3 -0
- data/react.rb.gemspec +24 -0
- data/spec/callbacks_spec.rb +107 -0
- data/spec/component_spec.rb +556 -0
- data/spec/element_spec.rb +60 -0
- data/spec/event_spec.rb +22 -0
- data/spec/react_spec.rb +168 -0
- data/spec/reactjs/index.html.erb +10 -0
- data/spec/spec_helper.rb +29 -0
- data/spec/validator_spec.rb +79 -0
- data/vendor/active_support/core_ext/array/extract_options.rb +29 -0
- data/vendor/active_support/core_ext/class/attribute.rb +127 -0
- data/vendor/active_support/core_ext/kernel/singleton_class.rb +13 -0
- data/vendor/active_support/core_ext/module/remove_method.rb +11 -0
- metadata +189 -0
@@ -0,0 +1,50 @@
|
|
1
|
+
require "native"
|
2
|
+
require 'active_support'
|
3
|
+
|
4
|
+
module React
|
5
|
+
HTML_TAGS = %w(a abbr address area article aside audio b base bdi bdo big blockquote body br
|
6
|
+
button canvas caption cite code col colgroup data datalist dd del details dfn
|
7
|
+
dialog div dl dt em embed fieldset figcaption figure footer form h1 h2 h3 h4 h5
|
8
|
+
h6 head header hr html i iframe img input ins kbd keygen label legend li link
|
9
|
+
main map mark menu menuitem meta meter nav noscript object ol optgroup option
|
10
|
+
output p param picture pre progress q rp rt ruby s samp script section select
|
11
|
+
small source span strong style sub summary sup table tbody td textarea tfoot th
|
12
|
+
thead time title tr track u ul var video wbr)
|
13
|
+
ATTRIBUTES = %w(accept acceptCharset accessKey action allowFullScreen allowTransparency alt
|
14
|
+
async autoComplete autoPlay cellPadding cellSpacing charSet checked classID
|
15
|
+
className cols colSpan content contentEditable contextMenu controls coords
|
16
|
+
crossOrigin data dateTime defer dir disabled download draggable encType form
|
17
|
+
formAction formEncType formMethod formNoValidate formTarget frameBorder height
|
18
|
+
hidden href hrefLang htmlFor httpEquiv icon id label lang list loop manifest
|
19
|
+
marginHeight marginWidth max maxLength media mediaGroup method min multiple
|
20
|
+
muted name noValidate open pattern placeholder poster preload radioGroup
|
21
|
+
readOnly rel required role rows rowSpan sandbox scope scrolling seamless
|
22
|
+
selected shape size sizes span spellCheck src srcDoc srcSet start step style
|
23
|
+
tabIndex target title type useMap value width wmode dangerouslySetInnerHTML)
|
24
|
+
|
25
|
+
def self.create_element(type, properties = {}, &block)
|
26
|
+
React::API.create_element(type, properties, &block)
|
27
|
+
end
|
28
|
+
|
29
|
+
def self.render(element, container)
|
30
|
+
component = Native(`React.render(#{element.to_n}, container, function(){#{yield if block_given?}})`)
|
31
|
+
component.class.include(React::Component::API)
|
32
|
+
component
|
33
|
+
end
|
34
|
+
|
35
|
+
def self.is_valid_element(element)
|
36
|
+
element.kind_of?(React::Element) && `React.isValidElement(#{element.to_n})`
|
37
|
+
end
|
38
|
+
|
39
|
+
def self.render_to_string(element)
|
40
|
+
`React.renderToString(#{element.to_n})`
|
41
|
+
end
|
42
|
+
|
43
|
+
def self.render_to_static_markup(element)
|
44
|
+
`React.renderToStaticMarkup(#{element.to_n})`
|
45
|
+
end
|
46
|
+
|
47
|
+
def self.unmount_component_at_node(node)
|
48
|
+
`React.unmountComponentAtNode(node)`
|
49
|
+
end
|
50
|
+
end
|
@@ -0,0 +1,65 @@
|
|
1
|
+
module React
|
2
|
+
class Validator
|
3
|
+
def self.build(&block)
|
4
|
+
validator = self.new
|
5
|
+
validator.instance_eval(&block)
|
6
|
+
validator
|
7
|
+
end
|
8
|
+
|
9
|
+
def initialize
|
10
|
+
@rules = {}
|
11
|
+
end
|
12
|
+
|
13
|
+
def requires(prop_name, options = {})
|
14
|
+
rule = options
|
15
|
+
options[:required] = true
|
16
|
+
@rules[prop_name] = options
|
17
|
+
end
|
18
|
+
|
19
|
+
def optional(prop_name, options = {})
|
20
|
+
rule = options
|
21
|
+
options[:required] = false
|
22
|
+
@rules[prop_name] = options
|
23
|
+
end
|
24
|
+
|
25
|
+
def validate(props)
|
26
|
+
errors = []
|
27
|
+
props.keys.each do |prop_name|
|
28
|
+
errors << "Provided prop `#{prop_name}` not specified in spec" if @rules[prop_name] == nil
|
29
|
+
end
|
30
|
+
|
31
|
+
props = props.select {|key| @rules.keys.include?(key) }
|
32
|
+
|
33
|
+
# requires or not
|
34
|
+
(@rules.keys - props.keys).each do |prop_name|
|
35
|
+
errors << "Required prop `#{prop_name}` was not specified" if @rules[prop_name][:required]
|
36
|
+
end
|
37
|
+
|
38
|
+
# type
|
39
|
+
props.each do |prop_name, value|
|
40
|
+
if klass = @rules[prop_name][:type]
|
41
|
+
if klass.is_a?(Array)
|
42
|
+
errors << "Provided prop `#{prop_name}` was not an Array of the specified type `#{klass[0]}`" unless value.all?{ |ele| ele.is_a?(klass[0]) }
|
43
|
+
else
|
44
|
+
errors << "Provided prop `#{prop_name}` was not the specified type `#{klass}`" unless value.is_a?(klass)
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
# values
|
50
|
+
props.each do |prop_name, value|
|
51
|
+
if values = @rules[prop_name][:values]
|
52
|
+
errors << "Value `#{value}` for prop `#{prop_name}` is not an allowed value" unless values.include?(value)
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
errors
|
57
|
+
end
|
58
|
+
|
59
|
+
def default_props
|
60
|
+
@rules
|
61
|
+
.select {|key, value| value.keys.include?("default") }
|
62
|
+
.inject({}) {|memo, (k,v)| memo[k] = v[:default]; memo}
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
data/react.rb.gemspec
ADDED
@@ -0,0 +1,24 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
require File.expand_path('../lib/react/version', __FILE__)
|
3
|
+
|
4
|
+
Gem::Specification.new do |s|
|
5
|
+
s.name = 'react.rb'
|
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.description = "Write reactive UI component with Ruby's elegancy and compiled to run in Javascript."
|
12
|
+
|
13
|
+
s.files = `git ls-files`.split("\n")
|
14
|
+
s.executables = `git ls-files -- bin/*`.split("\n").map { |f| File.basename(f) }
|
15
|
+
s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
|
16
|
+
s.require_paths = ['lib', 'vendor']
|
17
|
+
|
18
|
+
s.add_runtime_dependency 'opal', '~> 0.6.0'
|
19
|
+
s.add_runtime_dependency 'opal-activesupport'
|
20
|
+
s.add_development_dependency 'react-source', '~> 0.12'
|
21
|
+
s.add_development_dependency 'opal-rspec', '~> 0.3.0.beta3'
|
22
|
+
s.add_development_dependency 'sinatra'
|
23
|
+
s.add_development_dependency 'opal-jquery'
|
24
|
+
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,556 @@
|
|
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: 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
|
+
end
|
540
|
+
|
541
|
+
describe "isMounted()" do
|
542
|
+
it "should return true if after mounted" do
|
543
|
+
stub_const 'Foo', Class.new
|
544
|
+
Foo.class_eval do
|
545
|
+
include React::Component
|
546
|
+
|
547
|
+
def render
|
548
|
+
React.create_element("div")
|
549
|
+
end
|
550
|
+
end
|
551
|
+
|
552
|
+
component = renderToDocument(Foo)
|
553
|
+
expect(component.mounted?).to eq(true)
|
554
|
+
end
|
555
|
+
end
|
556
|
+
end
|