lib/react/api.rb
require 'react/native_library'
module React
# Provides the internal mechanisms to interface between reactrb and native components
# the code will attempt to create a js component wrapper on any rb class that has a
# render (or possibly _render_wrapper) method. The mapping between rb and js components
# is kept in the @@component_classes hash.
# Also provides the mechanism to build react elements
# TOOO - the code to deal with components should be moved to a module that will be included
# in a class which will then create the JS component for that class. That module will then
# be included in React::Component, but can be used by any class wanting to become a react
# component (but without other DSL characteristics.)
class API
@@component_classes = {}
def self.import_native_component(opal_class, native_class)
opal_class.instance_variable_set("@native_import", true)
@@component_classes[opal_class] = native_class
end
def self.eval_native_react_component(name)
component = `eval(name)`
raise "#{name} is not defined" if `#{component} === undefined`
is_component_class = `#{component}.prototype !== undefined` &&
(`!!#{component}.prototype.isReactComponent` ||
`!!#{component}.prototype.render`)
is_functional_component = `typeof #{component} === "function"`
is_not_using_react_v13 = `!Opal.global.React.version.match(/0\.13/)`
unless is_component_class || (is_not_using_react_v13 && is_functional_component)
raise 'does not appear to be a native react component'
end
component
end
def self.native_react_component?(name = nil)
return false unless name
eval_native_react_component(name)
rescue
nil
end
def self.create_native_react_class(type)
raise "Provided class should define `render` method" if !(type.method_defined? :render)
render_fn = (type.method_defined? :_render_wrapper) ? :_render_wrapper : :render
# this was hashing type.to_s, not sure why but .to_s does not work as it Foo::Bar::View.to_s just returns "View"
@@component_classes[type] ||= %x{
React.createClass({
displayName: #{type.name},
propTypes: #{type.respond_to?(:prop_types) ? type.prop_types.to_n : `{}`},
getDefaultProps: function(){
return #{type.respond_to?(:default_props) ? type.default_props.to_n : `{}`};
},
mixins: #{type.respond_to?(:native_mixins) ? type.native_mixins : `[]`},
statics: #{type.respond_to?(:static_call_backs) ? type.static_call_backs.to_n : `{}`},
componentWillMount: function() {
var instance = this._getOpalInstance.apply(this);
return #{`instance`.component_will_mount if type.method_defined? :component_will_mount};
},
componentDidMount: function() {
var instance = this._getOpalInstance.apply(this);
return #{`instance`.component_did_mount if type.method_defined? :component_did_mount};
},
componentWillReceiveProps: function(next_props) {
var instance = this._getOpalInstance.apply(this);
return #{`instance`.component_will_receive_props(Hash.new(`next_props`)) if type.method_defined? :component_will_receive_props};
},
shouldComponentUpdate: function(next_props, next_state) {
var instance = this._getOpalInstance.apply(this);
return #{`instance`.should_component_update?(Hash.new(`next_props`), Hash.new(`next_state`)) if type.method_defined? :should_component_update?};
},
componentWillUpdate: function(next_props, next_state) {
var instance = this._getOpalInstance.apply(this);
return #{`instance`.component_will_update(Hash.new(`next_props`), Hash.new(`next_state`)) if type.method_defined? :component_will_update};
},
componentDidUpdate: function(prev_props, prev_state) {
var instance = this._getOpalInstance.apply(this);
return #{`instance`.component_did_update(Hash.new(`prev_props`), Hash.new(`prev_state`)) if type.method_defined? :component_did_update};
},
componentWillUnmount: function() {
var instance = this._getOpalInstance.apply(this);
return #{`instance`.component_will_unmount if type.method_defined? :component_will_unmount};
},
_getOpalInstance: function() {
if (this.__opalInstance == undefined) {
var instance = #{type.new(`this`)};
} else {
var instance = this.__opalInstance;
}
this.__opalInstance = instance;
return instance;
},
render: function() {
var instance = this._getOpalInstance.apply(this);
return #{`instance`.send(render_fn).to_n};
}
})
}
end
def self.create_element(type, properties = {}, &block)
params = []
# Component Spec, Normal DOM, String or Native Component
if @@component_classes[type]
params << @@component_classes[type]
elsif type.kind_of?(Class)
params << create_native_react_class(type)
elsif React::Component::Tags::HTML_TAGS.include?(type)
params << type
elsif type.is_a? String
return React::Element.new(type)
else
raise "#{type} not implemented"
end
# Convert Passed in properties
properties = convert_props(properties)
params << properties.shallow_to_n
# Children Nodes
if block_given?
[yield].flatten.each do |ele|
params << ele.to_n
end
end
React::Element.new(`React.createElement.apply(null, #{params})`, type, properties, block)
end
def self.clear_component_class_cache
@@component_classes = {}
end
def self.convert_props(properties)
raise "Component parameters must be a hash. Instead you sent #{properties}" unless properties.is_a? Hash
props = {}
properties.map do |key, value|
if key == "class_name" && value.is_a?(Hash)
props[lower_camelize(key)] = `React.addons.classSet(#{value.to_n})`
elsif key == "class"
props["className"] = value
elsif ["style", "dangerously_set_inner_HTML"].include? key
props[lower_camelize(key)] = value.to_n
elsif key == 'ref' && value.is_a?(Proc)
unless React.const_defined?(:RefsCallbackExtension)
%x{
console.error(
"Warning: Using deprecated behavior of ref callback,",
"require \"react/ref_callback\" to get the correct behavior."
);
}
end
props[key] = value
elsif React::HASH_ATTRIBUTES.include?(key) && value.is_a?(Hash)
value.each { |k, v| props["#{key}-#{k.tr('_', '-')}"] = v.to_n }
else
props[React.html_attr?(lower_camelize(key)) ? lower_camelize(key) : key] = value
end
end
props
end
private
def self.lower_camelize(snake_cased_word)
words = snake_cased_word.split('_')
result = [words.first]
result.concat(words[1..-1].map {|word| word[0].upcase + word[1..-1] })
result.join('')
end
end
end