diff --git a/Gemfile b/Gemfile index 41134fb..2c2b60a 100644 --- a/Gemfile +++ b/Gemfile @@ -1,3 +1,4 @@ source 'https://rubygems.org' gem "opal-jquery", git: "https://github.com/opal/opal-jquery.git", branch: "master" +gem 'hyperloop-config', git: 'https://github.com/ruby-hyperloop/hyperloop-config.git', branch: 'edge' gemspec diff --git a/lib/hyper-react.rb b/lib/hyper-react.rb index 59c1693..8b29d80 100644 --- a/lib/hyper-react.rb +++ b/lib/hyper-react.rb @@ -11,24 +11,25 @@ module Hyperloop class Component end end - require 'react/top_level' - require 'react/top_level_render' + require 'native' require 'react/observable' require 'react/validator' + require 'react/element' + require 'react/api' require 'react/component' require 'react/component/dsl_instance_methods' require 'react/component/should_component_update' require 'react/component/tags' require 'react/component/base' - require 'react/element' require 'react/event' - require 'react/api' require 'react/rendering_context' require 'react/state' require 'react/object' require 'react/to_key' require 'react/ext/opal-jquery/element' require 'reactive-ruby/isomorphic_helpers' + require 'react/top_level' + require 'react/top_level_render' require 'rails-helpers/top_level_rails_component' require 'reactive-ruby/version' module Hyperloop diff --git a/lib/react/api.rb b/lib/react/api.rb index 2667a7f..74befbd 100644 --- a/lib/react/api.rb +++ b/lib/react/api.rb @@ -44,94 +44,106 @@ 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{ - class extends React.Component { - constructor(props) { - super(props); - 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.state = {}; - this.__opalInstanceInitializedState = false; - this.__opalInstanceSyncSetState = true; - this.__opalInstance = #{type.new(`this`)}; - this.__opalInstanceInitializedState = true; - this.__opalInstanceSyncSetState = false; - this.__name = #{type.name}; - } - static get displayName() { - if (typeof this.__name != "undefined") { - return this.__name; - } else { - return #{type.name}; - } - } - static set displayName(name) { - this.__name = name; - } - static get defaultProps() { - return #{type.respond_to?(:default_props) ? type.default_props.to_n : `{}`}; - } - static get propTypes() { - return #{type.respond_to?(:prop_types) ? type.prop_types.to_n : `{}`}; - } - componentWillMount() { - if (#{type.method_defined? :component_will_mount}) { + + @@component_classes[type] ||= begin + comp = %x{ + class extends React.Component { + constructor(props) { + super(props); + 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.state = {}; + this.__opalInstanceInitializedState = false; this.__opalInstanceSyncSetState = true; - this.__opalInstance.$component_will_mount(); + this.__opalInstance = #{type.new(`this`)}; + this.__opalInstanceInitializedState = true; this.__opalInstanceSyncSetState = false; + this.__name = #{type.name}; } - } - componentDidMount() { - this.__opalInstance.is_mounted = true - if (#{type.method_defined? :component_did_mount}) { - this.__opalInstanceSyncSetState = false; - this.__opalInstance.$component_did_mount(); + static get displayName() { + if (typeof this.__name != "undefined") { + return this.__name; + } else { + return #{type.name}; + } } - } - componentWillReceiveProps(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; + static set displayName(name) { + this.__name = name; } - } - shouldComponentUpdate(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; } - } - componentWillUpdate(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)); + static get defaultProps() { + return #{type.respond_to?(:default_props) ? type.default_props.to_n : `{}`}; } - } - componentDidUpdate(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)); + static get propTypes() { + return #{type.respond_to?(:prop_types) ? type.prop_types.to_n : `{}`}; } - } - componentWillUnmount() { - if (#{type.method_defined? :component_will_unmount}) { + componentWillMount() { + if (#{type.method_defined? :component_will_mount}) { + this.__opalInstanceSyncSetState = true; + this.__opalInstance.$component_will_mount(); + this.__opalInstanceSyncSetState = false; + } + } + componentDidMount() { + this.__opalInstance.is_mounted = true + if (#{type.method_defined? :component_did_mount}) { + this.__opalInstanceSyncSetState = false; + this.__opalInstance.$component_did_mount(); + } + } + componentWillReceiveProps(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(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; } + } + componentWillUpdate(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(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() { + if (#{type.method_defined? :component_will_unmount}) { + this.__opalInstanceSyncSetState = false; + this.__opalInstance.$component_will_unmount(); + } + this.__opalInstance.is_mounted = false; + } + + render() { this.__opalInstanceSyncSetState = false; - this.__opalInstance.$component_will_unmount(); + return this.__opalInstance.$send(render_fn).$to_n(); } - this.__opalInstance.is_mounted = false; } - componentDidCatch(error, info) { - if (#{type.method_defined? :component_did_catch}) { + } + # 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_for) && type.callbacks_for(:after_error) != [] + %x{ + comp.prototype.componentDidCatch = function(error, info) { this.__opalInstanceSyncSetState = false; this.__opalInstance.$component_did_catch(error, Opal.Hash.$new(info)); } } - render() { - this.__opalInstanceSyncSetState = false; - return this.__opalInstance.$send(render_fn).$to_n(); - } - } - } + end + comp + end end def self.create_element(type, properties = {}, &block) @@ -182,7 +194,7 @@ def self.convert_props(properties) elsif key == "key" props["key"] = value.to_key - + elsif key == 'ref' && value.is_a?(Proc) props[key] = %x{ function(dom_node){ diff --git a/lib/react/children.rb b/lib/react/children.rb index d6edb02..503e37a 100644 --- a/lib/react/children.rb +++ b/lib/react/children.rb @@ -6,6 +6,14 @@ def initialize(children) @children = children end + def render + each(&:render) + end + + def to_proc + -> () { render } + end + def each(&block) return to_enum(__callee__) { length } unless block_given? return [] unless length > 0 diff --git a/lib/react/component.rb b/lib/react/component.rb index 9430565..0b324b6 100644 --- a/lib/react/component.rb +++ b/lib/react/component.rb @@ -8,7 +8,6 @@ require 'react/component/api' require 'react/component/class_methods' require 'react/component/props_wrapper' -require 'native' module Hyperloop class Component @@ -70,10 +69,13 @@ def component_will_receive_props(next_props) # need to rethink how this works in opal-react, or if its actually that useful within the react.rb environment # for now we are just using it to clear processed_params React::State.set_state_context_to(self) { self.run_callback(:before_receive_props, next_props) } + @_receiving_props = true end def component_will_update(next_props, next_state) React::State.set_state_context_to(self) { self.run_callback(:before_update, next_props, next_state) } + params._reset_all_others_cache if @_receiving_props + @_receiving_props = false end def component_did_update(prev_props, prev_state) @@ -92,15 +94,7 @@ def component_will_unmount def component_did_catch(error, info) React::State.set_state_context_to(self) do - if self.class.callbacks_for(:after_error) == [] - if `typeof error.$backtrace === "function"` - `console.error(error.$backtrace().$join("\n"))` - else - `console.error(error, info)` - end - else - self.run_callback(:after_error, error, info) - end + self.run_callback(:after_error, error, info) end end @@ -109,7 +103,7 @@ def component_did_catch(error, info) def update_react_js_state(object, name, value) if object name = "#{object.class}.#{name}" unless object == self - # Date.now() has only millisecond precision, if several notifications of + # Date.now() has only millisecond precision, if several notifications of # observer happen within a millisecond, updates may get lost. # to mitigate this the Math.random() appends some random number # this way notifactions will happen as expected by the rest of hyperloop @@ -125,7 +119,7 @@ def update_react_js_state(object, name, value) def set_state_synchronously? @native.JS[:__opalInstanceSyncSetState] end - + def render raise 'no render defined' end unless method_defined?(:render) diff --git a/lib/react/component/class_methods.rb b/lib/react/component/class_methods.rb index 9993906..1df14c5 100644 --- a/lib/react/component/class_methods.rb +++ b/lib/react/component/class_methods.rb @@ -92,16 +92,7 @@ def param(*args) end def collect_other_params_as(name) - validator.allow_undefined_props = true - validator_in_lexical_scope = validator - props_wrapper.define_method(name) do - @_all_others ||= validator_in_lexical_scope.undefined_props(props) - end - - validator_in_lexial_scope = validator - props_wrapper.define_method(name) do - @_all_others ||= validator_in_lexial_scope.undefined_props(props) - end + validator.all_other_params(name) { props } end def define_state(*states, &block) @@ -173,7 +164,7 @@ def add_item_to_tree(current_tree, new_item) end end - def to_n + def to_n React::API.class_eval('@@component_classes')[self] end end diff --git a/lib/react/component/props_wrapper.rb b/lib/react/component/props_wrapper.rb index 0227a71..529dea2 100644 --- a/lib/react/component/props_wrapper.rb +++ b/lib/react/component/props_wrapper.rb @@ -44,6 +44,13 @@ def self.define_param(name, param_type) end end + def self.define_all_others(name) + define_method("#{name}") do + @_all_others_cache ||= yield(props) + end + end + + def initialize(component) @component = component end @@ -52,6 +59,11 @@ def [](prop) props[prop] end + + def _reset_all_others_cache + @_all_others_cache = nil + end + private def fetch_from_cache(name) diff --git a/lib/react/component/tags.rb b/lib/react/component/tags.rb index 92e8ade..4b82b9f 100644 --- a/lib/react/component/tags.rb +++ b/lib/react/component/tags.rb @@ -20,8 +20,10 @@ def present(component, *params, &children) React::RenderingContext.render(component, *params, &children) end - # define each predefined tag as an instance method + # define each predefined tag (downcase) as an instance method (deprecated) and as a component (upcase) HTML_TAGS.each do |tag| + + # deprecated - remove if tag == 'p' define_method(tag) do |*params, &children| if children || params.count == 0 || (params.count == 1 && params.first.is_a?(Hash)) @@ -35,8 +37,23 @@ def present(component, *params, &children) React::RenderingContext.render(tag, *params, &children) end end - alias_method tag.upcase, tag - const_set tag.upcase, tag + + # new style: allows custom hooks to be added and/or the render method to + # be modified. i.e. see how hyper-mesh deals with defaultValues in input tags + + klass = Class.new(Hyperloop::Component) do + # its complicated but the automatic inclusion of the Mixin is setup after all + # the files are loaded, so at this point we have to manually load it. + include Hyperloop::Component::Mixin + collect_other_params_as :opts + # we simply pass along all the params and children with the tag string name + render { React::RenderingContext.render(tag, params.opts, &children) } + + # after_error do |error, info| + # raise error + # end + end + const_set(tag.upcase, klass) end def self.html_tag_class_for(tag) diff --git a/lib/react/validator.rb b/lib/react/validator.rb index 805d2a8..7bba316 100644 --- a/lib/react/validator.rb +++ b/lib/react/validator.rb @@ -27,13 +27,9 @@ def optional(name, options = {}) define_rule(name, options) end - def allow_undefined_props=(allow) - @allow_undefined_props = allow - end - - def undefined_props(props) - self.allow_undefined_props = true - props.reject { |name, value| rules[name] } + def all_other_params(name) + @allow_undefined_props = true + props_wrapper.define_all_others(name) { |props| props.reject { |name, value| rules[name] } } end def validate(props) diff --git a/spec/react/children_spec.rb b/spec/react/children_spec.rb index ed0428c..0175c0d 100644 --- a/spec/react/children_spec.rb +++ b/spec/react/children_spec.rb @@ -129,4 +129,41 @@ def render end end end + + describe 'other methods' do + it 'responds to to_proc' do + mount 'Children' do + class ChildTester < Hyperloop::Component + render do + DIV(id: :tp, &children) + end + end + class Children < Hyperloop::Component + render do + ChildTester { "one".span; "two".span; "three".span } + end + end + end + expect(page).to have_content('one') + expect(page).to have_content('two') + expect(page).to have_content('three') + end + it 'responds to render' do + mount 'Children' do + class ChildTester < Hyperloop::Component + render do + DIV(id: :tp) { children.render } + end + end + class Children < Hyperloop::Component + render do + ChildTester { "one".span; "two".span; "three".span } + end + end + end + expect(page).to have_content('one') + expect(page).to have_content('two') + expect(page).to have_content('three') + end + end end diff --git a/spec/react/component_spec.rb b/spec/react/component_spec.rb index 5c1b5a6..e69d20a 100644 --- a/spec/react/component_spec.rb +++ b/spec/react/component_spec.rb @@ -132,7 +132,7 @@ def render end end expect_evaluate_ruby('Foo.get_error').to eq('ErrorFoo Error') - expect_evaluate_ruby('Foo.get_info').to eq("\n in ErrorFoo\n in div\n in Foo\n in React::TopLevelRailsComponent") + expect_evaluate_ruby('Foo.get_info').to eq("\n in ErrorFoo\n in div\n in React::Component::Tags::DIV\n in Foo\n in React::TopLevelRailsComponent") end end @@ -719,7 +719,7 @@ def render; Hash.new; end end end expect(page.driver.browser.manage.logs.get(:browser).map { |m| m.message.gsub(/\\n/, "\n") }.to_a.join("\n")) - .to match(/Instead the Hash \{\} was returned/) + .to match(/You may need to convert this to a string./) end it "will generate a message if render returns a Component class" do mount 'Foo' do @@ -737,7 +737,7 @@ def render; "hello".span; "goodby".span; end end end expect(page.driver.browser.manage.logs.get(:browser).map { |m| m.message.gsub(/\\n/, "\n") }.to_a.join("\n")) - .to match(/Instead 2 elements were generated/) + .to match(/Do you want to wrap your elements in a div?/) end it "will generate a message if the element generated is not the element returned" do mount 'Foo' do @@ -746,7 +746,7 @@ def render; "hello".span; "goodby".span.delete; end end end expect(page.driver.browser.manage.logs.get(:browser).map { |m| m.message.gsub(/\\n/, "\n") }.to_a.join("\n")) - .to match(/A different element was returned than was generated within the DSL/) + .to match(/Possibly improper use of Element#delete./) end end diff --git a/spec/react/native_library_spec.rb b/spec/react/native_library_spec.rb index 1ec036b..84b7b62 100644 --- a/spec/react/native_library_spec.rb +++ b/spec/react/native_library_spec.rb @@ -9,7 +9,7 @@ class Component < React::Component::Base backtrace :none render { NativeComponent(name: "There - #{params.time_stamp}") } end - + class NestedComponent < React::Component::Base param :time_stamp backtrace :none @@ -26,7 +26,7 @@ class NestedComponent < React::Component::Base end.to be_truthy end - it "imports a React.js functional stateless component" do + it "imports a React.js functional stateless component" do mount 'Foo', name: "There" do JS.call(:eval, 'window.NativeLibrary = { FunctionalComponent: function HelloMessage(props){ return React.createElement("div", null, "Hello ", props.name); }}') @@ -260,7 +260,7 @@ def render } JSCODE ) - React::Test::Utils.render_component_into_document(NativeLibraryTestModule::Component, time_stamp: Time.now) + React::Test::Utils.render_component_into_document(NativeLibraryTestModule::Component, time_stamp: Time.now.strftime('%Y%m%dT%H%M%S%z')) end expect(page.body[-100..-19]).to match(/
Hello There.*<\/div>/) end @@ -317,7 +317,7 @@ class Foo < React::Component::Base }}} JSCODE ) - React::Test::Utils.render_component_into_document(NativeLibraryTestModule::NestedComponent, time_stamp: Time.now) + React::Test::Utils.render_component_into_document(NativeLibraryTestModule::NestedComponent, time_stamp: Time.now.strftime('%Y%m%dT%H%M%S%z')) end expect(page.body[-100..-19]).to match(/
Hello There.*<\/div>/) end @@ -351,7 +351,7 @@ class Foo < React::Component::Base JSCODE ) begin - React::Test::Utils.render_component_into_document(NativeLibraryTestModule::NestedComponent, time_stamp: Time.now) + React::Test::Utils.render_component_into_document(NativeLibraryTestModule::NestedComponent, time_stamp: Time.now.strftime('%Y%m%dT%H%M%S%z')) rescue Exception => e e.message end diff --git a/spec/react/param_declaration_spec.rb b/spec/react/param_declaration_spec.rb index ceef6b9..b36bf31 100644 --- a/spec/react/param_declaration_spec.rb +++ b/spec/react/param_declaration_spec.rb @@ -14,6 +14,31 @@ def render expect(page.body[-35..-19]).to include("
biz
") end + it 'defines collect_other_params_as method on params proxy' do + mount 'Foo' do + class Foo < React::Component::Base + state s: :beginning, scope: :shared + def self.update_s(x) + mutate.s x + end + render do + Foo2(another_param: state.s) + end + end + + class Foo2 < Hyperloop::Component + collect_other_params_as :opts + + def render + DIV(id: :tp) { params.opts[:another_param] } + end + end + end + expect(page).to have_content('beginning') + evaluate_ruby("Foo.update_s 'updated'") + expect(page).to have_content('updated') + end + it "can create and access a required param" do mount 'Foo', foo: :bar do class Foo < React::Component::Base diff --git a/spec/react/validator_spec.rb b/spec/react/validator_spec.rb index 15e987d..cc1fbd7 100644 --- a/spec/react/validator_spec.rb +++ b/spec/react/validator_spec.rb @@ -95,28 +95,22 @@ class Bar; end end end - describe '#undefined_props' do - before :each do - on_client do - PROPS = { foo: 'foo', bar: 'bar', biz: 'biz', baz: 'baz' } - VALIDATOR = React::Validator.new.build do - requires :foo - optional :bar - end + it 'collects other params into a hash' do + evaluate_ruby do + PROPS = { foo: 'foo', bar: 'bar', biz: 'biz', baz: 'baz' } + VALIDATOR = React::Validator.new.build do + requires :foo + optional :bar + all_other_params :baz end - end - - - it 'slurps up any extra params into a hash' do - expect_evaluate_ruby('VALIDATOR.undefined_props(PROPS)').to eq({ "biz" => 'biz', "baz" => 'baz' }) - end - - it 'prevents validate non-specified params' do - evaluate_ruby do - VALIDATOR.undefined_props(PROPS) + class Dummy + def props + PROPS + end end - expect_evaluate_ruby('VALIDATOR.validate(PROPS)').to eq([]) end + expect_evaluate_ruby('VALIDATOR.validate(PROPS)').to eq([]) + expect_evaluate_ruby('VALIDATOR.props_wrapper.new(Dummy.new).baz').to eq({ "biz" => 'biz', "baz" => 'baz' }) end describe "default_props" do