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.
- checksums.yaml +7 -0
- data/.gitignore +30 -0
- data/Gemfile +3 -0
- data/Gemfile.lock +53 -0
- data/LICENSE +19 -0
- data/README.md +303 -0
- data/config.ru +15 -0
- data/example/examples/Gemfile +7 -0
- data/example/examples/Gemfile.lock +45 -0
- data/example/examples/config.ru +44 -0
- data/example/examples/hello.js.rb +43 -0
- data/example/react-tutorial/Gemfile +7 -0
- data/example/react-tutorial/Gemfile.lock +49 -0
- data/example/react-tutorial/README.md +8 -0
- data/example/react-tutorial/_comments.json +14 -0
- data/example/react-tutorial/config.ru +63 -0
- data/example/react-tutorial/example.js.rb +290 -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/rails-helpers/react_component.rb +32 -0
- data/lib/reactive-ruby.rb +23 -0
- data/lib/reactive-ruby/api.rb +177 -0
- data/lib/reactive-ruby/callbacks.rb +35 -0
- data/lib/reactive-ruby/component.rb +411 -0
- data/lib/reactive-ruby/element.rb +87 -0
- data/lib/reactive-ruby/event.rb +76 -0
- data/lib/reactive-ruby/ext/hash.rb +9 -0
- data/lib/reactive-ruby/ext/string.rb +8 -0
- data/lib/reactive-ruby/isomorphic_helpers.rb +223 -0
- data/lib/reactive-ruby/observable.rb +33 -0
- data/lib/reactive-ruby/rendering_context.rb +91 -0
- data/lib/reactive-ruby/serializers.rb +15 -0
- data/lib/reactive-ruby/state.rb +90 -0
- data/lib/reactive-ruby/top_level.rb +53 -0
- data/lib/reactive-ruby/validator.rb +83 -0
- data/lib/reactive-ruby/version.rb +3 -0
- data/logo1.png +0 -0
- data/logo2.png +0 -0
- data/logo3.png +0 -0
- data/reactive-ruby.gemspec +25 -0
- data/spec/callbacks_spec.rb +107 -0
- data/spec/component_spec.rb +597 -0
- data/spec/element_spec.rb +60 -0
- data/spec/event_spec.rb +22 -0
- data/spec/react_spec.rb +209 -0
- data/spec/reactjs/index.html.erb +11 -0
- data/spec/spec_helper.rb +29 -0
- data/spec/tutorial/tutorial_spec.rb +37 -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 +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
|
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
|