require 'hyperstack/component/native_library' module Hyperstack module Internal module Component # contains the name of all HTML tags, and the mechanism to register a component # 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 ReactWrapper @@component_classes = {} def self.stateless?(ncc) `typeof #{ncc} === 'symbol' || (typeof #{ncc} === 'function' && !(#{ncc}.prototype && #{ncc}.prototype.isReactComponent))` end 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` component = `component.default` if `component.__esModule` is_component_class = `#{component}.prototype !== undefined` && (`!!#{component}.prototype.isReactComponent` || `!!#{component}.prototype.render`) has_render_method = `typeof #{component}.render === "function"` unless is_component_class || stateless?(component) || has_render_method 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) true # Exception to be compatible with all versions of opal rescue Exception # rubocop:disable Lint/RescueException false end def self.add_after_error_hook(klass) add_after_error_hook_to_native(@@component_classes[klass]) end def self.add_after_error_hook_to_native(native_comp) return unless native_comp %x{ native_comp.prototype.componentDidCatch = function(error, info) { this.__opalInstanceSyncSetState = false; this.__opalInstance.$component_did_catch(error, Opal.Hash.$new(info)); } } end def self.create_native_react_class(type) raise "createReactClass is undefined. Add the 'react-create-class' npm module, and import it as 'createReactClass'" if `typeof(createReactClass)=='undefined'` raise "Provided class should define `render` method" if !(type.method_defined? :render) old_school = !type.method_defined?(:_render_wrapper) render_fn = old_school ? :render : :_render_wrapper # 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] ||= begin comp = %x{ createReactClass({ getInitialState: function() { this.mixins = #{type.respond_to?(:native_mixins) ? type.native_mixins : `[]`}; this.statics = #{type.respond_to?(:static_call_backs) ? type.static_call_backs.to_n : `{}`}; this.__opalInstanceInitializedState = false; this.__opalInstanceSyncSetState = true; this.__opalInstance = #{type.new(`this`)}; this.__opalInstanceInitializedState = true; this.__opalInstanceSyncSetState = false; this.__name = #{type.name}; return {} }, displayName: #{type.name}, getDefaultProps: function() { return #{type.respond_to?(:default_props) ? type.default_props.to_n : `{}`}; }, propTypes: #{type.respond_to?(:prop_types) ? type.prop_types.to_n : `{}`}, componentWillMount: old_school && function() { if (#{type.method_defined? :component_will_mount}) { this.__opalInstanceSyncSetState = true; this.__opalInstance.$component_will_mount(); this.__opalInstanceSyncSetState = false; } }, componentDidMount: function() { this.__opalInstance.__hyperstack_component_is_mounted = true if (#{type.method_defined? :component_did_mount}) { this.__opalInstanceSyncSetState = false; this.__opalInstance.$component_did_mount(); } }, UNSAFE_componentWillReceiveProps: function(next_props) { if (#{type.method_defined? :component_will_receive_props}) { this.__opalInstanceSyncSetState = true; this.__opalInstance.$component_will_receive_props(Opal.Hash.$new(next_props)); this.__opalInstanceSyncSetState = false; } }, shouldComponentUpdate: function(next_props, next_state) { if (#{type.method_defined? :should_component_update?}) { this.__opalInstanceSyncSetState = false; return this.__opalInstance["$should_component_update?"](Opal.Hash.$new(next_props), Opal.Hash.$new(next_state)); } else { return true; } }, UNSAFE_componentWillUpdate: function(next_props, next_state) { if (#{type.method_defined? :component_will_update}) { this.__opalInstanceSyncSetState = false; this.__opalInstance.$component_will_update(Opal.Hash.$new(next_props), Opal.Hash.$new(next_state)); } }, componentDidUpdate: function(prev_props, prev_state) { if (#{type.method_defined? :component_did_update}) { this.__opalInstanceSyncSetState = false; this.__opalInstance.$component_did_update(Opal.Hash.$new(prev_props), Opal.Hash.$new(prev_state)); } }, componentWillUnmount: function() { if (#{type.method_defined? :component_will_unmount}) { this.__opalInstanceSyncSetState = false; this.__opalInstance.$component_will_unmount(); } this.__opalInstance.__hyperstack_component_is_mounted = false; }, render: function() { this.__opalInstanceSyncSetState = false; return this.__opalInstance.$send(render_fn).$to_n(); } }) } # check to see if there is an after_error callback. If there is add a # componentDidCatch handler. Because legacy behavior is to allow any object # that responds to render to act as a component we have to make sure that # we have a callbacks_for method. This all becomes much easier once issue # #270 is resolved. if type.respond_to?(:callbacks?) && type.callbacks?(:after_error) add_after_error_hook_to_native comp end comp end end def self.create_element(type, *args, &block) params = [] # Component Spec, Normal DOM, String or Native Component ncc = @@component_classes[type] if ncc params << ncc elsif type.is_a?(Class) params << create_native_react_class(type) elsif block_given? || Tags::HTML_TAGS.include?(type) params << type elsif type.is_a?(String) return Hyperstack::Component::Element.new(type) else raise "#{type} not implemented" end # Convert Passed in properties ele = nil # create nil var for the ref to use ref = ->(ref) { ele._update_ref(ref) } unless stateless?(ncc) properties = convert_props(type, { ref: ref }, *args) params << properties.shallow_to_n # Children Nodes if block a = [block.call].flatten %x{ for(var i=0, l=a.length; i '_', '_' => '-')}"] = v.to_n } else props[Hyperstack::Component::ReactAPI.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] }).join('') end end end end end