From 314309b8e65816704db1375e5f27721d2a013906 Mon Sep 17 00:00:00 2001 From: David Chang Date: Tue, 31 Mar 2015 22:51:27 +0800 Subject: [PATCH 01/45] Upgrade react-source to v0.13 --- Gemfile.lock | 4 ++-- react.rb.gemspec | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Gemfile.lock b/Gemfile.lock index 82ccf8b..7317e76 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -30,7 +30,7 @@ GEM rack-protection (1.5.3) rack rake (10.4.2) - react-source (0.12.2) + react-source (0.13.1) ref (1.0.5) sinatra (1.4.5) rack (~> 1.4) @@ -55,6 +55,6 @@ DEPENDENCIES opal-jquery opal-rspec (~> 0.3.0.beta3) rake - react-source (~> 0.12) + react-source (~> 0.13) react.rb! sinatra diff --git a/react.rb.gemspec b/react.rb.gemspec index 1716f69..092089c 100644 --- a/react.rb.gemspec +++ b/react.rb.gemspec @@ -20,7 +20,7 @@ Gem::Specification.new do |s| s.add_runtime_dependency 'opal-activesupport' s.add_runtime_dependency 'sprockets-es6' s.add_runtime_dependency 'therubyracer' - s.add_development_dependency 'react-source', '~> 0.12' + s.add_development_dependency 'react-source', '~> 0.13' s.add_development_dependency 'opal-rspec', '~> 0.3.0.beta3' s.add_development_dependency 'sinatra' s.add_development_dependency 'opal-jquery' From 997c3a2e1bd1022ab720dc8392e81916cce1b678 Mon Sep 17 00:00:00 2001 From: David Chang Date: Tue, 31 Mar 2015 22:55:06 +0800 Subject: [PATCH 02/45] Update props validation spec --- spec/component_spec.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spec/component_spec.rb b/spec/component_spec.rb index eb5b017..569e843 100644 --- a/spec/component_spec.rb +++ b/spec/component_spec.rb @@ -351,7 +351,7 @@ def render; div; end } renderToDocument(Foo, bar: 10, lorem: Lorem.new) `window.console = org_console;` - expect(`log`).to eq(["Warning: In component `Foo`\nRequired prop `foo` was not specified\nProvided prop `bar` was not the specified type `String`"]) + expect(`log`).to eq(["Warning: Failed propType: In component `Foo`\nRequired prop `foo` was not specified\nProvided prop `bar` was not the specified type `String`"]) end it "should not log anything if validation pass" do From c23ae25fb145d7de0e37d3364e6b033bb3216d04 Mon Sep 17 00:00:00 2001 From: David Chang Date: Tue, 31 Mar 2015 23:04:46 +0800 Subject: [PATCH 03/45] Extract Component API to separate file --- lib/react/component.rb | 45 +----------------------------------- lib/react/component/api.rb | 47 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 48 insertions(+), 44 deletions(-) create mode 100644 lib/react/component/api.rb diff --git a/lib/react/component.rb b/lib/react/component.rb index 9b89a57..ba16b98 100644 --- a/lib/react/component.rb +++ b/lib/react/component.rb @@ -2,6 +2,7 @@ require 'active_support/core_ext/class/attribute' require 'react/callbacks' require "react/ext/hash" +require "react/component/api" module React module Component @@ -165,49 +166,5 @@ def define_state(*states) end end end - - module API - include Native - - alias_native :dom_node, :getDOMNode - alias_native :mounted?, :isMounted - alias_native :force_update!, :forceUpdate - - def set_props(prop, &block) - raise "No native ReactComponent associated" unless @native - %x{ - #{@native}.setProps(#{prop.shallow_to_n}, function(){ - #{block.call if block} - }); - } - end - - def set_props!(prop, &block) - raise "No native ReactComponent associated" unless @native - %x{ - #{@native}.replaceProps(#{prop.shallow_to_n}, function(){ - #{block.call if block} - }); - } - end - - def set_state(state, &block) - raise "No native ReactComponent associated" unless @native - %x{ - #{@native}.setState(#{state.shallow_to_n}, function(){ - #{block.call if block} - }); - } - end - - def set_state!(state, &block) - raise "No native ReactComponent associated" unless @native - %x{ - #{@native}.replaceState(#{state.shallow_to_n}, function(){ - #{block.call if block} - }); - } - end - end end end diff --git a/lib/react/component/api.rb b/lib/react/component/api.rb new file mode 100644 index 0000000..700021b --- /dev/null +++ b/lib/react/component/api.rb @@ -0,0 +1,47 @@ +module React + module Component + module API + include Native + + alias_native :dom_node, :getDOMNode + alias_native :mounted?, :isMounted + alias_native :force_update!, :forceUpdate + + def set_props(prop, &block) + raise "No native ReactComponent associated" unless @native + %x{ + #{@native}.setProps(#{prop.shallow_to_n}, function(){ + #{block.call if block} + }); + } + end + + def set_props!(prop, &block) + raise "No native ReactComponent associated" unless @native + %x{ + #{@native}.replaceProps(#{prop.shallow_to_n}, function(){ + #{block.call if block} + }); + } + end + + def set_state(state, &block) + raise "No native ReactComponent associated" unless @native + %x{ + #{@native}.setState(#{state.shallow_to_n}, function(){ + #{block.call if block} + }); + } + end + + def set_state!(state, &block) + raise "No native ReactComponent associated" unless @native + %x{ + #{@native}.replaceState(#{state.shallow_to_n}, function(){ + #{block.call if block} + }); + } + end + end + end +end \ No newline at end of file From 0e7b7bf7b209c70d6335247f0e4dcd27ab990d00 Mon Sep 17 00:00:00 2001 From: David Chang Date: Tue, 31 Mar 2015 23:22:15 +0800 Subject: [PATCH 04/45] Directly "mixin" React.Component class --- lib/react/api.rb | 72 +++++++++++-------------------------- lib/react/component/api.rb | 22 ++++++------ spec/reactjs/index.html.erb | 35 ++++++++++++++++++ 3 files changed, 67 insertions(+), 62 deletions(-) diff --git a/lib/react/api.rb b/lib/react/api.rb index b45739e..9720f0d 100644 --- a/lib/react/api.rb +++ b/lib/react/api.rb @@ -39,59 +39,27 @@ def self.clear_component_class_cache @@component_classes = {} end - def self.native_component_class(type) - @@component_classes[type.to_s] ||= %x{ - React.createClass({ - propTypes: #{type.respond_to?(:prop_types) ? type.prop_types.to_n : `{}`}, - getDefaultProps: function(){ - return #{type.respond_to?(:default_props) ? type.default_props.to_n : `{}`}; - }, - getInitialState: function(){ - return #{type.respond_to?(:initial_state) ? type.initial_state.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(`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?(`next_props`, `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(`next_props`, `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(`prev_props`, `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`.render.to_n}; - } - }) + def self.native_component_class(klass) + klass.class_eval do + native_alias :componentWillMount, :component_will_mount + native_alias :componentDidMount, :component_did_mount + def _render + self.render.to_n + end + native_alias :render, :_render + end + %x{ + + var f = function() { + var int = #{klass}.$new.call(#{klass}, arguments[0]); + int.state = #{klass.respond_to?(:initial_state) ? klass.initial_state.to_n : `{}`}; + return int; + }; + f.prototype = #{klass}._alloc.prototype; + + Object.assign(f.prototype, React.Component.prototype); } + @@component_classes[klass.to_s] ||= `f` end private diff --git a/lib/react/component/api.rb b/lib/react/component/api.rb index 700021b..52e37c8 100644 --- a/lib/react/component/api.rb +++ b/lib/react/component/api.rb @@ -1,16 +1,18 @@ module React module Component module API - include Native - - alias_native :dom_node, :getDOMNode - alias_native :mounted?, :isMounted - alias_native :force_update!, :forceUpdate - + def state + Native(`#{self}.state`) + end + + def force_update! + `#{self}.forceUpdate()` + end + def set_props(prop, &block) raise "No native ReactComponent associated" unless @native %x{ - #{@native}.setProps(#{prop.shallow_to_n}, function(){ + #{self}.setProps(#{prop.shallow_to_n}, function(){ #{block.call if block} }); } @@ -19,7 +21,7 @@ def set_props(prop, &block) def set_props!(prop, &block) raise "No native ReactComponent associated" unless @native %x{ - #{@native}.replaceProps(#{prop.shallow_to_n}, function(){ + #{self}.replaceProps(#{prop.shallow_to_n}, function(){ #{block.call if block} }); } @@ -28,7 +30,7 @@ def set_props!(prop, &block) def set_state(state, &block) raise "No native ReactComponent associated" unless @native %x{ - #{@native}.setState(#{state.shallow_to_n}, function(){ + #{self}.setState(#{state.shallow_to_n}, function(){ #{block.call if block} }); } @@ -37,7 +39,7 @@ def set_state(state, &block) def set_state!(state, &block) raise "No native ReactComponent associated" unless @native %x{ - #{@native}.replaceState(#{state.shallow_to_n}, function(){ + #{self}.replaceState(#{state.shallow_to_n}, function(){ #{block.call if block} }); } diff --git a/spec/reactjs/index.html.erb b/spec/reactjs/index.html.erb index aee6199..63bd143 100644 --- a/spec/reactjs/index.html.erb +++ b/spec/reactjs/index.html.erb @@ -5,6 +5,41 @@ <%= javascript_include_tag 'es5-shim.min' %> <%= javascript_include_tag 'react-with-addons' %> + <%= javascript_include_tag @server.main %> From 9cad22d72f21173c012a7066006636a25f94bb10 Mon Sep 17 00:00:00 2001 From: David Chang Date: Tue, 31 Mar 2015 23:23:34 +0800 Subject: [PATCH 05/45] Props is immutable now --- lib/react/component/api.rb | 18 ------------------ spec/component_spec.rb | 33 --------------------------------- 2 files changed, 51 deletions(-) diff --git a/lib/react/component/api.rb b/lib/react/component/api.rb index 52e37c8..1b79360 100644 --- a/lib/react/component/api.rb +++ b/lib/react/component/api.rb @@ -8,24 +8,6 @@ def state def force_update! `#{self}.forceUpdate()` end - - def set_props(prop, &block) - raise "No native ReactComponent associated" unless @native - %x{ - #{self}.setProps(#{prop.shallow_to_n}, function(){ - #{block.call if block} - }); - } - end - - def set_props!(prop, &block) - raise "No native ReactComponent associated" unless @native - %x{ - #{self}.replaceProps(#{prop.shallow_to_n}, function(){ - #{block.call if block} - }); - } - end def set_state(state, &block) raise "No native ReactComponent associated" unless @native diff --git a/spec/component_spec.rb b/spec/component_spec.rb index 569e843..78fd631 100644 --- a/spec/component_spec.rb +++ b/spec/component_spec.rb @@ -280,39 +280,6 @@ def render end end - describe "Props Updating" do - before do - stub_const 'Foo', Class.new - Foo.class_eval do - include React::Component - end - end - - it "should support original `setProps` as method `set_props`" do - Foo.class_eval do - def render - React.create_element("div") { params[:foo] } - end - end - - element = renderToDocument(Foo, {foo: 10}) - element.set_props(foo: 20) - expect(element.dom_node.innerHTML).to eq('20') - end - - it "should support original `replaceProps` as method `set_props!`" do - Foo.class_eval do - def render - React.create_element("div") { params[:foo] ? "exist" : "null" } - end - end - - element = renderToDocument(Foo, {foo: 10}) - element.set_props!(bar: 20) - expect(element.dom_node.innerHTML).to eq('null') - end - end - describe "Prop validation" do before do stub_const 'Foo', Class.new From 82ee8ae37a34a1bafbefa1ba72e9114872128a21 Mon Sep 17 00:00:00 2001 From: David Chang Date: Tue, 31 Mar 2015 23:29:01 +0800 Subject: [PATCH 06/45] Upgrade propTypes & defaultProps --- lib/react/api.rb | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lib/react/api.rb b/lib/react/api.rb index 9720f0d..5e7a897 100644 --- a/lib/react/api.rb +++ b/lib/react/api.rb @@ -56,7 +56,8 @@ def _render return int; }; f.prototype = #{klass}._alloc.prototype; - + f.propTypes = #{klass.respond_to?(:prop_types) ? klass.prop_types.to_n : `{}`}; + f.defaultProps = #{klass.respond_to?(:default_props) ? klass.default_props.to_n : `{}`}; Object.assign(f.prototype, React.Component.prototype); } @@component_classes[klass.to_s] ||= `f` From d11772aeeefac9f2797258af657a696f90bcb3f8 Mon Sep 17 00:00:00 2001 From: David Chang Date: Tue, 31 Mar 2015 23:42:14 +0800 Subject: [PATCH 07/45] Temporary fix test case rely on getDOMNode() --- lib/react/component/api.rb | 5 +++++ spec/component_spec.rb | 6 +++--- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/lib/react/component/api.rb b/lib/react/component/api.rb index 1b79360..78e8766 100644 --- a/lib/react/component/api.rb +++ b/lib/react/component/api.rb @@ -26,6 +26,11 @@ def set_state!(state, &block) }); } end + + #FIXME: Should be deprecated in favor of sth like, React.find_dom_node(component) + def dom_node + Native(`React.findDOMNode(#{self})`) + end end end end \ No newline at end of file diff --git a/spec/component_spec.rb b/spec/component_spec.rb index 78fd631..5f1150c 100644 --- a/spec/component_spec.rb +++ b/spec/component_spec.rb @@ -190,7 +190,7 @@ def render end element = renderToDocument(Foo) - expect(element.getDOMNode.textContent).to eq("10") + expect(element.dom_node.textContent).to eq("10") end it "should support original `setState` as `set_state` method" do @@ -265,7 +265,7 @@ def render end element = renderToDocument(Foo, prop: "foobar") - expect(element.getDOMNode.textContent).to eq("foobar") + expect(element.dom_node.textContent).to eq("foobar") end it "should access nested params as orignal Ruby object" do @@ -276,7 +276,7 @@ def render end element = renderToDocument(Foo, prop: [{foo: 10}]) - expect(element.getDOMNode.textContent).to eq("10") + expect(element.dom_node.textContent).to eq("10") end end From 22769f8c9482fa59da3adedfcba132be6bccc014 Mon Sep 17 00:00:00 2001 From: David Chang Date: Tue, 31 Mar 2015 23:43:33 +0800 Subject: [PATCH 08/45] set_state! ( replaceState() ) is deprecated --- lib/react/component/api.rb | 9 --------- spec/component_spec.rb | 13 ------------- 2 files changed, 22 deletions(-) diff --git a/lib/react/component/api.rb b/lib/react/component/api.rb index 78e8766..fb436c0 100644 --- a/lib/react/component/api.rb +++ b/lib/react/component/api.rb @@ -17,15 +17,6 @@ def set_state(state, &block) }); } end - - def set_state!(state, &block) - raise "No native ReactComponent associated" unless @native - %x{ - #{self}.replaceState(#{state.shallow_to_n}, function(){ - #{block.call if block} - }); - } - end #FIXME: Should be deprecated in favor of sth like, React.find_dom_node(component) def dom_node diff --git a/spec/component_spec.rb b/spec/component_spec.rb index 5f1150c..4b506b9 100644 --- a/spec/component_spec.rb +++ b/spec/component_spec.rb @@ -204,19 +204,6 @@ def render expect(element.state.foo).to be("bar") end - it "should support original `replaceState` as `set_state!` method" do - Foo.class_eval do - before_mount do - self.set_state(foo: "bar") - self.set_state!(bar: "lorem") - end - end - - element = renderToDocument(Foo) - expect(element.state.foo).to be_nil - expect(element.state.bar).to eq("lorem") - end - it "should support originl `state` method" do Foo.class_eval do before_mount do From b7add1a74f167e22b98610d13cf8a7a473e9341d Mon Sep 17 00:00:00 2001 From: David Chang Date: Wed, 1 Apr 2015 00:43:42 +0800 Subject: [PATCH 09/45] Deprecate the bridged native element usage --- lib/react/component.rb | 13 +++---------- lib/react/component/api.rb | 1 - 2 files changed, 3 insertions(+), 11 deletions(-) diff --git a/lib/react/component.rb b/lib/react/component.rb index ba16b98..4a4aaa6 100644 --- a/lib/react/component.rb +++ b/lib/react/component.rb @@ -21,21 +21,16 @@ def self.included(base) base.extend(ClassMethods) end - def initialize(native_element) - @native = native_element - end - def params - Hash.new(`#{@native}.props`) + Hash.new(`#{self}.props`) end def refs - Hash.new(`#{@native}.refs`) + Hash.new(`#{self}.refs`) end def state - raise "No native ReactComponent associated" unless @native - Hash.new(`#{@native}.state`) + Hash.new(`#{self}.state`) end def emit(event_name, *args) @@ -151,12 +146,10 @@ def define_state(*states) states.each do |name| # getter define_method("#{name}") do - return unless @native self.state[name] end # setter define_method("#{name}=") do |new_state| - return unless @native hash = {} hash[name] = new_state self.set_state(hash) diff --git a/lib/react/component/api.rb b/lib/react/component/api.rb index fb436c0..23c5edb 100644 --- a/lib/react/component/api.rb +++ b/lib/react/component/api.rb @@ -10,7 +10,6 @@ def force_update! end def set_state(state, &block) - raise "No native ReactComponent associated" unless @native %x{ #{self}.setState(#{state.shallow_to_n}, function(){ #{block.call if block} From 704cdee3b4b3ee3ab0843a8480efa7978a3b0743 Mon Sep 17 00:00:00 2001 From: David Chang Date: Wed, 1 Apr 2015 00:45:04 +0800 Subject: [PATCH 10/45] mounted? ( isMounted() ) is deprecated --- spec/component_spec.rb | 16 ---------------- 1 file changed, 16 deletions(-) diff --git a/spec/component_spec.rb b/spec/component_spec.rb index 4b506b9..bb7e2e5 100644 --- a/spec/component_spec.rb +++ b/spec/component_spec.rb @@ -533,22 +533,6 @@ def render end end - describe "isMounted()" do - it "should return true if after mounted" do - stub_const 'Foo', Class.new - Foo.class_eval do - include React::Component - - def render - React.create_element("div") - end - end - - component = renderToDocument(Foo) - expect(component.mounted?).to eq(true) - end - end - describe "Helpers" do describe "jsx" do it "should wrap passed JS object to React::Element" do From f43cdc7f0b3e2a597c4aa1bc3a5285342041cae8 Mon Sep 17 00:00:00 2001 From: David Chang Date: Wed, 1 Apr 2015 01:14:19 +0800 Subject: [PATCH 11/45] Expect Component property exposed as Hash --- spec/component_spec.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/spec/component_spec.rb b/spec/component_spec.rb index bb7e2e5..464fdfa 100644 --- a/spec/component_spec.rb +++ b/spec/component_spec.rb @@ -432,7 +432,7 @@ def render end element = renderToDocument(Foo) - expect(element.refs.field).not_to be_nil + expect(element.refs[:field]).not_to be_nil end it "should access refs through `refs` method" do @@ -447,7 +447,7 @@ def render element = renderToDocument(Foo) simulateEvent(:click, element) - expect(element.refs.field.value).to eq("some_stuff") + expect(element.refs[:field].value).to eq("some_stuff") end end From d0171071684245deb698f9cd01856c1a0b0307bb Mon Sep 17 00:00:00 2001 From: David Chang Date: Wed, 1 Apr 2015 01:14:33 +0800 Subject: [PATCH 12/45] Fix spec --- lib/react/component.rb | 5 ++++- lib/react/component/api.rb | 6 +++++- spec/spec_helper.rb | 2 +- 3 files changed, 10 insertions(+), 3 deletions(-) diff --git a/lib/react/component.rb b/lib/react/component.rb index 4a4aaa6..894cc63 100644 --- a/lib/react/component.rb +++ b/lib/react/component.rb @@ -104,7 +104,10 @@ def method_missing(name, *args, &block) @buffer << element element end - + + def to_n + self + end module ClassMethods def prop_types diff --git a/lib/react/component/api.rb b/lib/react/component/api.rb index 23c5edb..2b62fbd 100644 --- a/lib/react/component/api.rb +++ b/lib/react/component/api.rb @@ -19,7 +19,11 @@ def set_state(state, &block) #FIXME: Should be deprecated in favor of sth like, React.find_dom_node(component) def dom_node - Native(`React.findDOMNode(#{self})`) + if self.respond_to?(:getDOMNode) + self.getDOMNode + else + Native(`React.findDOMNode(#{self})`) + end end end end diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index f4a09e1..f54746c 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -16,7 +16,7 @@ def renderElementToDocument(element) def simulateEvent(event, element, params = {}) simulator = Native(`ReactTestUtils.Simulate`) - simulator[event.to_s].call(`#{element.to_n}.getDOMNode()`, params) + simulator[event.to_s].call(`#{element.dom_node.to_n}`, params) end def isElementOfType(element, type) From 072df76728af912ec69c3467851c3d6aa6539ee4 Mon Sep 17 00:00:00 2001 From: David Chang Date: Wed, 1 Apr 2015 01:24:39 +0800 Subject: [PATCH 13/45] `props` & `state` will alwasy return a Hash internal & external --- lib/react/component.rb | 4 ---- lib/react/component/api.rb | 6 +++++- spec/component_spec.rb | 20 ++++++++++---------- spec/react_spec.rb | 8 ++++---- 4 files changed, 19 insertions(+), 19 deletions(-) diff --git a/lib/react/component.rb b/lib/react/component.rb index 894cc63..b6ddd17 100644 --- a/lib/react/component.rb +++ b/lib/react/component.rb @@ -29,10 +29,6 @@ def refs Hash.new(`#{self}.refs`) end - def state - Hash.new(`#{self}.state`) - end - def emit(event_name, *args) self.params["_on#{event_name.to_s.event_camelize}"].call(*args) end diff --git a/lib/react/component/api.rb b/lib/react/component/api.rb index 2b62fbd..975e08d 100644 --- a/lib/react/component/api.rb +++ b/lib/react/component/api.rb @@ -2,7 +2,11 @@ module React module Component module API def state - Native(`#{self}.state`) + Hash.new(`#{self}.state`) + end + + def props + Hash.new(`#{self}.props`) end def force_update! diff --git a/spec/component_spec.rb b/spec/component_spec.rb index 464fdfa..20b41ab 100644 --- a/spec/component_spec.rb +++ b/spec/component_spec.rb @@ -97,7 +97,7 @@ def render end element = renderToDocument(Foo) - expect(element.state.foo).to be("bar") + expect(element.state[:foo]).to be("bar") end end @@ -122,7 +122,7 @@ def set_up end element = renderToDocument(Foo) - expect(element.state.foo).to be("bar") + expect(element.state[:foo]).to be("bar") end it "should define init state by passing a block to `define_state`" do @@ -131,7 +131,7 @@ def set_up end element = renderToDocument(Foo) - expect(element.state.foo).to be(10) + expect(element.state[:foo]).to be(10) end it "should define getter using `define_state`" do @@ -144,7 +144,7 @@ def bump end element = renderToDocument(Foo) - expect(element.state.foo).to be(30) + expect(element.state[:foo]).to be(30) end it "should define multiple state accessor by passing symols array to `define_state`" do @@ -158,8 +158,8 @@ def set_up end element = renderToDocument(Foo) - expect(element.state.foo).to be(10) - expect(element.state.foo2).to be(20) + expect(element.state[:foo]).to be(10) + expect(element.state[:foo2]).to be(20) end it "should invoke `define_state` multiple times to define states" do @@ -169,8 +169,8 @@ def set_up end element = renderToDocument(Foo) - expect(element.state.foo).to be(30) - expect(element.state.foo2).to be(40) + expect(element.state[:foo]).to be(30) + expect(element.state[:foo2]).to be(40) end it "should raise error if multiple states and block given at the same time" do @@ -201,7 +201,7 @@ def render end element = renderToDocument(Foo) - expect(element.state.foo).to be("bar") + expect(element.state[:foo]).to be("bar") end it "should support originl `state` method" do @@ -374,7 +374,7 @@ def render element = React.create_element(Foo) instance = renderElementToDocument(element) simulateEvent(:click, instance) - expect(instance.state.clicked).to eq(true) + expect(instance.state[:clicked]).to eq(true) end it "should invoke handler on `this.props` using emit" do diff --git a/spec/react_spec.rb b/spec/react_spec.rb index bad9a4e..5bafcfe 100644 --- a/spec/react_spec.rb +++ b/spec/react_spec.rb @@ -59,15 +59,15 @@ def render it "should render element with only one children correctly" do element = React.create_element(Foo) { React.create_element('span') } instance = renderElementToDocument(element) - expect(instance.props.children).not_to be_a(Array) - expect(instance.props.children.type).to eq("span") + expect(instance.props[:children]).not_to be_a(Array) + expect(instance.props[:children][:type]).to eq("span") end it "should render element with more than one children correctly" do element = React.create_element(Foo) { [React.create_element('span'), React.create_element('span')] } instance = renderElementToDocument(element) - expect(instance.props.children).to be_a(Array) - expect(instance.props.children.length).to eq(2) + expect(instance.props[:children]).to be_a(Array) + expect(instance.props[:children].length).to eq(2) end it "should create a valid element provided class defined `render`" do From bcee9ae37710dd771e2f0e2ec194dc3a4b80eb38 Mon Sep 17 00:00:00 2001 From: David Chang Date: Wed, 1 Apr 2015 10:10:31 +0800 Subject: [PATCH 14/45] React::Element is a toll-free bridge now --- lib/react/element.rb | 43 ++++++++++++++++++++++++++++++------------ lib/react/top_level.rb | 2 +- spec/react_spec.rb | 16 ++++++++-------- 3 files changed, 40 insertions(+), 21 deletions(-) diff --git a/lib/react/element.rb b/lib/react/element.rb index 98eb6b3..fd41026 100644 --- a/lib/react/element.rb +++ b/lib/react/element.rb @@ -1,36 +1,55 @@ require "./ext/string" module React - class Element - include Native - - alias_native :element_type, :type - alias_native :props, :props - - def initialize(native_element) - @native = native_element + class Element < `OpalReactElement` + def self.new(native_element) + native_element + end + + def to_n + self + end + + def props + Hash.new(`#{self}.props`) + end + + def element_type + `#{self}.type` + end + + def key + `#{self}.key` end def on(event_name) name = event_name.to_s.event_camelize if React::Event::BUILT_IN_EVENTS.include?("on#{name}") - self.props["on#{name}"] = %x{ + prop_name = "on#{name}" + callback = %x{ function(event){ #{yield React::Event.new(`event`)} } } else - self.props["_on#{name}"] = %x{ + prop_name = "_on#{name}" + callback = %x{ function(){ #{yield *Array(`arguments`)} } } end - self + new_props = `{}` + `new_props[#{prop_name}] = #{callback}` + `new_props.ref = #{self}.ref` + + element = `React.addons.cloneWithProps(#{self}, new_props)` + + element end def children - nodes = self.props.children + nodes = self.props[:children] class << nodes include Enumerable diff --git a/lib/react/top_level.rb b/lib/react/top_level.rb index 3e36d33..63476e5 100644 --- a/lib/react/top_level.rb +++ b/lib/react/top_level.rb @@ -33,7 +33,7 @@ def self.render(element, container) end def self.is_valid_element(element) - element.kind_of?(React::Element) && `React.isValidElement(#{element.to_n})` + `React.isValidElement(#{element})` end def self.render_to_string(element) diff --git a/spec/react_spec.rb b/spec/react_spec.rb index 5bafcfe..20bee19 100644 --- a/spec/react_spec.rb +++ b/spec/react_spec.rb @@ -27,7 +27,7 @@ it "should create a valid element with text as only child when block yield String" do element = React.create_element('div') { "lorem ipsum" } expect(React.is_valid_element(element)).to eq(true) - expect(element.props.children).to eq("lorem ipsum") + expect(element.props[:children]).to eq("lorem ipsum") end it "should create a valid element with children as array when block yield Array of element" do @@ -35,7 +35,7 @@ [React.create_element('span'), React.create_element('span'), React.create_element('span')] end expect(React.is_valid_element(element)).to eq(true) - expect(element.props.children.length).to eq(3) + expect(element.props[:children].length).to eq(3) end it "should render element with children as array when block yield Array of element" do @@ -77,7 +77,7 @@ def render it "should allow creating with properties" do element = React.create_element(Foo, foo: "bar") - expect(element.props.foo).to eq("bar") + expect(element.props[:foo]).to eq("bar") end it "should raise error if provided class doesn't defined `render`" do @@ -125,17 +125,17 @@ def render describe "create element with properties" do it "should enforce snake-cased property name" do element = React.create_element("div", class_name: "foo") - expect(element.props.className).to eq("foo") + expect(element.props[:className]).to eq("foo") end it "should allow custom property" do element = React.create_element("div", foo: "bar") - expect(element.props.foo).to eq("bar") + expect(element.props[:foo]).to eq("bar") end it "should not camel-case custom property" do element = React.create_element("div", foo_bar: "foo") - expect(element.props.foo_bar).to eq("foo") + expect(element.props[:foo_bar]).to eq("foo") end end @@ -144,13 +144,13 @@ def render classes = {foo: true, bar: false, lorem: true} element = React.create_element("div", class_name: classes) - expect(element.props.className).to eq("foo lorem") + expect(element.props[:className]).to eq("foo lorem") end it "should not alter behavior when passing a string" do element = React.create_element("div", class_name: "foo bar") - expect(element.props.className).to eq("foo bar") + expect(element.props[:className]).to eq("foo bar") end end end From e111ecc4243d1d5eefb6fc796ad05727ce0ed05a Mon Sep 17 00:00:00 2001 From: David Chang Date: Wed, 1 Apr 2015 10:13:44 +0800 Subject: [PATCH 15/45] Use React::Element directly without to_n --- lib/react/api.rb | 9 +++------ lib/react/element.rb | 6 +++--- lib/react/top_level.rb | 6 +++--- 3 files changed, 9 insertions(+), 12 deletions(-) diff --git a/lib/react/api.rb b/lib/react/api.rb index 5e7a897..5a67cf4 100644 --- a/lib/react/api.rb +++ b/lib/react/api.rb @@ -28,11 +28,11 @@ def self.create_element(type, properties = {}, &block) # Children Nodes if block_given? children = [yield].flatten.each do |ele| - params << ele.to_n + params << ele end end - return React::Element.new(`React.createElement.apply(null, #{params})`) + return `React.createElement.apply(null, #{params})` end def self.clear_component_class_cache @@ -43,10 +43,7 @@ def self.native_component_class(klass) klass.class_eval do native_alias :componentWillMount, :component_will_mount native_alias :componentDidMount, :component_did_mount - def _render - self.render.to_n - end - native_alias :render, :_render + native_alias :render, :render end %x{ diff --git a/lib/react/element.rb b/lib/react/element.rb index fd41026..481fb4d 100644 --- a/lib/react/element.rb +++ b/lib/react/element.rb @@ -60,14 +60,14 @@ def to_n def each(&block) if block_given? %x{ - React.Children.forEach(#{self.to_n}, function(context){ + React.Children.forEach(#{self}, function(context){ #{block.call(React::Element.new(`context`))} }) } else - Enumerator.new(`React.Children.count(#{self.to_n})`) do |y| + Enumerator.new(`React.Children.count(#{self})`) do |y| %x{ - React.Children.forEach(#{self.to_n}, function(context){ + React.Children.forEach(#{self}, function(context){ #{y << React::Element.new(`context`)} }) } diff --git a/lib/react/top_level.rb b/lib/react/top_level.rb index 63476e5..1154f9c 100644 --- a/lib/react/top_level.rb +++ b/lib/react/top_level.rb @@ -27,7 +27,7 @@ def self.create_element(type, properties = {}, &block) end def self.render(element, container) - component = Native(`React.render(#{element.to_n}, container, function(){#{yield if block_given?}})`) + component = Native(`React.render(#{element}, container, function(){#{yield if block_given?}})`) component.class.include(React::Component::API) component end @@ -37,11 +37,11 @@ def self.is_valid_element(element) end def self.render_to_string(element) - `React.renderToString(#{element.to_n})` + `React.renderToString(#{element})` end def self.render_to_static_markup(element) - `React.renderToStaticMarkup(#{element.to_n})` + `React.renderToStaticMarkup(#{element})` end def self.unmount_component_at_node(node) From 7147b5a97c31b79f9741cfe1a511f571eeed379c Mon Sep 17 00:00:00 2001 From: David Chang Date: Wed, 1 Apr 2015 10:22:37 +0800 Subject: [PATCH 16/45] Rename React::API to React::ComponentFactory --- lib/react.rb | 2 +- lib/react/api.rb | 72 ---------------------------------- lib/react/component_factory.rb | 30 ++++++++++++++ lib/react/ext/string.rb | 8 ++++ lib/react/top_level.rb | 34 +++++++++++++++- spec/component_spec.rb | 2 +- spec/react_spec.rb | 2 +- 7 files changed, 73 insertions(+), 77 deletions(-) delete mode 100644 lib/react/api.rb create mode 100644 lib/react/component_factory.rb diff --git a/lib/react.rb b/lib/react.rb index f99e731..8381123 100644 --- a/lib/react.rb +++ b/lib/react.rb @@ -4,7 +4,7 @@ require "react/element" require "react/event" require "react/version" - require "react/api" + require "react/component_factory" require "react/validator" else require "tilt" diff --git a/lib/react/api.rb b/lib/react/api.rb deleted file mode 100644 index 5a67cf4..0000000 --- a/lib/react/api.rb +++ /dev/null @@ -1,72 +0,0 @@ -module React - class API - @@component_classes = {} - - def self.create_element(type, properties = {}, &block) - params = [] - - # Component Spec or Nomral DOM - if type.kind_of?(Class) - raise "Provided class should define `render` method" if !(type.method_defined? :render) - params << self.native_component_class(type) - else - raise "#{type} not implemented" unless HTML_TAGS.include?(type) - params << type - end - - # Passed in properties - props = {} - properties.map do |key, value| - if key == "class_name" && value.is_a?(Hash) - props[lower_camelize(key)] = value.inject([]) {|ary, (k,v)| v ? ary.push(k) : ary}.join(" ") - else - props[React::ATTRIBUTES.include?(lower_camelize(key)) ? lower_camelize(key) : key] = value - end - end - params << props.shallow_to_n - - # Children Nodes - if block_given? - children = [yield].flatten.each do |ele| - params << ele - end - end - - return `React.createElement.apply(null, #{params})` - end - - def self.clear_component_class_cache - @@component_classes = {} - end - - def self.native_component_class(klass) - klass.class_eval do - native_alias :componentWillMount, :component_will_mount - native_alias :componentDidMount, :component_did_mount - native_alias :render, :render - end - %x{ - - var f = function() { - var int = #{klass}.$new.call(#{klass}, arguments[0]); - int.state = #{klass.respond_to?(:initial_state) ? klass.initial_state.to_n : `{}`}; - return int; - }; - f.prototype = #{klass}._alloc.prototype; - f.propTypes = #{klass.respond_to?(:prop_types) ? klass.prop_types.to_n : `{}`}; - f.defaultProps = #{klass.respond_to?(:default_props) ? klass.default_props.to_n : `{}`}; - Object.assign(f.prototype, React.Component.prototype); - } - @@component_classes[klass.to_s] ||= `f` - 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 diff --git a/lib/react/component_factory.rb b/lib/react/component_factory.rb new file mode 100644 index 0000000..23df371 --- /dev/null +++ b/lib/react/component_factory.rb @@ -0,0 +1,30 @@ +module React + class ComponentFactory + @@component_classes = {} + + def self.clear_component_class_cache + @@component_classes = {} + end + + def self.native_component_class(klass) + klass.class_eval do + native_alias :componentWillMount, :component_will_mount + native_alias :componentDidMount, :component_did_mount + native_alias :render, :render + end + %x{ + + var f = function() { + var int = #{klass}.$new.call(#{klass}, arguments[0]); + int.state = #{klass.respond_to?(:initial_state) ? klass.initial_state.to_n : `{}`}; + return int; + }; + f.prototype = #{klass}._alloc.prototype; + f.propTypes = #{klass.respond_to?(:prop_types) ? klass.prop_types.to_n : `{}`}; + f.defaultProps = #{klass.respond_to?(:default_props) ? klass.default_props.to_n : `{}`}; + Object.assign(f.prototype, React.Component.prototype); + } + @@component_classes[klass.to_s] ||= `f` + end + end +end diff --git a/lib/react/ext/string.rb b/lib/react/ext/string.rb index 4f979c1..32122e0 100644 --- a/lib/react/ext/string.rb +++ b/lib/react/ext/string.rb @@ -5,4 +5,12 @@ def event_camelize return capitalize ? word.substr(0,1).toUpperCase()+word.substr(1) : word; })` end + + def lower_camelize + # TODO Could be implemented more efficiently + words = self.split("_") + result = [words.first] + result.concat(words[1..-1].map {|word| word[0].upcase + word[1..-1] }) + result.join("") + end end \ No newline at end of file diff --git a/lib/react/top_level.rb b/lib/react/top_level.rb index 1154f9c..6874941 100644 --- a/lib/react/top_level.rb +++ b/lib/react/top_level.rb @@ -1,5 +1,6 @@ require "native" require 'active_support' +require "react/ext/string" module React HTML_TAGS = %w(a abbr address area article aside audio b base bdi bdo big blockquote body br @@ -23,7 +24,36 @@ module React tabIndex target title type useMap value width wmode dangerouslySetInnerHTML) def self.create_element(type, properties = {}, &block) - React::API.create_element(type, properties, &block) + params = [] + + # Component Spec or Nomral DOM + if type.kind_of?(Class) + raise "Provided class should define `render` method" if !(type.method_defined? :render) + params << React::ComponentFactory.native_component_class(type) + else + raise "#{type} not implemented" unless HTML_TAGS.include?(type) + params << type + end + + # Passed in properties + props = {} + properties.map do |key, value| + if key == "class_name" && value.is_a?(Hash) + props[key.lower_camelize] = value.inject([]) {|ary, (k,v)| v ? ary.push(k) : ary}.join(" ") + else + props[React::ATTRIBUTES.include?(key.lower_camelize) ? key.lower_camelize : key] = value + end + end + params << props.shallow_to_n + + # Children Nodes + if block_given? + children = [yield].flatten.each do |ele| + params << ele + end + end + + return `React.createElement.apply(null, #{params})` end def self.render(element, container) @@ -50,7 +80,7 @@ def self.unmount_component_at_node(node) def self.expose_native_class(*args) args.each do |klass| - `window[#{klass.to_s}] = #{React::API.native_component_class(klass)}` + `window[#{klass.to_s}] = #{React::ComponentFactory.native_component_class(klass)}` end end end diff --git a/spec/component_spec.rb b/spec/component_spec.rb index 20b41ab..14c2512 100644 --- a/spec/component_spec.rb +++ b/spec/component_spec.rb @@ -2,7 +2,7 @@ describe React::Component do after(:each) do - React::API.clear_component_class_cache + React::ComponentFactory.clear_component_class_cache end it "should define component spec methods" do diff --git a/spec/react_spec.rb b/spec/react_spec.rb index 20bee19..219554b 100644 --- a/spec/react_spec.rb +++ b/spec/react_spec.rb @@ -2,7 +2,7 @@ describe React do after(:each) do - React::API.clear_component_class_cache + React::ComponentFactory.clear_component_class_cache end describe "is_valid_element" do From edb132e8fb8d8ad54c7b34c84d4d748544a66db0 Mon Sep 17 00:00:00 2001 From: David Chang Date: Fri, 3 Apr 2015 21:53:03 +0800 Subject: [PATCH 17/45] Remove duplicated test --- spec/react_spec.rb | 8 -------- 1 file changed, 8 deletions(-) diff --git a/spec/react_spec.rb b/spec/react_spec.rb index 54bb425..1b31d94 100644 --- a/spec/react_spec.rb +++ b/spec/react_spec.rb @@ -37,14 +37,6 @@ expect(React.is_valid_element(element)).to eq(true) expect(element.children.length).to eq(3) end - - it "should render element with children as array when block yield Array of element" do - element = React.create_element('div') do - [React.create_element('span'), React.create_element('span'), React.create_element('span')] - end - instance = renderElementToDocument(element) - expect(instance.getDOMNode.childNodes.length).to eq(3) - end end describe "custom element" do before do From f570a46f0f998035ade60424f36a6e7ef1e374f6 Mon Sep 17 00:00:00 2001 From: David Chang Date: Fri, 3 Apr 2015 21:53:33 +0800 Subject: [PATCH 18/45] Native wrapper is no longer required --- spec/spec_helper.rb | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index 2ed31ff..ead4c35 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -9,9 +9,7 @@ def renderToDocument(type, options = {}) end def renderElementToDocument(element) - instance = Native(`ReactTestUtils.renderIntoDocument(#{element})`) - instance.class.include(React::Component::API) - return instance + `ReactTestUtils.renderIntoDocument(#{element})` end def simulateEvent(event, component, params = {}) From e7fac7e47966e4454e9cbc85448fb8b0a9f33f74 Mon Sep 17 00:00:00 2001 From: David Chang Date: Fri, 3 Apr 2015 21:53:47 +0800 Subject: [PATCH 19/45] getDOMNode() is deprecated --- spec/spec_helper.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index ead4c35..f932aa1 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -14,7 +14,7 @@ def renderElementToDocument(element) def simulateEvent(event, component, params = {}) simulator = Native(`ReactTestUtils.Simulate`) - simulator[event.to_s].call(`#{component.to_n}.getDOMNode()`, params) + simulator[event.to_s].call(`React.findDOMNode(#{component})`, params) end def isElementOfType(element, type) From 3c9894d89ae1ea24f820e6f093cc79eb90865fcd Mon Sep 17 00:00:00 2001 From: David Chang Date: Fri, 3 Apr 2015 21:56:01 +0800 Subject: [PATCH 20/45] Fix the bridging constructor --- lib/react/component_factory.rb | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/lib/react/component_factory.rb b/lib/react/component_factory.rb index 23df371..7f314d1 100644 --- a/lib/react/component_factory.rb +++ b/lib/react/component_factory.rb @@ -13,18 +13,18 @@ def self.native_component_class(klass) native_alias :render, :render end %x{ - - var f = function() { - var int = #{klass}.$new.call(#{klass}, arguments[0]); - int.state = #{klass.respond_to?(:initial_state) ? klass.initial_state.to_n : `{}`}; - return int; + function ctor(){ + this.constructor = ctor; + this.state = #{klass.respond_to?(:initial_state) ? klass.initial_state.to_n : `{}`}; + React.Component.apply(this, arguments); + #{klass}._alloc.prototype.$initialize.call(this); }; - f.prototype = #{klass}._alloc.prototype; - f.propTypes = #{klass.respond_to?(:prop_types) ? klass.prop_types.to_n : `{}`}; - f.defaultProps = #{klass.respond_to?(:default_props) ? klass.default_props.to_n : `{}`}; - Object.assign(f.prototype, React.Component.prototype); + ctor.prototype = klass._proto; + Object.assign(ctor.prototype, React.Component.prototype); + ctor.propTypes = #{klass.respond_to?(:prop_types) ? klass.prop_types.to_n : `{}`}; + ctor.defaultProps = #{klass.respond_to?(:default_props) ? klass.default_props.to_n : `{}`}; } - @@component_classes[klass.to_s] ||= `f` + @@component_classes[klass.to_s] ||= `ctor` end end end From 304d7f98c1a712754cb2a94f5e639832818ca862 Mon Sep 17 00:00:00 2001 From: David Chang Date: Fri, 3 Apr 2015 22:07:37 +0800 Subject: [PATCH 21/45] Remove needless access to ReactElement prototype --- spec/reactjs/index.html.erb | 2 -- 1 file changed, 2 deletions(-) diff --git a/spec/reactjs/index.html.erb b/spec/reactjs/index.html.erb index 63bd143..1837d65 100644 --- a/spec/reactjs/index.html.erb +++ b/spec/reactjs/index.html.erb @@ -37,8 +37,6 @@ } }); } - OpalReactElement = new Function(); - OpalReactElement.prototype = Object.getPrototypeOf(React.createElement('')); <%= javascript_include_tag @server.main %> From 9fc21f2adca04bd89ee22e4e8ba3073684d2b766 Mon Sep 17 00:00:00 2001 From: David Chang Date: Fri, 3 Apr 2015 23:07:25 +0800 Subject: [PATCH 22/45] Add find_dom_node to top level API --- lib/react/top_level.rb | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/lib/react/top_level.rb b/lib/react/top_level.rb index 6874941..e9c55f8 100644 --- a/lib/react/top_level.rb +++ b/lib/react/top_level.rb @@ -83,4 +83,8 @@ def self.expose_native_class(*args) `window[#{klass.to_s}] = #{React::ComponentFactory.native_component_class(klass)}` end end + + def self.find_dom_node(component) + `React.findDOMNode(component)` + end end From 2d295ad4ffb5cddfe38010da215ff9a917d95a79 Mon Sep 17 00:00:00 2001 From: David Chang Date: Fri, 3 Apr 2015 23:07:55 +0800 Subject: [PATCH 23/45] Clean up testing --- lib/react/testing.rb | 14 ++++++++ spec/component_spec.rb | 79 ++++++++++++++++++++---------------------- spec/element_spec.rb | 12 +++---- spec/event_spec.rb | 6 ++-- spec/react_spec.rb | 8 ++--- spec/spec_helper.rb | 28 +++------------ 6 files changed, 67 insertions(+), 80 deletions(-) create mode 100644 lib/react/testing.rb diff --git a/lib/react/testing.rb b/lib/react/testing.rb new file mode 100644 index 0000000..3915922 --- /dev/null +++ b/lib/react/testing.rb @@ -0,0 +1,14 @@ +module React + module Testing + `var ReactTestUtils = React.addons.TestUtils` + + def simulate_event(event_name, dom_element, event_data = {}) + simulator = Native(`ReactTestUtils.Simulate`) + simulator[event_name].call(dom_element, event_data) + end + + def render_to_document(element) + `ReactTestUtils.renderIntoDocument(element)` + end + end +end diff --git a/spec/component_spec.rb b/spec/component_spec.rb index 2730205..6c01838 100644 --- a/spec/component_spec.rb +++ b/spec/component_spec.rb @@ -1,10 +1,6 @@ require "spec_helper" describe React::Component do - after(:each) do - React::ComponentFactory.clear_component_class_cache - end - it "should define component spec methods" do stub_const 'Foo', Class.new Foo.class_eval do @@ -50,7 +46,7 @@ def bar2; end expect_any_instance_of(Foo).to receive(:bar) expect_any_instance_of(Foo).to receive(:bar2) - renderToDocument(Foo) + render_to_document(React.create_element(Foo)) end it "should invoke `after_mount` registered methods when `componentDidMount()`" do @@ -63,7 +59,7 @@ def bar4; end expect_any_instance_of(Foo).to receive(:bar3) expect_any_instance_of(Foo).to receive(:bar4) - renderToDocument(Foo) + render_to_document(React.create_element(Foo)) end it "should allow multiple class declared life cycle hooker" do @@ -84,7 +80,7 @@ def render expect_any_instance_of(Foo).to receive(:bar) - renderToDocument(Foo) + render_to_document(React.create_element(Foo)) end it "should allow block for life cycle callback" do @@ -96,8 +92,8 @@ def render end end - element = renderToDocument(Foo) - expect(element.state[:foo]).to be("bar") + instance = render_to_document(React.create_element(Foo)) + expect(instance.state[:foo]).to be("bar") end end @@ -121,8 +117,8 @@ def set_up end end - element = renderToDocument(Foo) - expect(element.state[:foo]).to be("bar") + instance = render_to_document(React.create_element(Foo)) + expect(instance.state[:foo]).to be("bar") end it "should define init state by passing a block to `define_state`" do @@ -130,8 +126,8 @@ def set_up define_state(:foo) { 10 } end - element = renderToDocument(Foo) - expect(element.state[:foo]).to be(10) + instance = render_to_document(React.create_element(Foo)) + expect(instance.state[:foo]).to be(10) end it "should define getter using `define_state`" do @@ -143,8 +139,8 @@ def bump end end - element = renderToDocument(Foo) - expect(element.state[:foo]).to be(30) + instance = render_to_document(React.create_element(Foo)) + expect(instance.state[:foo]).to be(30) end it "should define multiple state accessor by passing symols array to `define_state`" do @@ -157,9 +153,9 @@ def set_up end end - element = renderToDocument(Foo) - expect(element.state[:foo]).to be(10) - expect(element.state[:foo2]).to be(20) + instance = render_to_document(React.create_element(Foo)) + expect(instance.state[:foo]).to be(10) + expect(instance.state[:foo2]).to be(20) end it "should invoke `define_state` multiple times to define states" do @@ -168,9 +164,9 @@ def set_up define_state(:foo2) { 40 } end - element = renderToDocument(Foo) - expect(element.state[:foo]).to be(30) - expect(element.state[:foo2]).to be(40) + instance = render_to_document(React.create_element(Foo)) + expect(instance.state[:foo]).to be(30) + expect(instance.state[:foo2]).to be(40) end it "should raise error if multiple states and block given at the same time" do @@ -189,8 +185,8 @@ def render end end - element = renderToDocument(Foo) - expect(element.dom_node.textContent).to eq("10") + instance = render_to_document(React.create_element(Foo)) + expect(instance.dom_node.textContent).to eq("10") end it "should support original `setState` as `set_state` method" do @@ -200,8 +196,8 @@ def render end end - element = renderToDocument(Foo) - expect(element.state[:foo]).to be("bar") + instance = render_to_document(React.create_element(Foo)) + expect(instance.state[:foo]).to be("bar") end it "should support originl `state` method" do @@ -251,8 +247,8 @@ def render end end - element = renderToDocument(Foo, prop: "foobar") - expect(element.dom_node.textContent).to eq("foobar") + instance = render_to_document(React.create_element(Foo, prop: "foobar")) + expect(instance.dom_node.textContent).to eq("foobar") end it "should access nested params as orignal Ruby object" do @@ -262,8 +258,8 @@ def render end end - element = renderToDocument(Foo, prop: [{foo: 10}]) - expect(element.dom_node.textContent).to eq("10") + instance = render_to_document(React.create_element(Foo, prop: [{foo: 10}])) + expect(instance.dom_node.textContent).to eq("10") end end @@ -303,7 +299,7 @@ def render; div; end var org_console = window.console; window.console = {warn: function(str){log.push(str)}} } - renderToDocument(Foo, bar: 10, lorem: Lorem.new) + render_to_document(React.create_element(Foo, bar: 10, lorem: Lorem.new)) `window.console = org_console;` expect(`log`).to eq(["Warning: Failed propType: In component `Foo`\nRequired prop `foo` was not specified\nProvided prop `bar` was not the specified type `String`"]) end @@ -325,7 +321,7 @@ def render; div; end var org_console = window.console; window.console = {warn: function(str){log.push(str)}} } - renderToDocument(Foo, foo: 10, bar: "10", lorem: Lorem.new) + render_to_document(React.create_element(Foo, foo: 10, bar: "10", lorem: Lorem.new)) `window.console = org_console;` expect(`log`).to eq([]) end @@ -371,9 +367,8 @@ def render end end - element = React.create_element(Foo) - instance = renderElementToDocument(element) - simulateEvent(:click, instance) + instance = render_to_document(React.create_element(Foo)) + simulate_event(:click, React.find_dom_node(instance)) expect(instance.state[:clicked]).to eq(true) end @@ -392,7 +387,7 @@ def render expect { |b| element = React.create_element(Foo).on(:foo_submit, &b) - renderElementToDocument(element) + render_to_document(element) }.to yield_with_args("bar") end @@ -411,7 +406,7 @@ def render expect { |b| element = React.create_element(Foo).on(:foo_invoked, &b) - renderElementToDocument(element) + render_to_document(element) }.to yield_with_args([1,2,3], "bar") end end @@ -431,8 +426,8 @@ def render end end - element = renderToDocument(Foo) - expect(element.refs[:field]).not_to be_nil + instance = render_to_document(React.create_element(Foo)) + expect(instance.refs[:field]).not_to be_nil end it "should access refs through `refs` method" do @@ -444,10 +439,10 @@ def render end end - element = renderToDocument(Foo) - simulateEvent(:click, element) + instance = render_to_document(React.create_element(Foo)) + simulate_event(:click, React.find_dom_node(instance)) - expect(element.refs[:field].value).to eq("some_stuff") + expect(instance.refs[:field].value).to eq("some_stuff") end end @@ -529,7 +524,7 @@ def render expect(Kernel).to receive(:p).with("first") expect(Kernel).to receive(:p).with("second") - renderToDocument(Foo) + render_to_document(React.create_element(Foo)) end end end diff --git a/spec/element_spec.rb b/spec/element_spec.rb index 6934842..70d71c3 100644 --- a/spec/element_spec.rb +++ b/spec/element_spec.rb @@ -54,20 +54,20 @@ it "should be subscribable through `on(:event_name)` method" do expect { |b| element = React.create_element("div").on(:click, &b) - instance = renderElementToDocument(element) - simulateEvent(:click, instance) + instance = render_to_document(element) + simulate_event(:click, React.find_dom_node(instance)) }.to yield_with_args(React::Event) expect { |b| element = React.create_element("div").on(:key_down, &b) - instance = renderElementToDocument(element) - simulateEvent(:keyDown, instance, {key: "Enter"}) + instance = render_to_document(element) + simulate_event(:keyDown, React.find_dom_node(instance), {key: "Enter"}) }.to yield_control expect { |b| element = React.create_element("form").on(:submit, &b) - instance = renderElementToDocument(element) - simulateEvent(:submit, instance, {}) + instance = render_to_document(element) + simulate_event(:submit, React.find_dom_node(instance), {}) }.to yield_control end diff --git a/spec/event_spec.rb b/spec/event_spec.rb index 40f3c7f..11a14e3 100644 --- a/spec/event_spec.rb +++ b/spec/event_spec.rb @@ -16,7 +16,7 @@ expect(event).to respond_to(:prevent_default) expect(event).to respond_to(:stop_propagation) end - instance = renderElementToDocument(element) - simulateEvent(:click, instance) + instance = render_to_document(element) + simulate_event(:click, React.find_dom_node(instance)) end -end \ No newline at end of file +end diff --git a/spec/react_spec.rb b/spec/react_spec.rb index 1b31d94..6ab108e 100644 --- a/spec/react_spec.rb +++ b/spec/react_spec.rb @@ -1,10 +1,6 @@ require "spec_helper" describe React do - after(:each) do - React::ComponentFactory.clear_component_class_cache - end - describe "is_valid_element" do it "should return true if passed a valid element" do element = `React.createElement('div')` @@ -105,8 +101,8 @@ def render end end - renderToDocument(Foo) - renderToDocument(Foo) + render_to_document(React.create_element(Foo)) + render_to_document(React.create_element(Foo)) expect(`count`).to eq(2) end diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index f932aa1..6ed1d37 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -1,27 +1,9 @@ require 'react' - -module ReactTestHelpers - `var ReactTestUtils = React.addons.TestUtils` - - def renderToDocument(type, options = {}) - element = React.create_element(type, options) - return renderElementToDocument(element) - end - - def renderElementToDocument(element) - `ReactTestUtils.renderIntoDocument(#{element})` - end - - def simulateEvent(event, component, params = {}) - simulator = Native(`ReactTestUtils.Simulate`) - simulator[event.to_s].call(`React.findDOMNode(#{component})`, params) - end - - def isElementOfType(element, type) - `React.addons.TestUtils.isElementOfType(#{element}, #{type.cached_component_class})` - end -end +require 'react/testing' RSpec.configure do |config| - config.include ReactTestHelpers + config.include React::Testing + config.after :each do + React::ComponentFactory.clear_component_class_cache + end end From c4208fd41dc1c157552c73a293a116ea601d57f2 Mon Sep 17 00:00:00 2001 From: David Chang Date: Fri, 3 Apr 2015 23:09:11 +0800 Subject: [PATCH 24/45] Update changelog --- CHANGELOG.md | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2219472..be51d9d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,7 +1,11 @@ +## 0.3.0 +* Add `React::Testing` helpers + + ## 0.2.0 * Deprecating `jsx` helper method in component instance * Deprecating `React::Element.new`, use `React.create_element` instead * `React::Element` is now toll-free bridge to [ReactElement](http://facebook.github.io/react/docs/glossary.html#react-elements) * `React::Element#props` now return a `Hash` rather than a `Native` -* `React::Element#children` now handling empty child & single child correctly \ No newline at end of file +* `React::Element#children` now handling empty child & single child correctly From aa3fd9a9e4a2db8806c58855886f9d4144a499c1 Mon Sep 17 00:00:00 2001 From: David Chang Date: Sun, 3 May 2015 23:28:44 +0800 Subject: [PATCH 25/45] native_alias all the life cycle methods --- lib/react/component_factory.rb | 5 +++++ spec/component_factory_spec.rb | 29 +++++++++++++++++++++++++++++ 2 files changed, 34 insertions(+) create mode 100644 spec/component_factory_spec.rb diff --git a/lib/react/component_factory.rb b/lib/react/component_factory.rb index 7f314d1..7cec376 100644 --- a/lib/react/component_factory.rb +++ b/lib/react/component_factory.rb @@ -10,6 +10,11 @@ def self.native_component_class(klass) klass.class_eval do native_alias :componentWillMount, :component_will_mount native_alias :componentDidMount, :component_did_mount + native_alias :componentWillReceiveProps, :component_will_receive_props + native_alias :shouldComponentUpdate, :should_component_update? + native_alias :componentWillUpdate, :component_will_update + native_alias :componentDidUpdate, :component_did_update + native_alias :componentWillUnmount, :component_will_unmount native_alias :render, :render end %x{ diff --git a/spec/component_factory_spec.rb b/spec/component_factory_spec.rb new file mode 100644 index 0000000..706cd57 --- /dev/null +++ b/spec/component_factory_spec.rb @@ -0,0 +1,29 @@ +require "spec_helper" + +describe React::ComponentFactory do + describe "native_component_class" do + it "should bridge the defined life cycle methods" do + stub_const 'Foo', Class.new + Foo.class_eval do + def component_will_mount; end + def component_did_mount; end + def component_will_receive_props; end + def should_component_update?; end + def component_will_update; end + def component_did_update; end + def component_will_unmount; end + end + + ctor = React::ComponentFactory.native_component_class(Foo) + instance = `new ctor` + expect(`instance.$component_will_mount`).to be(`instance.componentWillMount`) + expect(`instance.$component_did_mount`).to be(`instance.componentDidMount`) + expect(`instance.$component_will_receive_props`).to be(`instance.componentWillReceiveProps`) + expect(`instance.$should_component_update?`).to be(`instance.shouldComponentUpdate`) + expect(`instance.$component_will_update`).to be(`instance.componentWillUpdate`) + expect(`instance.$component_did_update`).to be(`instance.componentDidUpdate`) + expect(`instance.$component_will_unmount`).to be(`instance.componentWillUnmount`) + + end + end +end From 8916501177b59fbc0cd27946e73da7aeca25af25 Mon Sep 17 00:00:00 2001 From: David Chang Date: Sun, 3 May 2015 23:40:26 +0800 Subject: [PATCH 26/45] Deprecate React::Component::API#dom_node --- lib/react/component/api.rb | 9 ++------- spec/component_spec.rb | 6 +++--- 2 files changed, 5 insertions(+), 10 deletions(-) diff --git a/lib/react/component/api.rb b/lib/react/component/api.rb index 975e08d..b1e725f 100644 --- a/lib/react/component/api.rb +++ b/lib/react/component/api.rb @@ -21,14 +21,9 @@ def set_state(state, &block) } end - #FIXME: Should be deprecated in favor of sth like, React.find_dom_node(component) def dom_node - if self.respond_to?(:getDOMNode) - self.getDOMNode - else - Native(`React.findDOMNode(#{self})`) - end + raise "`dom_node` is deprecated in favor of `React.find_dom_node`" end end end -end \ No newline at end of file +end diff --git a/spec/component_spec.rb b/spec/component_spec.rb index 6c01838..a3dd9f6 100644 --- a/spec/component_spec.rb +++ b/spec/component_spec.rb @@ -186,7 +186,7 @@ def render end instance = render_to_document(React.create_element(Foo)) - expect(instance.dom_node.textContent).to eq("10") + expect(`#{React.find_dom_node(instance)}.textContent`).to eq("10") end it "should support original `setState` as `set_state` method" do @@ -248,7 +248,7 @@ def render end instance = render_to_document(React.create_element(Foo, prop: "foobar")) - expect(instance.dom_node.textContent).to eq("foobar") + expect(`#{React.find_dom_node(instance)}.textContent`).to eq("foobar") end it "should access nested params as orignal Ruby object" do @@ -259,7 +259,7 @@ def render end instance = render_to_document(React.create_element(Foo, prop: [{foo: 10}])) - expect(instance.dom_node.textContent).to eq("10") + expect(`#{React.find_dom_node(instance)}.textContent`).to eq("10") end end From fe108bff53539e0e9170794ad4d109b1e229379a Mon Sep 17 00:00:00 2001 From: David Chang Date: Sun, 3 May 2015 23:48:16 +0800 Subject: [PATCH 27/45] Move Object.assign shim to React::ComponentFactory --- lib/react/component_factory.rb | 31 +++++++++++++++++++++++++++++++ spec/reactjs/index.html.erb | 33 --------------------------------- 2 files changed, 31 insertions(+), 33 deletions(-) diff --git a/lib/react/component_factory.rb b/lib/react/component_factory.rb index 7cec376..a015791 100644 --- a/lib/react/component_factory.rb +++ b/lib/react/component_factory.rb @@ -18,6 +18,37 @@ def self.native_component_class(klass) native_alias :render, :render end %x{ + if (!Object.assign) { + Object.defineProperty(Object, 'assign', { + enumerable: false, + configurable: true, + writable: true, + value: function(target, firstSource) { + 'use strict'; + if (target === undefined || target === null) { + throw new TypeError('Cannot convert first argument to object'); + } + + var to = Object(target); + for (var i = 1; i < arguments.length; i++) { + var nextSource = arguments[i]; + if (nextSource === undefined || nextSource === null) { + continue; + } + + var keysArray = Object.keys(Object(nextSource)); + for (var nextIndex = 0, len = keysArray.length; nextIndex < len; nextIndex++) { + var nextKey = keysArray[nextIndex]; + var desc = Object.getOwnPropertyDescriptor(nextSource, nextKey); + if (desc !== undefined && desc.enumerable) { + to[nextKey] = nextSource[nextKey]; + } + } + } + return to; + } + }); + } function ctor(){ this.constructor = ctor; this.state = #{klass.respond_to?(:initial_state) ? klass.initial_state.to_n : `{}`}; diff --git a/spec/reactjs/index.html.erb b/spec/reactjs/index.html.erb index 1837d65..aee6199 100644 --- a/spec/reactjs/index.html.erb +++ b/spec/reactjs/index.html.erb @@ -5,39 +5,6 @@ <%= javascript_include_tag 'es5-shim.min' %> <%= javascript_include_tag 'react-with-addons' %> - <%= javascript_include_tag @server.main %> From 6a30839135419bc7db34dcc1ba404ba116c0ceef Mon Sep 17 00:00:00 2001 From: David Chang Date: Sun, 3 May 2015 23:58:50 +0800 Subject: [PATCH 28/45] Fix syntax error --- spec/component_factory_spec.rb | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/spec/component_factory_spec.rb b/spec/component_factory_spec.rb index 706cd57..71b1085 100644 --- a/spec/component_factory_spec.rb +++ b/spec/component_factory_spec.rb @@ -19,11 +19,10 @@ def component_will_unmount; end expect(`instance.$component_will_mount`).to be(`instance.componentWillMount`) expect(`instance.$component_did_mount`).to be(`instance.componentDidMount`) expect(`instance.$component_will_receive_props`).to be(`instance.componentWillReceiveProps`) - expect(`instance.$should_component_update?`).to be(`instance.shouldComponentUpdate`) + expect(`instance["$should_component_update?"]`).to be(`instance.shouldComponentUpdate`) expect(`instance.$component_will_update`).to be(`instance.componentWillUpdate`) expect(`instance.$component_did_update`).to be(`instance.componentDidUpdate`) expect(`instance.$component_will_unmount`).to be(`instance.componentWillUnmount`) - end end end From cafa213d9e06d8fff1787f17920b087e2ccaffdb Mon Sep 17 00:00:00 2001 From: David Chang Date: Sun, 3 May 2015 23:59:56 +0800 Subject: [PATCH 29/45] Fix test --- lib/react/top_level.rb | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/lib/react/top_level.rb b/lib/react/top_level.rb index e9c55f8..cbcaa1b 100644 --- a/lib/react/top_level.rb +++ b/lib/react/top_level.rb @@ -25,9 +25,11 @@ module React def self.create_element(type, properties = {}, &block) params = [] - + # Component Spec or Nomral DOM - if type.kind_of?(Class) + if `(typeof type === 'function')` + params << type + elsif type.kind_of?(Class) raise "Provided class should define `render` method" if !(type.method_defined? :render) params << React::ComponentFactory.native_component_class(type) else From 75896a8acefc7f20d9db612b4cb5ee3c94c5e0d1 Mon Sep 17 00:00:00 2001 From: David Chang Date: Sun, 10 May 2015 19:39:41 +0800 Subject: [PATCH 30/45] Move Opal Ruby code to opal folder --- lib/opal/react.rb | 7 +++++ lib/opal/react/jsx_support.rb | 22 +++++++++++++++ lib/opal/react/version.rb | 5 ++++ lib/react.rb | 36 ++++++++++++------------ lib/react/ext/jsx_support.rb | 21 -------------- lib/react/version.rb | 3 -- opal/react.rb | 6 ++++ {lib => opal}/react/callbacks.rb | 0 {lib => opal}/react/component.rb | 0 {lib => opal}/react/component/api.rb | 0 {lib => opal}/react/component_factory.rb | 0 {lib => opal}/react/element.rb | 0 {lib => opal}/react/event.rb | 0 {lib => opal}/react/ext/hash.rb | 0 {lib => opal}/react/ext/string.rb | 0 {lib => opal}/react/testing.rb | 0 {lib => opal}/react/top_level.rb | 0 {lib => opal}/react/validator.rb | 0 react.rb.gemspec | 4 +-- 19 files changed, 60 insertions(+), 44 deletions(-) create mode 100644 lib/opal/react.rb create mode 100644 lib/opal/react/jsx_support.rb create mode 100644 lib/opal/react/version.rb delete mode 100644 lib/react/ext/jsx_support.rb delete mode 100644 lib/react/version.rb create mode 100644 opal/react.rb rename {lib => opal}/react/callbacks.rb (100%) rename {lib => opal}/react/component.rb (100%) rename {lib => opal}/react/component/api.rb (100%) rename {lib => opal}/react/component_factory.rb (100%) rename {lib => opal}/react/element.rb (100%) rename {lib => opal}/react/event.rb (100%) rename {lib => opal}/react/ext/hash.rb (100%) rename {lib => opal}/react/ext/string.rb (100%) rename {lib => opal}/react/testing.rb (100%) rename {lib => opal}/react/top_level.rb (100%) rename {lib => opal}/react/validator.rb (100%) diff --git a/lib/opal/react.rb b/lib/opal/react.rb new file mode 100644 index 0000000..dd7483e --- /dev/null +++ b/lib/opal/react.rb @@ -0,0 +1,7 @@ +require 'opal' +require "opal-activesupport" + +require "opal/react/jsx_support" + +Opal.append_path File.expand_path('../../../opal', __FILE__).untaint +Opal.append_path File.expand_path('../../../vendor', __FILE__).untaint diff --git a/lib/opal/react/jsx_support.rb b/lib/opal/react/jsx_support.rb new file mode 100644 index 0000000..51efec9 --- /dev/null +++ b/lib/opal/react/jsx_support.rb @@ -0,0 +1,22 @@ +require 'react/jsx' +require 'tilt' +require "sprockets" + +module Opal + module React + module JSX + class Template < Tilt::Template + self.default_mime_type = 'application/javascript' + + def prepare + end + + def evaluate(scope, locals, &block) + @output ||= ::React::JSX.compile(data) + end + end + end + end +end + +Sprockets.register_engine '.jsx', Opal::React::JSX::Template diff --git a/lib/opal/react/version.rb b/lib/opal/react/version.rb new file mode 100644 index 0000000..a95415a --- /dev/null +++ b/lib/opal/react/version.rb @@ -0,0 +1,5 @@ +module Opal + module React + VERSION = "0.3.0" + end +end diff --git a/lib/react.rb b/lib/react.rb index 8381123..cc5f398 100644 --- a/lib/react.rb +++ b/lib/react.rb @@ -1,18 +1,18 @@ -if RUBY_ENGINE == 'opal' - require "react/top_level" - require "react/component" - require "react/element" - require "react/event" - require "react/version" - require "react/component_factory" - require "react/validator" -else - require "tilt" - require "opal" - require "react/version" - require "opal-activesupport" - require "react/ext/jsx_support" - - Opal.append_path File.expand_path('../', __FILE__).untaint - Opal.append_path File.expand_path('../../vendor', __FILE__).untaint -end +# if RUBY_ENGINE == 'opal' +# require "react/top_level" +# require "react/component" +# require "react/element" +# require "react/event" +# require "react/version" +# require "react/component_factory" +# require "react/validator" +# else +# require "tilt" +# require "opal" +# require "react/version" +# require "opal-activesupport" +# require "react/ext/jsx_support" +require "opal/react" +# Opal.append_path File.expand_path('../', __FILE__).untaint +# Opal.append_path File.expand_path('../../vendor', __FILE__).untaint +#end diff --git a/lib/react/ext/jsx_support.rb b/lib/react/ext/jsx_support.rb deleted file mode 100644 index 7734f5a..0000000 --- a/lib/react/ext/jsx_support.rb +++ /dev/null @@ -1,21 +0,0 @@ -require 'react/jsx' -require 'tilt' -require "sprockets" - -module React - module JSX - - class Template < Tilt::Template - self.default_mime_type = 'application/javascript' - - def prepare - end - - def evaluate(scope, locals, &block) - @output ||= React::JSX.compile(data) - end - end - end -end - -Sprockets.register_engine '.jsx', React::JSX::Template diff --git a/lib/react/version.rb b/lib/react/version.rb deleted file mode 100644 index 50ba0cd..0000000 --- a/lib/react/version.rb +++ /dev/null @@ -1,3 +0,0 @@ -module React - VERSION = "0.3.0" -end diff --git a/opal/react.rb b/opal/react.rb new file mode 100644 index 0000000..2f30dbb --- /dev/null +++ b/opal/react.rb @@ -0,0 +1,6 @@ +require "react/top_level" +require "react/component" +require "react/element" +require "react/event" +require "react/component_factory" +require "react/validator" diff --git a/lib/react/callbacks.rb b/opal/react/callbacks.rb similarity index 100% rename from lib/react/callbacks.rb rename to opal/react/callbacks.rb diff --git a/lib/react/component.rb b/opal/react/component.rb similarity index 100% rename from lib/react/component.rb rename to opal/react/component.rb diff --git a/lib/react/component/api.rb b/opal/react/component/api.rb similarity index 100% rename from lib/react/component/api.rb rename to opal/react/component/api.rb diff --git a/lib/react/component_factory.rb b/opal/react/component_factory.rb similarity index 100% rename from lib/react/component_factory.rb rename to opal/react/component_factory.rb diff --git a/lib/react/element.rb b/opal/react/element.rb similarity index 100% rename from lib/react/element.rb rename to opal/react/element.rb diff --git a/lib/react/event.rb b/opal/react/event.rb similarity index 100% rename from lib/react/event.rb rename to opal/react/event.rb diff --git a/lib/react/ext/hash.rb b/opal/react/ext/hash.rb similarity index 100% rename from lib/react/ext/hash.rb rename to opal/react/ext/hash.rb diff --git a/lib/react/ext/string.rb b/opal/react/ext/string.rb similarity index 100% rename from lib/react/ext/string.rb rename to opal/react/ext/string.rb diff --git a/lib/react/testing.rb b/opal/react/testing.rb similarity index 100% rename from lib/react/testing.rb rename to opal/react/testing.rb diff --git a/lib/react/top_level.rb b/opal/react/top_level.rb similarity index 100% rename from lib/react/top_level.rb rename to opal/react/top_level.rb diff --git a/lib/react/validator.rb b/opal/react/validator.rb similarity index 100% rename from lib/react/validator.rb rename to opal/react/validator.rb diff --git a/react.rb.gemspec b/react.rb.gemspec index a7ae41c..bc2f022 100644 --- a/react.rb.gemspec +++ b/react.rb.gemspec @@ -1,9 +1,9 @@ # -*- encoding: utf-8 -*- -require File.expand_path('../lib/react/version', __FILE__) +require File.expand_path('../lib/opal/react/version', __FILE__) Gem::Specification.new do |s| s.name = 'react.rb' - s.version = React::VERSION + s.version = Opal::React::VERSION s.author = 'David Chang' s.email = 'zeta11235813@gmail.com' s.homepage = 'https://github.com/zetachang/react.rb' From 7e26ed6e4f1e1ff69a6a8d8d36d0c1dcd45924d2 Mon Sep 17 00:00:00 2001 From: David Chang Date: Sun, 10 May 2015 19:54:44 +0800 Subject: [PATCH 31/45] Make react-source as runtime dependency --- Gemfile.lock | 2 +- Rakefile | 5 ++--- config.ru | 3 +-- lib/opal/react.rb | 1 + lib/opal/react/source.rb | 9 +++++++++ react.rb.gemspec | 3 ++- 6 files changed, 16 insertions(+), 7 deletions(-) create mode 100644 lib/opal/react/source.rb diff --git a/Gemfile.lock b/Gemfile.lock index 81c7329..de4cc74 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -5,6 +5,7 @@ PATH opal (~> 0.6.0) opal-activesupport (~> 0) react-jsx (~> 0.8.0) + react-source (~> 0.13) sprockets (>= 2.2.3, < 3.0.0) therubyracer (~> 0) @@ -58,6 +59,5 @@ DEPENDENCIES opal-jquery (~> 0) opal-rspec (~> 0.3.0.beta3) rake (~> 10) - react-source (~> 0.13) react.rb! sinatra (~> 1) diff --git a/Rakefile b/Rakefile index d4c0f54..7845cae 100644 --- a/Rakefile +++ b/Rakefile @@ -3,10 +3,9 @@ Bundler.require Bundler::GemHelper.install_tasks require 'opal/rspec/rake_task' -require "react/source" Opal::RSpec::RakeTask.new(:default) do |s| - s.append_path File.dirname(::React::Source.bundled_path_for("react-with-addons")) + s.append_path Opal::React.bundled_path s.append_path 'spec/vendor' s.index_path = 'spec/reactjs/index.html.erb' -end \ No newline at end of file +end diff --git a/config.ru b/config.ru index 1a647c1..ab9515e 100644 --- a/config.ru +++ b/config.ru @@ -2,7 +2,6 @@ require 'bundler' Bundler.require require "opal-rspec" -require "react/source" Opal.append_path File.expand_path('../spec', __FILE__) @@ -10,7 +9,7 @@ run Opal::Server.new { |s| s.main = 'opal/rspec/sprockets_runner' s.append_path 'spec' s.append_path 'spec/vendor' - s.append_path File.dirname(::React::Source.bundled_path_for("react-with-addons.js")) + s.append_path Opal::React.bundled_path s.debug = true s.index_path = 'spec/reactjs/index.html.erb' } diff --git a/lib/opal/react.rb b/lib/opal/react.rb index dd7483e..9adf9ba 100644 --- a/lib/opal/react.rb +++ b/lib/opal/react.rb @@ -2,6 +2,7 @@ require "opal-activesupport" require "opal/react/jsx_support" +require "opal/react/source" Opal.append_path File.expand_path('../../../opal', __FILE__).untaint Opal.append_path File.expand_path('../../../vendor', __FILE__).untaint diff --git a/lib/opal/react/source.rb b/lib/opal/react/source.rb new file mode 100644 index 0000000..12db113 --- /dev/null +++ b/lib/opal/react/source.rb @@ -0,0 +1,9 @@ +require "react/source" + +module Opal + module React + def self.bundled_path + File.dirname(::React::Source.bundled_path_for("react-with-addons.js")) + end + end +end diff --git a/react.rb.gemspec b/react.rb.gemspec index bc2f022..db5748d 100644 --- a/react.rb.gemspec +++ b/react.rb.gemspec @@ -21,7 +21,8 @@ Gem::Specification.new do |s| s.add_runtime_dependency 'therubyracer', '~> 0' s.add_runtime_dependency 'react-jsx', '~> 0.8.0' s.add_runtime_dependency 'sprockets', '>= 2.2.3', '< 3.0.0' - s.add_development_dependency 'react-source', '~> 0.13' + s.add_runtime_dependency 'react-source', '~> 0.13' + s.add_development_dependency 'opal-rspec', '~> 0.3.0.beta3' s.add_development_dependency 'sinatra', '~> 1' s.add_development_dependency 'opal-jquery', '~> 0' From f4302a62a7882146b537c7659b6e838b8bae5fbb Mon Sep 17 00:00:00 2001 From: David Chang Date: Sun, 10 May 2015 20:28:01 +0800 Subject: [PATCH 32/45] camelize all props passed in React#create_element --- opal/react/component.rb | 7 +++++-- opal/react/element.rb | 5 ++--- opal/react/ext/string.rb | 10 +++++++++- opal/react/top_level.rb | 13 +------------ spec/react_spec.rb | 7 ++++--- 5 files changed, 21 insertions(+), 21 deletions(-) diff --git a/opal/react/component.rb b/opal/react/component.rb index 415f56c..bb4d13b 100644 --- a/opal/react/component.rb +++ b/opal/react/component.rb @@ -22,7 +22,10 @@ def self.included(base) end def params - Hash.new(`#{self}.props`) + Hash.new(`#{self}.props`).inject({}) do |memo, (k,v)| + memo[k.underscore] = v + memo + end end def refs @@ -30,7 +33,7 @@ def refs end def emit(event_name, *args) - self.params["_on#{event_name.to_s.event_camelize}"].call(*args) + self.params["on_#{event_name.to_s}"].call(*args) end def component_will_mount diff --git a/opal/react/element.rb b/opal/react/element.rb index 9d87e09..4021c2d 100644 --- a/opal/react/element.rb +++ b/opal/react/element.rb @@ -25,16 +25,15 @@ def ref def on(event_name) name = event_name.to_s.event_camelize + prop_key = "on#{name}" - if React::Event::BUILT_IN_EVENTS.include?("on#{name}") - prop_key = "on#{name}" + if React::Event::BUILT_IN_EVENTS.include?(prop_key) callback = %x{ function(event){ #{yield React::Event.new(`event`)} } } else - prop_key = "_on#{name}" callback = %x{ function(){ #{yield *Array(`arguments`)} diff --git a/opal/react/ext/string.rb b/opal/react/ext/string.rb index 32122e0..728331e 100644 --- a/opal/react/ext/string.rb +++ b/opal/react/ext/string.rb @@ -6,6 +6,7 @@ def event_camelize })` end + # 'class_name' => 'className' def lower_camelize # TODO Could be implemented more efficiently words = self.split("_") @@ -13,4 +14,11 @@ def lower_camelize result.concat(words[1..-1].map {|word| word[0].upcase + word[1..-1] }) result.join("") end -end \ No newline at end of file + + # 'className' => 'class_name' + def underscore + return `self.replace(/[A-Z]/g, function(){ + return '_' + arguments[0].toLowerCase(); + });` + end +end diff --git a/opal/react/top_level.rb b/opal/react/top_level.rb index cbcaa1b..b2bb761 100644 --- a/opal/react/top_level.rb +++ b/opal/react/top_level.rb @@ -11,17 +11,6 @@ module React output p param picture pre progress q rp rt ruby s samp script section select small source span strong style sub summary sup table tbody td textarea tfoot th thead time title tr track u ul var video wbr) - ATTRIBUTES = %w(accept acceptCharset accessKey action allowFullScreen allowTransparency alt - async autoComplete autoPlay cellPadding cellSpacing charSet checked classID - className cols colSpan content contentEditable contextMenu controls coords - crossOrigin data dateTime defer dir disabled download draggable encType form - formAction formEncType formMethod formNoValidate formTarget frameBorder height - hidden href hrefLang htmlFor httpEquiv icon id label lang list loop manifest - marginHeight marginWidth max maxLength media mediaGroup method min multiple - muted name noValidate open pattern placeholder poster preload radioGroup - readOnly rel required role rows rowSpan sandbox scope scrolling seamless - selected shape size sizes span spellCheck src srcDoc srcSet start step style - tabIndex target title type useMap value width wmode dangerouslySetInnerHTML) def self.create_element(type, properties = {}, &block) params = [] @@ -43,7 +32,7 @@ def self.create_element(type, properties = {}, &block) if key == "class_name" && value.is_a?(Hash) props[key.lower_camelize] = value.inject([]) {|ary, (k,v)| v ? ary.push(k) : ary}.join(" ") else - props[React::ATTRIBUTES.include?(key.lower_camelize) ? key.lower_camelize : key] = value + props[key.lower_camelize] = value end end params << props.shallow_to_n diff --git a/spec/react_spec.rb b/spec/react_spec.rb index 586ee41..1ac2610 100644 --- a/spec/react_spec.rb +++ b/spec/react_spec.rb @@ -129,9 +129,10 @@ def render expect(element.props[:foo]).to eq("bar") end - it "should not camel-case custom property" do - element = React.create_element("div", foo_bar: "foo") - expect(element.props[:foo_bar]).to eq("foo") + it "should camel-case all property" do + element = React.create_element("div", foo_bar: "foo", class_name: 'fancy') + expect(element.props[:fooBar]).to eq("foo") + expect(element.props[:className]).to eq("fancy") end end From 2b593eb86f58376a1e59aa6e241a8382ce1356fd Mon Sep 17 00:00:00 2001 From: David Chang Date: Sun, 10 May 2015 20:39:11 +0800 Subject: [PATCH 33/45] Use React.cloneElement to implement event subscription #18 --- opal/react/element.rb | 7 +++++-- spec/element_spec.rb | 9 ++++++--- 2 files changed, 11 insertions(+), 5 deletions(-) diff --git a/opal/react/element.rb b/opal/react/element.rb index 4021c2d..9213887 100644 --- a/opal/react/element.rb +++ b/opal/react/element.rb @@ -41,9 +41,12 @@ def on(event_name) } end - `self.props[#{prop_key}] = #{callback}` + new_prop = `{}` + `new_prop[prop_key] = callback` - self + new_element = `React.cloneElement(#{self}, #{new_prop})` + + return new_element end def children diff --git a/spec/element_spec.rb b/spec/element_spec.rb index 70d71c3..f57cd1d 100644 --- a/spec/element_spec.rb +++ b/spec/element_spec.rb @@ -71,9 +71,12 @@ }.to yield_control end - it "should return self for `on` method" do - element = React.create_element("div") - expect(element.on(:click){}).to eq(element) + it "should return the copied ReactElement for `on` method" do + element = React.create_element("div", {foo: "bar"}) + + new_element = element.on(:click){} + expect(new_element).to be_a(React::Element) + expect(new_element.props[:foo]).to eq('bar') end end From f08b8f8856562bfa306b9c7f1c88c5ce238773c1 Mon Sep 17 00:00:00 2001 From: David Chang Date: Sun, 10 May 2015 20:59:38 +0800 Subject: [PATCH 34/45] Props will be passed as the first argument of initializer --- README.md | 9 +++++---- opal/react/component_factory.rb | 5 +++-- 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 1bb5c61..709cbea 100644 --- a/README.md +++ b/README.md @@ -46,12 +46,12 @@ puts React.render_to_static_markup(React.create_element(HelloMessage)) ### More complicated one -To hook into native ReactComponent life cycle, the native `this` will be passed to the class's initializer. And all corresponding life cycle methods (`componentDidMount`, etc) will be invoked on the instance using the snake-case method name. +To hook into native ReactComponent life cycle, the props will be passed as the first argument to the class's initializer. And all corresponding life cycle methods (`componentDidMount`, etc) will be invoked on the instance using the snake-case method name. ```ruby class HelloMessage - def initialize(native) - @native = Native(native) + def initialize(props) + puts props end def component_will_mount @@ -59,12 +59,13 @@ class HelloMessage end def render - React.create_element("div") { "Hello #{@native[:props][:name]}!" } + React.create_element("div") { "Hello #{self.props[:name]}!" } end end puts React.render_to_static_markup(React.create_element(HelloMessage, name: 'John')) +# => {"name"=>"John"} # => will_mount! # => '
Hello John!
' ``` diff --git a/opal/react/component_factory.rb b/opal/react/component_factory.rb index a015791..e9b4eef 100644 --- a/opal/react/component_factory.rb +++ b/opal/react/component_factory.rb @@ -8,6 +8,7 @@ def self.clear_component_class_cache def self.native_component_class(klass) klass.class_eval do + include(React::Component::API) native_alias :componentWillMount, :component_will_mount native_alias :componentDidMount, :component_did_mount native_alias :componentWillReceiveProps, :component_will_receive_props @@ -49,11 +50,11 @@ def self.native_component_class(klass) } }); } - function ctor(){ + function ctor(props){ this.constructor = ctor; this.state = #{klass.respond_to?(:initial_state) ? klass.initial_state.to_n : `{}`}; React.Component.apply(this, arguments); - #{klass}._alloc.prototype.$initialize.call(this); + #{klass}._alloc.prototype.$initialize.call(this, Opal.Hash.$new(props)); }; ctor.prototype = klass._proto; Object.assign(ctor.prototype, React.Component.prototype); From 688fcfa5a7e06c62d538e399dc091e08aa0d77f5 Mon Sep 17 00:00:00 2001 From: David Chang Date: Sun, 10 May 2015 21:09:57 +0800 Subject: [PATCH 35/45] Clarify integration with react-source --- README.md | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/README.md b/README.md index 709cbea..b297442 100644 --- a/README.md +++ b/README.md @@ -26,6 +26,27 @@ React.render(React.create_element('h1'){ "Hello World!" }, `document.body`) For integration with server (Sinatra, etc), see setup of [TodoMVC](example/todos) or the [official docs](http://opalrb.org/docs/) of Opal. +## React.js Dependency + +React.js v0.13 is required to use react.rb, you can access the pre-bundled source through `Opal::React.bundled_path` directory, example below shows setup for a basic rack app. + +```ruby +#config.ru +run Opal::Server.new {|s| + s.append_path './' + s.append_path Opal::React.bundled_path + s.main = 'example' + s.debug = true + s.index_path = "index.html.erb" +} +``` + +```erb + +<%= javascript_include_tag "react" %> +<%= javascript_include_tag "example" %> +``` + ## Usage ### A Simple Component From 3f80fea7f71a93be31040085e0be6989d0c3edef Mon Sep 17 00:00:00 2001 From: Brady Wied Date: Tue, 12 May 2015 16:47:26 -0600 Subject: [PATCH 36/45] low hanging fruit/opal 0.8 fixes --- opal/react/component.rb | 2 +- opal/react/element.rb | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/opal/react/component.rb b/opal/react/component.rb index bb4d13b..c18d6e7 100644 --- a/opal/react/component.rb +++ b/opal/react/component.rb @@ -1,4 +1,4 @@ -require "./ext/string" +require "react/ext/string" require 'active_support/core_ext/class/attribute' require 'react/callbacks' require "react/ext/hash" diff --git a/opal/react/element.rb b/opal/react/element.rb index 9213887..ebf717b 100644 --- a/opal/react/element.rb +++ b/opal/react/element.rb @@ -1,4 +1,4 @@ -require "./ext/string" +require "react/ext/string" module React class Element < `(function(){var r = React;var f = function(){};var c = r.createClass({render:function(){return null;}});f.prototype = Object.getPrototypeOf(r.createElement(c));return f;})()` From e6d38bac6e9f41ccac5f8f85f3dde35f1960f3f4 Mon Sep 17 00:00:00 2001 From: Brady Wied Date: Wed, 13 May 2015 18:03:57 -0600 Subject: [PATCH 37/45] Make these tests more robust (fix the console object back in case the assertion fails) --- spec/component_spec.rb | 20 ++++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) diff --git a/spec/component_spec.rb b/spec/component_spec.rb index a3dd9f6..6a642cd 100644 --- a/spec/component_spec.rb +++ b/spec/component_spec.rb @@ -299,9 +299,14 @@ def render; div; end var org_console = window.console; window.console = {warn: function(str){log.push(str)}} } - render_to_document(React.create_element(Foo, bar: 10, lorem: Lorem.new)) - `window.console = org_console;` - expect(`log`).to eq(["Warning: Failed propType: In component `Foo`\nRequired prop `foo` was not specified\nProvided prop `bar` was not the specified type `String`"]) + + begin + render_to_document(React.create_element(Foo, bar: 10, lorem: Lorem.new)) + + expect(`log`).to eq(["Warning: Failed propType: In component `Foo`\nRequired prop `foo` was not specified\nProvided prop `bar` was not the specified type `String`"]) + ensure + `window.console = org_console;` + end end it "should not log anything if validation pass" do @@ -321,9 +326,12 @@ def render; div; end var org_console = window.console; window.console = {warn: function(str){log.push(str)}} } - render_to_document(React.create_element(Foo, foo: 10, bar: "10", lorem: Lorem.new)) - `window.console = org_console;` - expect(`log`).to eq([]) + begin + render_to_document(React.create_element(Foo, foo: 10, bar: "10", lorem: Lorem.new)) + expect(`log`).to eq([]) + ensure + `window.console = org_console;` + end end end From 78eea540085ae68b17ff9f4dac6036985817565c Mon Sep 17 00:00:00 2001 From: Brady Wied Date: Wed, 13 May 2015 18:17:57 -0600 Subject: [PATCH 38/45] Adapt to new Opal 0.8 hidden methods --- opal/react/component_factory.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/opal/react/component_factory.rb b/opal/react/component_factory.rb index e9b4eef..fb0b2e2 100644 --- a/opal/react/component_factory.rb +++ b/opal/react/component_factory.rb @@ -54,9 +54,9 @@ def self.native_component_class(klass) this.constructor = ctor; this.state = #{klass.respond_to?(:initial_state) ? klass.initial_state.to_n : `{}`}; React.Component.apply(this, arguments); - #{klass}._alloc.prototype.$initialize.call(this, Opal.Hash.$new(props)); + #{klass}.$$alloc.prototype.$initialize.call(this, Opal.Hash.$new(props)); }; - ctor.prototype = klass._proto; + ctor.prototype = klass.$$proto; Object.assign(ctor.prototype, React.Component.prototype); ctor.propTypes = #{klass.respond_to?(:prop_types) ? klass.prop_types.to_n : `{}`}; ctor.defaultProps = #{klass.respond_to?(:default_props) ? klass.default_props.to_n : `{}`}; From aad1557023f6659cb4b3bf557b9aa0e120b646e9 Mon Sep 17 00:00:00 2001 From: Brady Wied Date: Wed, 13 May 2015 18:31:55 -0600 Subject: [PATCH 39/45] Adapt to the fact that native_alias fails if some of the methods aren't implemented --- opal/react/component_factory.rb | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/opal/react/component_factory.rb b/opal/react/component_factory.rb index fb0b2e2..f0c5795 100644 --- a/opal/react/component_factory.rb +++ b/opal/react/component_factory.rb @@ -9,13 +9,18 @@ def self.clear_component_class_cache def self.native_component_class(klass) klass.class_eval do include(React::Component::API) - native_alias :componentWillMount, :component_will_mount - native_alias :componentDidMount, :component_did_mount - native_alias :componentWillReceiveProps, :component_will_receive_props - native_alias :shouldComponentUpdate, :should_component_update? - native_alias :componentWillUpdate, :component_will_update - native_alias :componentDidUpdate, :component_did_update - native_alias :componentWillUnmount, :component_will_unmount + # In Opal 0.8, native_alias fails if the method isn't there but we don't want to force all of these to be implemented + optional_native_alias = lambda do |js, ruby| + not_there = `!(#{self}.$$proto['$' + #{ruby}])` + native_alias js, ruby unless not_there + end + optional_native_alias[:componentWillMount, :component_will_mount] + optional_native_alias[:componentDidMount, :component_did_mount] + optional_native_alias[:componentWillReceiveProps, :component_will_receive_props] + optional_native_alias[:shouldComponentUpdate, :should_component_update?] + optional_native_alias[:componentWillUpdate, :component_will_update] + optional_native_alias[:componentDidUpdate, :component_did_update] + optional_native_alias[:componentWillUnmount, :component_will_unmount] native_alias :render, :render end %x{ From 1a93972bfa9d92c37f4bfbb67bd4a9daa84562cb Mon Sep 17 00:00:00 2001 From: David Chang Date: Wed, 10 Jun 2015 22:35:50 +0800 Subject: [PATCH 40/45] Update opal / sprockets / opal-rspec --- Gemfile.lock | 33 +++++++++++++++------------------ react.rb.gemspec | 6 +++--- 2 files changed, 18 insertions(+), 21 deletions(-) diff --git a/Gemfile.lock b/Gemfile.lock index de4cc74..c072fa6 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -2,11 +2,11 @@ PATH remote: . specs: react.rb (0.3.0) - opal (~> 0.6.0) + opal (>= 0.7.0, < 0.9) opal-activesupport (~> 0) react-jsx (~> 0.8.0) react-source (~> 0.13) - sprockets (>= 2.2.3, < 3.0.0) + sprockets (~> 3.1) therubyracer (~> 0) GEM @@ -14,19 +14,20 @@ GEM specs: execjs (2.5.2) hike (1.2.3) - json (1.8.2) + json (1.8.3) libv8 (3.16.14.7) - multi_json (1.11.0) - opal (0.6.3) - source_map - sprockets + opal (0.7.1) + hike (~> 1.2) + sourcemap (~> 0.1.0) + sprockets (>= 2.2.3, < 4.0.0) + tilt (~> 1.4) opal-activesupport (0.1.0) opal (>= 0.5.0, < 1.0.0) opal-jquery (0.2.0) opal (>= 0.5.0, < 1.0.0) - opal-rspec (0.3.0.beta3) - opal (>= 0.6.0, < 1.0.0) - rack (1.6.0) + opal-rspec (0.4.2) + opal (~> 0.7.0) + rack (1.6.1) rack-protection (1.5.3) rack rake (10.4.2) @@ -34,19 +35,15 @@ GEM execjs (>= 2.0.2) json (>= 1.8.0) react-source (>= 0.4.1) - react-source (0.13.2) + react-source (0.13.3) ref (1.0.5) sinatra (1.4.6) rack (~> 1.4) rack-protection (~> 1.4) tilt (>= 1.3, < 3) - source_map (3.0.1) - json - sprockets (2.12.3) - hike (~> 1.2) - multi_json (~> 1.0) + sourcemap (0.1.1) + sprockets (3.2.0) rack (~> 1.0) - tilt (~> 1.1, != 1.3.0) therubyracer (0.12.2) libv8 (~> 3.16.14.0) ref @@ -57,7 +54,7 @@ PLATFORMS DEPENDENCIES opal-jquery (~> 0) - opal-rspec (~> 0.3.0.beta3) + opal-rspec (~> 0.4.2) rake (~> 10) react.rb! sinatra (~> 1) diff --git a/react.rb.gemspec b/react.rb.gemspec index db5748d..326549f 100644 --- a/react.rb.gemspec +++ b/react.rb.gemspec @@ -16,14 +16,14 @@ Gem::Specification.new do |s| s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n") s.require_paths = ['lib', 'vendor'] - s.add_runtime_dependency 'opal', '~> 0.6.0' + s.add_runtime_dependency 'opal', ['>= 0.7.0', '< 0.9'] s.add_runtime_dependency 'opal-activesupport', '~> 0' s.add_runtime_dependency 'therubyracer', '~> 0' s.add_runtime_dependency 'react-jsx', '~> 0.8.0' - s.add_runtime_dependency 'sprockets', '>= 2.2.3', '< 3.0.0' + s.add_runtime_dependency 'sprockets', '~> 3.1' s.add_runtime_dependency 'react-source', '~> 0.13' - s.add_development_dependency 'opal-rspec', '~> 0.3.0.beta3' + s.add_development_dependency 'opal-rspec', '~> 0.4.2' s.add_development_dependency 'sinatra', '~> 1' s.add_development_dependency 'opal-jquery', '~> 0' s.add_development_dependency 'rake', '~> 10' From 5d359b73f67e1e99bf0141330acfadfc84aa2b3f Mon Sep 17 00:00:00 2001 From: David Chang Date: Sun, 14 Jun 2015 10:40:54 +0800 Subject: [PATCH 41/45] Fix displayName not set properly regression --- opal/react/component_factory.rb | 1 + 1 file changed, 1 insertion(+) diff --git a/opal/react/component_factory.rb b/opal/react/component_factory.rb index f0c5795..394698a 100644 --- a/opal/react/component_factory.rb +++ b/opal/react/component_factory.rb @@ -62,6 +62,7 @@ def self.native_component_class(klass) #{klass}.$$alloc.prototype.$initialize.call(this, Opal.Hash.$new(props)); }; ctor.prototype = klass.$$proto; + ctor.displayName = #{klass.to_s}; Object.assign(ctor.prototype, React.Component.prototype); ctor.propTypes = #{klass.respond_to?(:prop_types) ? klass.prop_types.to_n : `{}`}; ctor.defaultProps = #{klass.respond_to?(:default_props) ? klass.default_props.to_n : `{}`}; From 3f9560b823647dd90e02ade59990a9c979c1f560 Mon Sep 17 00:00:00 2001 From: David Chang Date: Sun, 14 Jun 2015 16:19:03 +0800 Subject: [PATCH 42/45] React.render now return the original component instance --- opal/react/top_level.rb | 4 +--- spec/react_spec.rb | 8 -------- 2 files changed, 1 insertion(+), 11 deletions(-) diff --git a/opal/react/top_level.rb b/opal/react/top_level.rb index b2bb761..45b5e8b 100644 --- a/opal/react/top_level.rb +++ b/opal/react/top_level.rb @@ -48,9 +48,7 @@ def self.create_element(type, properties = {}, &block) end def self.render(element, container) - component = Native(`React.render(#{element}, container, function(){#{yield if block_given?}})`) - component.class.include(React::Component::API) - component + `React.render(#{element}, container, function(){#{yield if block_given?}})` end def self.is_valid_element(element) diff --git a/spec/react_spec.rb b/spec/react_spec.rb index 1ac2610..a5547cb 100644 --- a/spec/react_spec.rb +++ b/spec/react_spec.rb @@ -169,14 +169,6 @@ def render React.render(React.create_element('span') { "lorem" }, div) end - it "should return a React::Component::API compatible object" do - div = `document.createElement("div")` - component = React.render(React.create_element('span') { "lorem" }, div) - React::Component::API.public_instance_methods(true).each do |method_name| - expect(component).to respond_to(method_name) - end - end - pending "should return nil to prevent abstraction leakage" do div = `document.createElement("div")` expect { From d29b79f0eb953517dc8119460db99a5e505d3fbc Mon Sep 17 00:00:00 2001 From: David Chang Date: Sun, 14 Jun 2015 16:20:20 +0800 Subject: [PATCH 43/45] React::Component::API#refs return a shallow Ruby hash now --- opal/react/component.rb | 4 ---- opal/react/component/api.rb | 15 +++++++++++++++ spec/component_spec.rb | 7 ++++--- 3 files changed, 19 insertions(+), 7 deletions(-) diff --git a/opal/react/component.rb b/opal/react/component.rb index c18d6e7..4f76d4c 100644 --- a/opal/react/component.rb +++ b/opal/react/component.rb @@ -28,10 +28,6 @@ def params end end - def refs - Hash.new(`#{self}.refs`) - end - def emit(event_name, *args) self.params["on_#{event_name.to_s}"].call(*args) end diff --git a/opal/react/component/api.rb b/opal/react/component/api.rb index b1e725f..3c726c9 100644 --- a/opal/react/component/api.rb +++ b/opal/react/component/api.rb @@ -20,6 +20,21 @@ def set_state(state, &block) }); } end + + def refs + hash = {} + + %x{ + var refs = self.refs; + for (var property in refs) { + if (refs.hasOwnProperty(property)) { + #{hash[`property`] = `refs[property]`} + } + } + } + + hash + end def dom_node raise "`dom_node` is deprecated in favor of `React.find_dom_node`" diff --git a/spec/component_spec.rb b/spec/component_spec.rb index 6a642cd..2dd3b94 100644 --- a/spec/component_spec.rb +++ b/spec/component_spec.rb @@ -435,14 +435,15 @@ def render end instance = render_to_document(React.create_element(Foo)) - expect(instance.refs[:field]).not_to be_nil + expect(`#{React.find_dom_node(instance.refs[:field])}.tagName`).to eq('INPUT') end it "should access refs through `refs` method" do Foo.class_eval do def render React.create_element("input", type: :text, ref: :field).on(:click) do - refs[:field].value = "some_stuff" + input_field = Native(React.find_dom_node(refs[:field])) + input_field.value = "some_stuff" end end end @@ -450,7 +451,7 @@ def render instance = render_to_document(React.create_element(Foo)) simulate_event(:click, React.find_dom_node(instance)) - expect(instance.refs[:field].value).to eq("some_stuff") + expect(`#{React.find_dom_node(instance.refs[:field])}.value`).to eq("some_stuff") end end From 01406a22a99b34f0a3619a17789dbbd6f519ca0d Mon Sep 17 00:00:00 2001 From: David Chang Date: Sun, 14 Jun 2015 16:25:01 +0800 Subject: [PATCH 44/45] Upgrade to opal '0.8.0.rc1' --- Gemfile | 4 +++- Gemfile.lock | 20 +++++++++++++------- react.rb.gemspec | 1 - 3 files changed, 16 insertions(+), 9 deletions(-) diff --git a/Gemfile b/Gemfile index d926697..08edb90 100644 --- a/Gemfile +++ b/Gemfile @@ -1,2 +1,4 @@ source 'https://rubygems.org' -gemspec \ No newline at end of file +gem 'opal', '0.8.0.rc1' +gem 'opal-rspec', github: 'opal/opal-rspec' +gemspec diff --git a/Gemfile.lock b/Gemfile.lock index c072fa6..d1b82e6 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,3 +1,10 @@ +GIT + remote: git://github.com/opal/opal-rspec.git + revision: e2290d56ff5147e5a2b96258ef5bdc2b54a9e6af + specs: + opal-rspec (0.4.2) + opal (>= 0.7.0, < 0.9) + PATH remote: . specs: @@ -16,17 +23,15 @@ GEM hike (1.2.3) json (1.8.3) libv8 (3.16.14.7) - opal (0.7.1) + opal (0.8.0.rc1) hike (~> 1.2) sourcemap (~> 0.1.0) - sprockets (>= 2.2.3, < 4.0.0) - tilt (~> 1.4) + sprockets (~> 3.1) + tilt (>= 1.4) opal-activesupport (0.1.0) opal (>= 0.5.0, < 1.0.0) opal-jquery (0.2.0) opal (>= 0.5.0, < 1.0.0) - opal-rspec (0.4.2) - opal (~> 0.7.0) rack (1.6.1) rack-protection (1.5.3) rack @@ -47,14 +52,15 @@ GEM therubyracer (0.12.2) libv8 (~> 3.16.14.0) ref - tilt (1.4.1) + tilt (2.0.1) PLATFORMS ruby DEPENDENCIES + opal (= 0.8.0.rc1) opal-jquery (~> 0) - opal-rspec (~> 0.4.2) + opal-rspec! rake (~> 10) react.rb! sinatra (~> 1) diff --git a/react.rb.gemspec b/react.rb.gemspec index 326549f..1f36ace 100644 --- a/react.rb.gemspec +++ b/react.rb.gemspec @@ -23,7 +23,6 @@ Gem::Specification.new do |s| s.add_runtime_dependency 'sprockets', '~> 3.1' s.add_runtime_dependency 'react-source', '~> 0.13' - s.add_development_dependency 'opal-rspec', '~> 0.4.2' s.add_development_dependency 'sinatra', '~> 1' s.add_development_dependency 'opal-jquery', '~> 0' s.add_development_dependency 'rake', '~> 10' From 759b76aa2e24f18e4be0ecf5e1067825498e3dcc Mon Sep 17 00:00:00 2001 From: David Chang Date: Sun, 11 Oct 2015 11:44:19 +0800 Subject: [PATCH 45/45] [WIP] Try to fix the "on" method --- examples/todos/app/application.rb | 3 +- examples/todos/app/components/app.react.rb | 1 + opal/react/component.rb | 21 ++++++++-- opal/react/element.rb | 48 ++++++++++++---------- opal/react/element_children_handle.rb | 17 ++++++++ spec/component_spec.rb | 31 ++++++++++++++ spec/element_spec.rb | 1 + 7 files changed, 96 insertions(+), 26 deletions(-) create mode 100644 opal/react/element_children_handle.rb diff --git a/examples/todos/app/application.rb b/examples/todos/app/application.rb index 6985307..296a165 100755 --- a/examples/todos/app/application.rb +++ b/examples/todos/app/application.rb @@ -14,7 +14,8 @@ Vienna::Router.new.tap do |router| router.route('/:filter') do |params| - component.set_props(filter: params[:filter].empty? ? "all" : params[:filter]) + element = React.create_element(TodoAppView, filter: params[:filter].empty? ? "all" : params[:filter]) + component = React.render(element, `document.getElementById('todoapp')`) end end.update diff --git a/examples/todos/app/components/app.react.rb b/examples/todos/app/components/app.react.rb index 09fd045..baa8766 100644 --- a/examples/todos/app/components/app.react.rb +++ b/examples/todos/app/components/app.react.rb @@ -17,6 +17,7 @@ class TodoAppView Todo.on(:create) { Todo.adapter.sync_models(Todo); reload_current_filter } Todo.on(:update) { Todo.adapter.sync_models(Todo); reload_current_filter } Todo.on(:destroy) { Todo.adapter.sync_models(Todo); reload_current_filter } + reload_current_filter end before_receive_props do |next_props| diff --git a/opal/react/component.rb b/opal/react/component.rb index 4f76d4c..31b692e 100644 --- a/opal/react/component.rb +++ b/opal/react/component.rb @@ -3,6 +3,7 @@ require 'react/callbacks' require "react/ext/hash" require "react/component/api" +require "react/element_children_handle" module React module Component @@ -80,20 +81,34 @@ def method_missing(name, *args, &block) if name == "_p_tag" name = "p" end - - @buffer = [] unless @buffer + + unless @buffer + puts "first render for #{name}" + @buffer = [] + @root_element = true + else + @root_element = false + end + if block current = @buffer + is_root = @root_element @buffer = [] result = block.call element = React.create_element(name, *args) { @buffer.count == 0 ? result : @buffer } @buffer = current + @root_element = is_root else element = React.create_element(name, *args) end @buffer << element - element + + if @root_element + element + else + React::ElementChildrenHandle.new(@buffer, @buffer.length - 1) + end end def to_n diff --git a/opal/react/element.rb b/opal/react/element.rb index ebf717b..bd167c2 100644 --- a/opal/react/element.rb +++ b/opal/react/element.rb @@ -2,27 +2,7 @@ module React class Element < `(function(){var r = React;var f = function(){};var c = r.createClass({render:function(){return null;}});f.prototype = Object.getPrototypeOf(r.createElement(c));return f;})()` - def self.new - raise "use React.create_element instead" - end - - def element_type - `self.type` - end - - def key - Native(`self.key`) - end - - def props - Hash.new(`self.props`) - end - - def ref - Native(`self.ref`) - end - - def on(event_name) + def self.attach_event_callback(element, event_name) name = event_name.to_s.event_camelize prop_key = "on#{name}" @@ -44,10 +24,34 @@ def on(event_name) new_prop = `{}` `new_prop[prop_key] = callback` - new_element = `React.cloneElement(#{self}, #{new_prop})` + new_element = `React.cloneElement(#{element}, #{new_prop})` return new_element end + + def self.new + raise "use React.create_element instead" + end + + def element_type + `self.type` + end + + def key + Native(`self.key`) + end + + def props + Hash.new(`self.props`) + end + + def ref + Native(`self.ref`) + end + + def on(event_name, &block) + self.class.attach_event_callback(self, event_name, &block) + end def children nodes = `self.props.children` diff --git a/opal/react/element_children_handle.rb b/opal/react/element_children_handle.rb new file mode 100644 index 0000000..bcdcc78 --- /dev/null +++ b/opal/react/element_children_handle.rb @@ -0,0 +1,17 @@ +require "react/element" + +module React + class ElementChildrenHandle + def initialize(children, index) + @children = children + @index = index + end + + def on(event_name, &block) + old_element = @children[@index] + new_element = React::Element.attach_event_callback(old_element, event_name, &block) + @children[@index] = new_element + self + end + end +end diff --git a/spec/component_spec.rb b/spec/component_spec.rb index 2dd3b94..3dcec79 100644 --- a/spec/component_spec.rb +++ b/spec/component_spec.rb @@ -535,5 +535,36 @@ def render expect(Kernel).to receive(:p).with("second") render_to_document(React.create_element(Foo)) end + + it "should return React::Element for root element" do + stub_const 'Foo', Class.new + Foo.class_eval do + include React::Component + + def render + div + end + end + + expect(Foo.new.render).to be_a(React::Element) + end + + it "should return React::ElementChildrenHandle for inner children" do + 'var inner;' + + stub_const 'Foo', Class.new + Foo.class_eval do + include React::Component + + def render + div do + `inner = #{div {'lorem'}}` + end + end + end + + expect(Foo.new.render).to be_a(React::Element) + expect(`inner`).to be_a(React::ElementChildrenHandle) + end end end diff --git a/spec/element_spec.rb b/spec/element_spec.rb index f57cd1d..9a7ce91 100644 --- a/spec/element_spec.rb +++ b/spec/element_spec.rb @@ -4,6 +4,7 @@ it "should be toll-free bridged to React.Element" do element = React.create_element('div') expect(`React.isValidElement(#{element})`).to eq(true) + expect(element.kind_of?(React::Element)).to eq(true) end describe "#new" do