hyper-component 0.12.3 → 0.99.0
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 +5 -5
- data/.codeclimate.yml +27 -0
- data/.gitignore +42 -41
- data/.travis.yml +29 -0
- data/CHANGELOG.md +143 -0
- data/DOCS.md +1515 -0
- data/Gemfile +5 -2
- data/Gemfile.lock +244 -193
- data/LICENSE +5 -7
- data/README.md +49 -0
- data/Rakefile +40 -0
- data/hyper-component.gemspec +41 -31
- data/lib/hyper-component.rb +44 -9
- data/lib/rails-helpers/top_level_rails_component.rb +79 -0
- data/lib/react/api.rb +270 -0
- data/lib/react/callbacks.rb +42 -0
- data/lib/react/children.rb +38 -0
- data/lib/react/component.rb +189 -0
- data/lib/react/component/api.rb +70 -0
- data/lib/react/component/base.rb +13 -0
- data/lib/react/component/class_methods.rb +175 -0
- data/lib/react/component/dsl_instance_methods.rb +23 -0
- data/lib/react/component/params.rb +6 -0
- data/lib/react/component/props_wrapper.rb +90 -0
- data/lib/react/component/should_component_update.rb +99 -0
- data/lib/react/component/tags.rb +116 -0
- data/lib/react/config.rb +5 -0
- data/lib/react/element.rb +159 -0
- data/lib/react/event.rb +76 -0
- data/lib/react/ext/hash.rb +9 -0
- data/lib/react/ext/opal-jquery/element.rb +37 -0
- data/lib/react/ext/string.rb +8 -0
- data/lib/react/native_library.rb +87 -0
- data/lib/react/object.rb +15 -0
- data/lib/react/react-source-server.rb +3 -0
- data/lib/react/react-source.rb +17 -0
- data/lib/react/ref_callback.rb +31 -0
- data/lib/react/rendering_context.rb +149 -0
- data/lib/react/server.rb +19 -0
- data/lib/react/state_wrapper.rb +23 -0
- data/lib/react/test.rb +16 -0
- data/lib/react/test/dsl.rb +17 -0
- data/lib/react/test/matchers/render_html_matcher.rb +56 -0
- data/lib/react/test/rspec.rb +15 -0
- data/lib/react/test/session.rb +37 -0
- data/lib/react/test/utils.rb +71 -0
- data/lib/react/to_key.rb +26 -0
- data/lib/react/top_level.rb +110 -0
- data/lib/react/top_level_render.rb +28 -0
- data/lib/react/validator.rb +132 -0
- data/lib/reactive-ruby/component_loader.rb +43 -0
- data/lib/reactive-ruby/isomorphic_helpers.rb +233 -0
- data/lib/reactive-ruby/rails.rb +8 -0
- data/lib/reactive-ruby/rails/component_mount.rb +48 -0
- data/lib/reactive-ruby/rails/controller_helper.rb +14 -0
- data/lib/reactive-ruby/rails/railtie.rb +20 -0
- data/lib/reactive-ruby/serializers.rb +23 -0
- data/lib/reactive-ruby/server_rendering/contextual_renderer.rb +46 -0
- data/lib/reactive-ruby/server_rendering/hyper_asset_container.rb +46 -0
- data/lib/{hyperloop/component → reactive-ruby}/version.rb +1 -1
- data/lib/reactrb/auto-import.rb +27 -0
- data/misc/generators/reactive_ruby/test_app/templates/assets/javascripts/components.rb +3 -0
- data/misc/generators/reactive_ruby/test_app/templates/assets/javascripts/server_rendering.js +5 -0
- data/misc/generators/reactive_ruby/test_app/templates/assets/javascripts/test_application.rb +2 -0
- data/misc/generators/reactive_ruby/test_app/templates/boot.rb.erb +6 -0
- data/misc/generators/reactive_ruby/test_app/templates/script/rails +5 -0
- data/misc/generators/reactive_ruby/test_app/templates/test_application.rb.erb +13 -0
- data/misc/generators/reactive_ruby/test_app/templates/views/components/hello_world.rb +11 -0
- data/misc/generators/reactive_ruby/test_app/templates/views/components/todo.rb +14 -0
- data/misc/generators/reactive_ruby/test_app/templates/views/layouts/test_layout.html.erb +0 -0
- data/misc/generators/reactive_ruby/test_app/test_app_generator.rb +121 -0
- data/misc/how-component-name-lookup-works.md +145 -0
- data/misc/hyperloop-logo-small-pink.png +0 -0
- data/misc/logo1.png +0 -0
- data/misc/logo2.png +0 -0
- data/misc/logo3.png +0 -0
- data/path_release_steps.md +9 -0
- metadata +260 -37
- data/CODE_OF_CONDUCT.md +0 -49
@@ -0,0 +1,23 @@
|
|
1
|
+
module HyperStore
|
2
|
+
class StateWrapper < BaseStoreClass # < BasicObject
|
3
|
+
|
4
|
+
def [](state)
|
5
|
+
`#{__from__.instance_variable_get('@native')}.state[#{state}] || #{nil}`
|
6
|
+
end
|
7
|
+
|
8
|
+
def []=(state, new_value)
|
9
|
+
`#{__from__.instance_variable_get('@native')}.state[#{state}] = new_value`
|
10
|
+
end
|
11
|
+
|
12
|
+
alias pre_component_method_missing method_missing
|
13
|
+
|
14
|
+
def method_missing(method, *args)
|
15
|
+
if method.end_with?('!') && __from__.respond_to?(:deprecation_warning)
|
16
|
+
__from__.deprecation_warning("The mutator 'state.#{method}' has been deprecated. Use 'mutate.#{method.sub(/\!$/,'')}' instead.")
|
17
|
+
__from__.mutate.__send__(method.chop, *args)
|
18
|
+
else
|
19
|
+
pre_component_method_missing(method, *args)
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
data/lib/react/test.rb
ADDED
@@ -0,0 +1,17 @@
|
|
1
|
+
require 'react/test'
|
2
|
+
|
3
|
+
module React
|
4
|
+
module Test
|
5
|
+
module DSL
|
6
|
+
def component
|
7
|
+
React::Test.current_session
|
8
|
+
end
|
9
|
+
|
10
|
+
Session::DSL_METHODS.each do |method|
|
11
|
+
define_method method do |*args, &block|
|
12
|
+
component.public_send(method, *args, &block)
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
@@ -0,0 +1,56 @@
|
|
1
|
+
module React
|
2
|
+
module Test
|
3
|
+
module Matchers
|
4
|
+
class RenderHTMLMatcher
|
5
|
+
def initialize(expected)
|
6
|
+
@expected = expected
|
7
|
+
@params = {}
|
8
|
+
end
|
9
|
+
|
10
|
+
def with_params(params)
|
11
|
+
@params = params
|
12
|
+
self
|
13
|
+
end
|
14
|
+
|
15
|
+
def matches?(component)
|
16
|
+
@component = component
|
17
|
+
@actual = render_to_html
|
18
|
+
@expected == @actual
|
19
|
+
end
|
20
|
+
|
21
|
+
def failure_message
|
22
|
+
failure_string
|
23
|
+
end
|
24
|
+
|
25
|
+
def failure_message_when_negated
|
26
|
+
failure_string(:negative)
|
27
|
+
end
|
28
|
+
|
29
|
+
alias negative_failure_message failure_message_when_negated
|
30
|
+
|
31
|
+
private
|
32
|
+
|
33
|
+
def render_to_html
|
34
|
+
element = React.create_element(@component, @params)
|
35
|
+
React::Server.render_to_static_markup(element)
|
36
|
+
end
|
37
|
+
|
38
|
+
def failure_string(negative = false)
|
39
|
+
str = "expected '#{@component.name}' with params '#{@params}' to "
|
40
|
+
str = str + "not " if negative
|
41
|
+
str = str + "render '#{@expected}', but '#{@actual}' was rendered."
|
42
|
+
str
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
def render_static_html(*args)
|
47
|
+
RenderHTMLMatcher.new(*args)
|
48
|
+
end
|
49
|
+
|
50
|
+
def render(*args)
|
51
|
+
%x{ console.error("Warning: `render` matcher is deprecated in favor of `render_static_html`."); }
|
52
|
+
RenderHTMLMatcher.new(*args)
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
require 'react/test/dsl'
|
2
|
+
require 'react/test/matchers/render_html_matcher'
|
3
|
+
|
4
|
+
RSpec.configure do |config|
|
5
|
+
config.include React::Test::DSL, type: :component
|
6
|
+
config.include React::Test::Matchers, type: :component
|
7
|
+
|
8
|
+
config.after do
|
9
|
+
React::Test.reset_session!
|
10
|
+
end
|
11
|
+
|
12
|
+
config.before do
|
13
|
+
# nothing yet
|
14
|
+
end
|
15
|
+
end
|
@@ -0,0 +1,37 @@
|
|
1
|
+
module React
|
2
|
+
module Test
|
3
|
+
class Session
|
4
|
+
DSL_METHODS = %i[mount instance update_params html].freeze
|
5
|
+
|
6
|
+
def mount(component_klass, params = {})
|
7
|
+
@element = React.create_element(component_klass, params)
|
8
|
+
instance
|
9
|
+
end
|
10
|
+
|
11
|
+
def instance
|
12
|
+
unless @instance
|
13
|
+
@container = `document.createElement('div')`
|
14
|
+
@instance = React.render(@element, @container)
|
15
|
+
end
|
16
|
+
@instance
|
17
|
+
end
|
18
|
+
|
19
|
+
def update_params(params, &block)
|
20
|
+
cloned_element = React::Element.new(`React.cloneElement(#{@element.to_n}, #{params.to_n})`)
|
21
|
+
React.render(cloned_element, @container, &block)
|
22
|
+
nil
|
23
|
+
end
|
24
|
+
|
25
|
+
def html
|
26
|
+
html = `#@container.innerHTML`
|
27
|
+
%x{
|
28
|
+
var REGEX_REMOVE_ROOT_IDS = /\s?data-reactroot="[^"]*"/g;
|
29
|
+
var REGEX_REMOVE_IDS = /\s?data-reactid="[^"]+"/g;
|
30
|
+
html = html.replace(REGEX_REMOVE_ROOT_IDS, '');
|
31
|
+
html = html.replace(REGEX_REMOVE_IDS, '');
|
32
|
+
}
|
33
|
+
return html
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
@@ -0,0 +1,71 @@
|
|
1
|
+
module React
|
2
|
+
module Test
|
3
|
+
class Utils
|
4
|
+
def self.render_component_into_document(component, args = {})
|
5
|
+
element = React.create_element(component, args)
|
6
|
+
render_into_document(element)
|
7
|
+
end
|
8
|
+
|
9
|
+
def self.render_into_document(element)
|
10
|
+
raise "You should pass a valid React::Element" unless React.is_valid_element?(element)
|
11
|
+
dom_el = `document.body.querySelector('div[data-react-class="React.TopLevelRailsComponent"]').appendChild(document.createElement('div'))`
|
12
|
+
React.render(element, dom_el)
|
13
|
+
end
|
14
|
+
|
15
|
+
def self.simulate_click(element)
|
16
|
+
# element must be a component or a dom node or a element
|
17
|
+
el = if `typeof element.nodeType !== "undefined"`
|
18
|
+
element
|
19
|
+
elsif element.respond_to? :dom_node
|
20
|
+
element.dom_node
|
21
|
+
elsif element.is_a? React::Element
|
22
|
+
`ReactDOM.findDOMNode(#{element.to_n}.native)`
|
23
|
+
else
|
24
|
+
element
|
25
|
+
end
|
26
|
+
%x{
|
27
|
+
var evob = new MouseEvent('click', {
|
28
|
+
view: window,
|
29
|
+
bubbles: true,
|
30
|
+
cancelable: true
|
31
|
+
});
|
32
|
+
el.dispatchEvent(evob);
|
33
|
+
}
|
34
|
+
end
|
35
|
+
|
36
|
+
def self.simulate_keydown(element, key_name = "Enter")
|
37
|
+
# element must be a component or a dom node or a element
|
38
|
+
el = if `typeof element.nodeType !== "undefined"`
|
39
|
+
element
|
40
|
+
elsif element.respond_to? :dom_node
|
41
|
+
element.dom_node
|
42
|
+
elsif element.is_a? React::Element
|
43
|
+
`ReactDOM.findDOMNode(#{element.to_n}.native)`
|
44
|
+
else
|
45
|
+
element
|
46
|
+
end
|
47
|
+
%x{
|
48
|
+
var evob = new KeyboardEvent('keydown', { key: key_name, bubbles: true, cancelable: true });
|
49
|
+
el.dispatchEvent(evob);
|
50
|
+
}
|
51
|
+
end
|
52
|
+
|
53
|
+
def self.simulate_submit(element)
|
54
|
+
# element must be a component or a dom node or a element
|
55
|
+
el = if `typeof element.nodeType !== "undefined"`
|
56
|
+
element
|
57
|
+
elsif element.respond_to? :dom_node
|
58
|
+
element.dom_node
|
59
|
+
elsif element.is_a? React::Element
|
60
|
+
`ReactDOM.findDOMNode(#{element.to_n}.native)`
|
61
|
+
else
|
62
|
+
element
|
63
|
+
end
|
64
|
+
%x{
|
65
|
+
var evob = new Event('submit', { bubbles: true, cancelable: true });
|
66
|
+
el.dispatchEvent(evob);
|
67
|
+
}
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
data/lib/react/to_key.rb
ADDED
@@ -0,0 +1,26 @@
|
|
1
|
+
# to_key method returns a suitable unique id that can be used as
|
2
|
+
# a react `key`. Other classes may override to_key as needed
|
3
|
+
# for example hyper_mesh returns the object id of the internal
|
4
|
+
# backing record.
|
5
|
+
#
|
6
|
+
# to_key is automatically called on objects passed as keys for
|
7
|
+
# example Foo(key: my_object) results in Foo(key: my_object.to_key)
|
8
|
+
class Object
|
9
|
+
def to_key
|
10
|
+
object_id
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
# for Number to_key can just be the number itself
|
15
|
+
class Number
|
16
|
+
def to_key
|
17
|
+
self
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
# for Boolean to_key can be true or false
|
22
|
+
class Boolean
|
23
|
+
def to_key
|
24
|
+
self
|
25
|
+
end
|
26
|
+
end
|
@@ -0,0 +1,110 @@
|
|
1
|
+
require "native"
|
2
|
+
require 'active_support/core_ext/object/try'
|
3
|
+
require 'react/component/tags'
|
4
|
+
require 'react/component/base'
|
5
|
+
|
6
|
+
module React
|
7
|
+
|
8
|
+
ATTRIBUTES = %w(accept acceptCharset accessKey action allowFullScreen allowTransparency alt
|
9
|
+
async autoComplete autoPlay cellPadding cellSpacing charSet checked classID
|
10
|
+
className cols colSpan content contentEditable contextMenu controls coords
|
11
|
+
crossOrigin data dateTime defer dir disabled download draggable encType form
|
12
|
+
formAction formEncType formMethod formNoValidate formTarget frameBorder height
|
13
|
+
hidden href hrefLang htmlFor httpEquiv icon id label lang list loop manifest
|
14
|
+
marginHeight marginWidth max maxLength media mediaGroup method min multiple
|
15
|
+
muted name noValidate open pattern placeholder poster preload radioGroup
|
16
|
+
readOnly rel required role rows rowSpan sandbox scope scrolling seamless
|
17
|
+
selected shape size sizes span spellCheck src srcDoc srcSet start step style
|
18
|
+
tabIndex target title type useMap value width wmode dangerouslySetInnerHTML) +
|
19
|
+
#SVG ATTRIBUTES
|
20
|
+
%w(clipPath cx cy d dx dy fill fillOpacity fontFamily
|
21
|
+
fontSize fx fy gradientTransform gradientUnits markerEnd
|
22
|
+
markerMid markerStart offset opacity patternContentUnits
|
23
|
+
patternUnits points preserveAspectRatio r rx ry spreadMethod
|
24
|
+
stopColor stopOpacity stroke strokeDasharray strokeLinecap
|
25
|
+
strokeOpacity strokeWidth textAnchor transform version
|
26
|
+
viewBox x1 x2 x xlinkActuate xlinkArcrole xlinkHref xlinkRole
|
27
|
+
xlinkShow xlinkTitle xlinkType xmlBase xmlLang xmlSpace y1 y2 y)
|
28
|
+
HASH_ATTRIBUTES = %w(data aria)
|
29
|
+
HTML_TAGS = React::Component::Tags::HTML_TAGS
|
30
|
+
|
31
|
+
def self.html_tag?(name)
|
32
|
+
tags = HTML_TAGS
|
33
|
+
%x{
|
34
|
+
for(var i = 0; i < tags.length; i++) {
|
35
|
+
if(tags[i] === name)
|
36
|
+
return true;
|
37
|
+
}
|
38
|
+
return false;
|
39
|
+
}
|
40
|
+
end
|
41
|
+
|
42
|
+
def self.html_attr?(name)
|
43
|
+
attrs = ATTRIBUTES
|
44
|
+
%x{
|
45
|
+
for(var i = 0; i < attrs.length; i++) {
|
46
|
+
if(attrs[i] === name)
|
47
|
+
return true;
|
48
|
+
}
|
49
|
+
return false;
|
50
|
+
}
|
51
|
+
end
|
52
|
+
|
53
|
+
def self.create_element(type, properties = {}, &block)
|
54
|
+
React::API.create_element(type, properties, &block)
|
55
|
+
end
|
56
|
+
|
57
|
+
def self.render(element, container)
|
58
|
+
%x{
|
59
|
+
console.error(
|
60
|
+
"Warning: Using deprecated behavior of `React.render`,",
|
61
|
+
"require \"react/top_level_render\" to get the correct behavior."
|
62
|
+
);
|
63
|
+
}
|
64
|
+
container = `container.$$class ? container[0] : container`
|
65
|
+
if !(`typeof ReactDOM === 'undefined'`)
|
66
|
+
component = Native(`ReactDOM.render(#{element.to_n}, container, function(){#{yield if block_given?}})`) # v0.15+
|
67
|
+
else
|
68
|
+
raise "render is not defined. In React >= v15 you must import it with ReactDOM"
|
69
|
+
end
|
70
|
+
|
71
|
+
component.class.include(React::Component::API)
|
72
|
+
component
|
73
|
+
end
|
74
|
+
|
75
|
+
def self.is_valid_element(element)
|
76
|
+
%x{ console.error("Warning: `is_valid_element` is deprecated in favor of `is_valid_element?`."); }
|
77
|
+
element.kind_of?(React::Element) && `React.isValidElement(#{element.to_n})`
|
78
|
+
end
|
79
|
+
|
80
|
+
def self.is_valid_element?(element)
|
81
|
+
element.kind_of?(React::Element) && `React.isValidElement(#{element.to_n})`
|
82
|
+
end
|
83
|
+
|
84
|
+
def self.render_to_string(element)
|
85
|
+
%x{ console.error("Warning: `React.render_to_string` is deprecated in favor of `React::Server.render_to_string`."); }
|
86
|
+
if !(`typeof ReactDOMServer === 'undefined'`)
|
87
|
+
React::RenderingContext.build { `ReactDOMServer.renderToString(#{element.to_n})` } # v0.15+
|
88
|
+
else
|
89
|
+
raise "renderToString is not defined. In React >= v15 you must import it with ReactDOMServer"
|
90
|
+
end
|
91
|
+
end
|
92
|
+
|
93
|
+
def self.render_to_static_markup(element)
|
94
|
+
%x{ console.error("Warning: `React.render_to_static_markup` is deprecated in favor of `React::Server.render_to_static_markup`."); }
|
95
|
+
if !(`typeof ReactDOMServer === 'undefined'`)
|
96
|
+
React::RenderingContext.build { `ReactDOMServer.renderToStaticMarkup(#{element.to_n})` } # v0.15+
|
97
|
+
else
|
98
|
+
raise "renderToStaticMarkup is not defined. In React >= v15 you must import it with ReactDOMServer"
|
99
|
+
end
|
100
|
+
end
|
101
|
+
|
102
|
+
def self.unmount_component_at_node(node)
|
103
|
+
if !(`typeof ReactDOM === 'undefined'`)
|
104
|
+
`ReactDOM.unmountComponentAtNode(node.$$class ? node[0] : node)` # v0.15+
|
105
|
+
else
|
106
|
+
raise "unmountComponentAtNode is not defined. In React >= v15 you must import it with ReactDOM"
|
107
|
+
end
|
108
|
+
end
|
109
|
+
|
110
|
+
end
|
@@ -0,0 +1,28 @@
|
|
1
|
+
module React
|
2
|
+
def self.render(element, container)
|
3
|
+
raise "ReactDOM.render is not defined. In React >= v15 you must import it with ReactDOM" if (`typeof ReactDOM === 'undefined'`)
|
4
|
+
|
5
|
+
container = `container.$$class ? container[0] : container`
|
6
|
+
|
7
|
+
if block_given?
|
8
|
+
cb = %x{
|
9
|
+
function(){
|
10
|
+
setTimeout(function(){
|
11
|
+
#{yield}
|
12
|
+
}, 0)
|
13
|
+
}
|
14
|
+
}
|
15
|
+
native = `ReactDOM.render(#{element.to_n}, container, cb)`
|
16
|
+
else
|
17
|
+
native = `ReactDOM.render(#{element.to_n}, container)`
|
18
|
+
end
|
19
|
+
|
20
|
+
if `#{native}.__opalInstance !== undefined && #{native}.__opalInstance !== null`
|
21
|
+
`#{native}.__opalInstance`
|
22
|
+
elsif `ReactDOM.findDOMNode !== undefined && #{native}.nodeType === undefined`
|
23
|
+
`ReactDOM.findDOMNode(#{native})`
|
24
|
+
else
|
25
|
+
native
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
@@ -0,0 +1,132 @@
|
|
1
|
+
module React
|
2
|
+
class Validator
|
3
|
+
attr_accessor :errors
|
4
|
+
attr_reader :props_wrapper
|
5
|
+
private :errors, :props_wrapper
|
6
|
+
|
7
|
+
def initialize(props_wrapper = Class.new(Component::PropsWrapper))
|
8
|
+
@props_wrapper = props_wrapper
|
9
|
+
end
|
10
|
+
|
11
|
+
def self.build(&block)
|
12
|
+
self.new.build(&block)
|
13
|
+
end
|
14
|
+
|
15
|
+
def build(&block)
|
16
|
+
instance_eval(&block)
|
17
|
+
self
|
18
|
+
end
|
19
|
+
|
20
|
+
def requires(name, options = {})
|
21
|
+
options[:required] = true
|
22
|
+
define_rule(name, options)
|
23
|
+
end
|
24
|
+
|
25
|
+
def optional(name, options = {})
|
26
|
+
options[:required] = false
|
27
|
+
define_rule(name, options)
|
28
|
+
end
|
29
|
+
|
30
|
+
def all_other_params(name)
|
31
|
+
@allow_undefined_props = true
|
32
|
+
props_wrapper.define_all_others(name) { |props| props.reject { |name, value| rules[name] } }
|
33
|
+
end
|
34
|
+
|
35
|
+
def validate(props)
|
36
|
+
self.errors = []
|
37
|
+
validate_undefined(props) unless allow_undefined_props?
|
38
|
+
props = coerce_native_hash_values(defined_props(props))
|
39
|
+
validate_required(props)
|
40
|
+
props.each do |name, value|
|
41
|
+
validate_types(name, value)
|
42
|
+
validate_allowed(name, value)
|
43
|
+
end
|
44
|
+
errors
|
45
|
+
end
|
46
|
+
|
47
|
+
def default_props
|
48
|
+
rules
|
49
|
+
.select {|key, value| value.keys.include?("default") }
|
50
|
+
.inject({}) {|memo, (k,v)| memo[k] = v[:default]; memo}
|
51
|
+
end
|
52
|
+
|
53
|
+
private
|
54
|
+
|
55
|
+
def defined_props(props)
|
56
|
+
props.select { |name| rules.keys.include?(name) }
|
57
|
+
end
|
58
|
+
|
59
|
+
def allow_undefined_props?
|
60
|
+
!!@allow_undefined_props
|
61
|
+
end
|
62
|
+
|
63
|
+
def rules
|
64
|
+
@rules ||= { children: { required: false } }
|
65
|
+
end
|
66
|
+
|
67
|
+
def define_rule(name, options = {})
|
68
|
+
rules[name] = coerce_native_hash_values(options)
|
69
|
+
props_wrapper.define_param(name, options[:type])
|
70
|
+
end
|
71
|
+
|
72
|
+
def errors
|
73
|
+
@errors ||= []
|
74
|
+
end
|
75
|
+
|
76
|
+
def validate_types(prop_name, value)
|
77
|
+
return unless klass = rules[prop_name][:type]
|
78
|
+
if !klass.is_a?(Array)
|
79
|
+
allow_nil = !!rules[prop_name][:allow_nil]
|
80
|
+
type_check("`#{prop_name}`", value, klass, allow_nil)
|
81
|
+
elsif klass.length > 0
|
82
|
+
validate_value_array(prop_name, value)
|
83
|
+
else
|
84
|
+
allow_nil = !!rules[prop_name][:allow_nil]
|
85
|
+
type_check("`#{prop_name}`", value, Array, allow_nil)
|
86
|
+
end
|
87
|
+
end
|
88
|
+
|
89
|
+
def type_check(prop_name, value, klass, allow_nil)
|
90
|
+
return if allow_nil && value.nil?
|
91
|
+
return if value.is_a?(klass)
|
92
|
+
return if klass.respond_to?(:_react_param_conversion) &&
|
93
|
+
klass._react_param_conversion(value, :validate_only)
|
94
|
+
errors << "Provided prop #{prop_name} could not be converted to #{klass}"
|
95
|
+
end
|
96
|
+
|
97
|
+
def validate_allowed(prop_name, value)
|
98
|
+
return unless values = rules[prop_name][:values]
|
99
|
+
return if values.include?(value)
|
100
|
+
errors << "Value `#{value}` for prop `#{prop_name}` is not an allowed value"
|
101
|
+
end
|
102
|
+
|
103
|
+
def validate_required(props)
|
104
|
+
(rules.keys - props.keys).each do |name|
|
105
|
+
next unless rules[name][:required]
|
106
|
+
errors << "Required prop `#{name}` was not specified"
|
107
|
+
end
|
108
|
+
end
|
109
|
+
|
110
|
+
def validate_undefined(props)
|
111
|
+
(props.keys - rules.keys).each do |prop_name|
|
112
|
+
errors << "Provided prop `#{prop_name}` not specified in spec"
|
113
|
+
end
|
114
|
+
end
|
115
|
+
|
116
|
+
def validate_value_array(name, value)
|
117
|
+
klass = rules[name][:type]
|
118
|
+
allow_nil = !!rules[name][:allow_nil]
|
119
|
+
value.each_with_index do |item, index|
|
120
|
+
type_check("`#{name}`[#{index}]", Native(item), klass[0], allow_nil)
|
121
|
+
end
|
122
|
+
rescue NoMethodError
|
123
|
+
errors << "Provided prop `#{name}` was not an Array"
|
124
|
+
end
|
125
|
+
|
126
|
+
def coerce_native_hash_values(hash)
|
127
|
+
hash.each do |key, value|
|
128
|
+
hash[key] = Native(value)
|
129
|
+
end
|
130
|
+
end
|
131
|
+
end
|
132
|
+
end
|