reactrb/reactrb

View on GitHub
lib/react/api.rb

Summary

Maintainability
B
6 hrs
Test Coverage
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