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
data/lib/react/api.rb
ADDED
@@ -0,0 +1,95 @@
|
|
1
|
+
module React
|
2
|
+
class API
|
3
|
+
@@component_classes = {}
|
4
|
+
|
5
|
+
def self.create_element(type, properties = {}, &block)
|
6
|
+
params = []
|
7
|
+
|
8
|
+
# Component Spec or Nomral DOM
|
9
|
+
if type.kind_of?(Class)
|
10
|
+
raise "Provided class should define `render` method" if !(type.method_defined? :render)
|
11
|
+
@@component_classes[type.to_s] ||= %x{
|
12
|
+
React.createClass({
|
13
|
+
propTypes: #{type.respond_to?(:prop_types) ? type.prop_types.to_n : `{}`},
|
14
|
+
getDefaultProps: function(){
|
15
|
+
return #{type.respond_to?(:default_props) ? type.default_props.to_n : `{}`};
|
16
|
+
},
|
17
|
+
getInitialState: function(){
|
18
|
+
return #{type.respond_to?(:initial_state) ? type.initial_state.to_n : `{}`};
|
19
|
+
},
|
20
|
+
componentWillMount: function() {
|
21
|
+
instance = #{type.new(`this`)};
|
22
|
+
return #{`instance`.component_will_mount if type.method_defined? :component_will_mount};
|
23
|
+
},
|
24
|
+
componentDidMount: function() {
|
25
|
+
instance = #{type.new(`this`)};
|
26
|
+
return #{`instance`.component_did_mount if type.method_defined? :component_did_mount};
|
27
|
+
},
|
28
|
+
componentWillReceiveProps: function(next_props) {
|
29
|
+
instance = #{type.new(`this`)};
|
30
|
+
return #{`instance`.component_will_receive_props(`next_props`) if type.method_defined? :component_will_receive_props};
|
31
|
+
},
|
32
|
+
shouldComponentUpdate: function(next_props, next_state) {
|
33
|
+
instance = #{type.new(`this`)};
|
34
|
+
return #{`instance`.should_component_update?(`next_props`, `next_state`) if type.method_defined? :should_component_update?};
|
35
|
+
},
|
36
|
+
componentWillUpdate: function(next_props, next_state) {
|
37
|
+
instance = #{type.new(`this`)};
|
38
|
+
return #{`instance`.component_will_update(`next_props`, `next_state`) if type.method_defined? :component_will_update};
|
39
|
+
},
|
40
|
+
componentDidUpdate: function(prev_props, prev_state) {
|
41
|
+
instance = #{type.new(`this`)};
|
42
|
+
return #{`instance`.component_did_update(`prev_props`, `prev_state`) if type.method_defined? :component_did_update};
|
43
|
+
},
|
44
|
+
componentWillUnmount: function() {
|
45
|
+
instance = #{type.new(`this`)};
|
46
|
+
return #{`instance`.component_will_unmount if type.method_defined? :component_will_unmount};
|
47
|
+
},
|
48
|
+
render: function() {
|
49
|
+
instance = #{type.new(`this`)};
|
50
|
+
return #{`instance`.render.to_n};
|
51
|
+
}
|
52
|
+
})
|
53
|
+
}
|
54
|
+
|
55
|
+
params << @@component_classes[type.to_s]
|
56
|
+
else
|
57
|
+
raise "#{type} not implemented" unless HTML_TAGS.include?(type)
|
58
|
+
params << type
|
59
|
+
end
|
60
|
+
|
61
|
+
# Passed in properties
|
62
|
+
props = {}
|
63
|
+
properties.map do |key, value|
|
64
|
+
if key == "class_name" && value.is_a?(Hash)
|
65
|
+
props[lower_camelize(key)] = `React.addons.classSet(#{value.to_n})`
|
66
|
+
else
|
67
|
+
props[React::ATTRIBUTES.include?(lower_camelize(key)) ? lower_camelize(key) : key] = value
|
68
|
+
end
|
69
|
+
end
|
70
|
+
params << props.shallow_to_n
|
71
|
+
|
72
|
+
# Children Nodes
|
73
|
+
if block_given?
|
74
|
+
children = [yield].flatten.each do |ele|
|
75
|
+
params << ele.to_n
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
return React::Element.new(`React.createElement.apply(null, #{params})`)
|
80
|
+
end
|
81
|
+
|
82
|
+
def self.clear_component_class_cache
|
83
|
+
@@component_classes = {}
|
84
|
+
end
|
85
|
+
|
86
|
+
private
|
87
|
+
|
88
|
+
def self.lower_camelize(snake_cased_word)
|
89
|
+
words = snake_cased_word.split("_")
|
90
|
+
result = [words.first]
|
91
|
+
result.concat(words[1..-1].map {|word| word[0].upcase + word[1..-1] })
|
92
|
+
result.join("")
|
93
|
+
end
|
94
|
+
end
|
95
|
+
end
|
@@ -0,0 +1,35 @@
|
|
1
|
+
require 'active_support/core_ext/class/attribute'
|
2
|
+
|
3
|
+
module React
|
4
|
+
module Callbacks
|
5
|
+
def self.included(base)
|
6
|
+
base.extend(ClassMethods)
|
7
|
+
end
|
8
|
+
|
9
|
+
def run_callback(name, *args)
|
10
|
+
attribute_name = "_#{name}_callbacks"
|
11
|
+
callbacks = self.class.send(attribute_name)
|
12
|
+
callbacks.each do |callback|
|
13
|
+
if callback.is_a?(Proc)
|
14
|
+
instance_exec(*args, &callback)
|
15
|
+
else
|
16
|
+
send(callback, *args)
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
module ClassMethods
|
22
|
+
def define_callback(callback_name)
|
23
|
+
attribute_name = "_#{callback_name}_callbacks"
|
24
|
+
class_attribute(attribute_name)
|
25
|
+
self.send("#{attribute_name}=", [])
|
26
|
+
define_singleton_method(callback_name) do |*args, &block|
|
27
|
+
callbacks = self.send(attribute_name)
|
28
|
+
callbacks.concat(args)
|
29
|
+
callbacks.push(block) if block_given?
|
30
|
+
self.send("#{attribute_name}=", callbacks)
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
@@ -0,0 +1,197 @@
|
|
1
|
+
require "./ext/string"
|
2
|
+
require 'active_support/core_ext/class/attribute'
|
3
|
+
require 'react/callbacks'
|
4
|
+
require "react/ext/hash"
|
5
|
+
|
6
|
+
module React
|
7
|
+
module Component
|
8
|
+
def self.included(base)
|
9
|
+
base.include(API)
|
10
|
+
base.include(React::Callbacks)
|
11
|
+
base.class_eval do
|
12
|
+
class_attribute :init_state, :validator
|
13
|
+
define_callback :before_mount
|
14
|
+
define_callback :after_mount
|
15
|
+
define_callback :before_receive_props
|
16
|
+
define_callback :before_update
|
17
|
+
define_callback :after_update
|
18
|
+
define_callback :before_unmount
|
19
|
+
end
|
20
|
+
base.extend(ClassMethods)
|
21
|
+
end
|
22
|
+
|
23
|
+
def initialize(native_element)
|
24
|
+
@native = native_element
|
25
|
+
end
|
26
|
+
|
27
|
+
def params
|
28
|
+
Hash.new(`#{@native}.props`)
|
29
|
+
end
|
30
|
+
|
31
|
+
def refs
|
32
|
+
Hash.new(`#{@native}.refs`)
|
33
|
+
end
|
34
|
+
|
35
|
+
def state
|
36
|
+
raise "No native ReactComponent associated" unless @native
|
37
|
+
Hash.new(`#{@native}.state`)
|
38
|
+
end
|
39
|
+
|
40
|
+
def emit(event_name, *args)
|
41
|
+
self.params["_on#{event_name.to_s.event_camelize}"].call(*args)
|
42
|
+
end
|
43
|
+
|
44
|
+
def component_will_mount
|
45
|
+
self.run_callback(:before_mount)
|
46
|
+
end
|
47
|
+
|
48
|
+
def component_did_mount
|
49
|
+
self.run_callback(:after_mount)
|
50
|
+
end
|
51
|
+
|
52
|
+
def component_will_receive_props(next_props)
|
53
|
+
self.run_callback(:before_receive_props, Hash.new(next_props))
|
54
|
+
end
|
55
|
+
|
56
|
+
def should_component_update?(next_props, next_state)
|
57
|
+
self.respond_to?(:needs_update?) ? self.needs_update?(Hash.new(next_props), Hash.new(next_state)) : true
|
58
|
+
end
|
59
|
+
|
60
|
+
def component_will_update(next_props, next_state)
|
61
|
+
self.run_callback(:before_update, Hash.new(next_props), Hash.new(next_state))
|
62
|
+
end
|
63
|
+
|
64
|
+
def component_did_update(prev_props, prev_state)
|
65
|
+
self.run_callback(:after_update, Hash.new(prev_props), Hash.new(prev_state))
|
66
|
+
end
|
67
|
+
|
68
|
+
def component_will_unmount
|
69
|
+
self.run_callback(:before_unmount)
|
70
|
+
end
|
71
|
+
|
72
|
+
def method_missing(name, *args, &block)
|
73
|
+
unless (React::HTML_TAGS.include?(name) || name == 'present')
|
74
|
+
return super
|
75
|
+
end
|
76
|
+
|
77
|
+
if name == "present"
|
78
|
+
name = args.shift
|
79
|
+
end
|
80
|
+
|
81
|
+
@buffer = [] unless @buffer
|
82
|
+
if block
|
83
|
+
current = @buffer
|
84
|
+
@buffer = []
|
85
|
+
result = block.call
|
86
|
+
element = React.create_element(name, *args) { @buffer.count == 0 ? result : @buffer }
|
87
|
+
@buffer = current
|
88
|
+
else
|
89
|
+
element = React.create_element(name, *args)
|
90
|
+
end
|
91
|
+
|
92
|
+
@buffer << element
|
93
|
+
element
|
94
|
+
end
|
95
|
+
|
96
|
+
|
97
|
+
module ClassMethods
|
98
|
+
def prop_types
|
99
|
+
if self.validator
|
100
|
+
{
|
101
|
+
_componentValidator: %x{
|
102
|
+
function(props, propName, componentName) {
|
103
|
+
var errors = #{validator.validate(Hash.new(`props`))};
|
104
|
+
var error = new Error(#{"In component `" + self.name + "`\n" + `errors`.join("\n")});
|
105
|
+
return #{`errors`.count > 0 ? `error` : `undefined`};
|
106
|
+
}
|
107
|
+
}
|
108
|
+
}
|
109
|
+
else
|
110
|
+
{}
|
111
|
+
end
|
112
|
+
end
|
113
|
+
|
114
|
+
def initial_state
|
115
|
+
self.init_state || {}
|
116
|
+
end
|
117
|
+
|
118
|
+
def default_props
|
119
|
+
self.validator ? self.validator.default_props : {}
|
120
|
+
end
|
121
|
+
|
122
|
+
def params(&block)
|
123
|
+
self.validator = React::Validator.build(&block)
|
124
|
+
end
|
125
|
+
|
126
|
+
def define_state(*states)
|
127
|
+
raise "Block could be only given when define exactly one state" if block_given? && states.count > 1
|
128
|
+
|
129
|
+
self.init_state = {} unless self.init_state
|
130
|
+
|
131
|
+
if block_given?
|
132
|
+
self.init_state[states[0]] = yield
|
133
|
+
end
|
134
|
+
states.each do |name|
|
135
|
+
# getter
|
136
|
+
define_method("#{name}") do
|
137
|
+
return unless @native
|
138
|
+
self.state[name]
|
139
|
+
end
|
140
|
+
# setter
|
141
|
+
define_method("#{name}=") do |new_state|
|
142
|
+
return unless @native
|
143
|
+
hash = {}
|
144
|
+
hash[name] = new_state
|
145
|
+
self.set_state(hash)
|
146
|
+
|
147
|
+
new_state
|
148
|
+
end
|
149
|
+
end
|
150
|
+
end
|
151
|
+
end
|
152
|
+
|
153
|
+
module API
|
154
|
+
include Native
|
155
|
+
|
156
|
+
alias_native :dom_node, :getDOMNode
|
157
|
+
alias_native :mounted?, :isMounted
|
158
|
+
alias_native :force_update!, :forceUpdate
|
159
|
+
|
160
|
+
def set_props(prop, &block)
|
161
|
+
raise "No native ReactComponent associated" unless @native
|
162
|
+
%x{
|
163
|
+
#{@native}.setProps(#{prop.shallow_to_n}, function(){
|
164
|
+
#{block.call if block}
|
165
|
+
});
|
166
|
+
}
|
167
|
+
end
|
168
|
+
|
169
|
+
def set_props!(prop, &block)
|
170
|
+
raise "No native ReactComponent associated" unless @native
|
171
|
+
%x{
|
172
|
+
#{@native}.replaceProps(#{prop.shallow_to_n}, function(){
|
173
|
+
#{block.call if block}
|
174
|
+
});
|
175
|
+
}
|
176
|
+
end
|
177
|
+
|
178
|
+
def set_state(state, &block)
|
179
|
+
raise "No native ReactComponent associated" unless @native
|
180
|
+
%x{
|
181
|
+
#{@native}.setState(#{state.shallow_to_n}, function(){
|
182
|
+
#{block.call if block}
|
183
|
+
});
|
184
|
+
}
|
185
|
+
end
|
186
|
+
|
187
|
+
def set_state!(state, &block)
|
188
|
+
raise "No native ReactComponent associated" unless @native
|
189
|
+
%x{
|
190
|
+
#{@native}.replaceState(#{state.shallow_to_n}, function(){
|
191
|
+
#{block.call if block}
|
192
|
+
});
|
193
|
+
}
|
194
|
+
end
|
195
|
+
end
|
196
|
+
end
|
197
|
+
end
|
@@ -0,0 +1,63 @@
|
|
1
|
+
require "./ext/string"
|
2
|
+
|
3
|
+
module React
|
4
|
+
class Element
|
5
|
+
include Native
|
6
|
+
|
7
|
+
alias_native :element_type, :type
|
8
|
+
alias_native :props, :props
|
9
|
+
|
10
|
+
def initialize(native_element)
|
11
|
+
@native = native_element
|
12
|
+
end
|
13
|
+
|
14
|
+
def on(event_name)
|
15
|
+
name = event_name.to_s.event_camelize
|
16
|
+
if React::Event::BUILT_IN_EVENTS.include?("on#{name}")
|
17
|
+
self.props["on#{name}"] = %x{
|
18
|
+
function(event){
|
19
|
+
#{yield React::Event.new(`event`)}
|
20
|
+
}
|
21
|
+
}
|
22
|
+
else
|
23
|
+
self.props["_on#{name}"] = %x{
|
24
|
+
function(){
|
25
|
+
#{yield *Array(`arguments`)}
|
26
|
+
}
|
27
|
+
}
|
28
|
+
end
|
29
|
+
self
|
30
|
+
end
|
31
|
+
|
32
|
+
def children
|
33
|
+
nodes = self.props.children
|
34
|
+
class << nodes
|
35
|
+
include Enumerable
|
36
|
+
|
37
|
+
def to_n
|
38
|
+
self
|
39
|
+
end
|
40
|
+
|
41
|
+
def each(&block)
|
42
|
+
if block_given?
|
43
|
+
%x{
|
44
|
+
React.Children.forEach(#{self.to_n}, function(context){
|
45
|
+
#{block.call(React::Element.new(`context`))}
|
46
|
+
})
|
47
|
+
}
|
48
|
+
else
|
49
|
+
Enumerator.new(`React.Children.count(#{self.to_n})`) do |y|
|
50
|
+
%x{
|
51
|
+
React.Children.forEach(#{self.to_n}, function(context){
|
52
|
+
#{y << React::Element.new(`context`)}
|
53
|
+
})
|
54
|
+
}
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
nodes
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
data/lib/react/event.rb
ADDED
@@ -0,0 +1,76 @@
|
|
1
|
+
module React
|
2
|
+
class Event
|
3
|
+
include Native
|
4
|
+
alias_native :bubbles, :bubbles
|
5
|
+
alias_native :cancelable, :cancelable
|
6
|
+
alias_native :current_target, :currentTarget
|
7
|
+
alias_native :default_prevented, :defaultPrevented
|
8
|
+
alias_native :event_phase, :eventPhase
|
9
|
+
alias_native :is_trusted?, :isTrusted
|
10
|
+
alias_native :native_event, :nativeEvent
|
11
|
+
alias_native :target, :target
|
12
|
+
alias_native :timestamp, :timeStamp
|
13
|
+
alias_native :event_type, :type
|
14
|
+
alias_native :prevent_default, :preventDefault
|
15
|
+
alias_native :stop_propagation, :stopPropagation
|
16
|
+
# Clipboard
|
17
|
+
alias_native :clipboard_data, :clipboardData
|
18
|
+
# Keyboard
|
19
|
+
alias_native :alt_key, :altKey
|
20
|
+
alias_native :char_code, :charCode
|
21
|
+
alias_native :ctrl_key, :ctrlKey
|
22
|
+
alias_native :get_modifier_state, :getModifierState
|
23
|
+
alias_native :key, :key
|
24
|
+
alias_native :key_code, :keyCode
|
25
|
+
alias_native :locale, :locale
|
26
|
+
alias_native :location, :location
|
27
|
+
alias_native :meta_key, :metaKey
|
28
|
+
alias_native :repeat, :repeat
|
29
|
+
alias_native :shift_key, :shiftKey
|
30
|
+
alias_native :which, :which
|
31
|
+
# Focus
|
32
|
+
alias_native :related_target, :relatedTarget
|
33
|
+
# Mouse
|
34
|
+
alias_native :alt_key, :altKey
|
35
|
+
alias_native :button, :button
|
36
|
+
alias_native :buttons, :buttons
|
37
|
+
alias_native :client_x, :clientX
|
38
|
+
alias_native :client_y, :clientY
|
39
|
+
alias_native :ctrl_key, :ctrlKey
|
40
|
+
alias_native :get_modifier_state, :getModifierState
|
41
|
+
alias_native :meta_key, :metaKey
|
42
|
+
alias_native :page_x, :pageX
|
43
|
+
alias_native :page_y, :pageY
|
44
|
+
alias_native :related_target, :relatedTarget
|
45
|
+
alias_native :screen_x, :screen_x
|
46
|
+
alias_native :screen_y, :screen_y
|
47
|
+
alias_native :shift_key, :shift_key
|
48
|
+
# Touch
|
49
|
+
alias_native :alt_key, :altKey
|
50
|
+
alias_native :changed_touches, :changedTouches
|
51
|
+
alias_native :ctrl_key, :ctrlKey
|
52
|
+
alias_native :get_modifier_state, :getModifierState
|
53
|
+
alias_native :meta_key, :metaKey
|
54
|
+
alias_native :shift_key, :shiftKey
|
55
|
+
alias_native :target_touches, :targetTouches
|
56
|
+
alias_native :touches, :touches
|
57
|
+
# UI
|
58
|
+
alias_native :detail, :detail
|
59
|
+
alias_native :view, :view
|
60
|
+
# Wheel
|
61
|
+
alias_native :delta_mode, :deltaMode
|
62
|
+
alias_native :delta_x, :deltaX
|
63
|
+
alias_native :delta_y, :deltaY
|
64
|
+
alias_native :delta_z, :deltaZ
|
65
|
+
|
66
|
+
BUILT_IN_EVENTS = %w{onCopy onCut onPaste onKeyDown onKeyPress onKeyUp
|
67
|
+
onFocus onBlur onChange onInput onSubmit onClick onDoubleClick onDrag
|
68
|
+
onDragEnd onDragEnter onDragExit onDragLeave onDragOver onDragStart onDrop
|
69
|
+
onMouseDown onMouseEnter onMouseLeave onMouseMove onMouseOut onMouseOver
|
70
|
+
onMouseUp onTouchCancel onTouchEnd onTouchMove onTouchStart onScroll}
|
71
|
+
|
72
|
+
def initialize(native_element)
|
73
|
+
@native = native_element
|
74
|
+
end
|
75
|
+
end
|
76
|
+
end
|