From 58ea2d1520e6480d0a7d0fb3dbf204c7ce2711aa Mon Sep 17 00:00:00 2001 From: Mitch VanDuyn Date: Mon, 8 Jun 2015 17:32:46 -0400 Subject: [PATCH 1/5] added observables --- Gemfile.lock | 3 +++ opal/react.rb | 1 + opal/react/observable.rb | 35 +++++++++++++++++++++++++ spec/observable_spec.rb | 55 ++++++++++++++++++++++++++++++++++++++++ upgradetodos.txt | 42 ++++++++++++++++++++++++++++++ 5 files changed, 136 insertions(+) create mode 100644 opal/react/observable.rb create mode 100644 spec/observable_spec.rb create mode 100644 upgradetodos.txt diff --git a/Gemfile.lock b/Gemfile.lock index de4cc74..281ef9e 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -61,3 +61,6 @@ DEPENDENCIES rake (~> 10) react.rb! sinatra (~> 1) + +BUNDLED WITH + 1.10.2 diff --git a/opal/react.rb b/opal/react.rb index 2f30dbb..879748f 100644 --- a/opal/react.rb +++ b/opal/react.rb @@ -4,3 +4,4 @@ require "react/event" require "react/component_factory" require "react/validator" +require "react/observable" diff --git a/opal/react/observable.rb b/opal/react/observable.rb new file mode 100644 index 0000000..d8dcbbf --- /dev/null +++ b/opal/react/observable.rb @@ -0,0 +1,35 @@ +module React + + class Observable + + # This is an internal class that is used to add other higher level features to state, and params + + def initialize(value, on_change = nil, &block) + @value = value + @on_change = on_change || block + end + + def method_missing(method_sym, *args, &block) + @value.send(method_sym, *args, &block).tap { |result| @on_change.call result } + end + + def respond_to?(method, *args) + if [:call, :to_proc].include? method + true + else + @value.respond_to? method, *args + end + end + + def call(new_value) + @on_change.call new_value + @value = new_value + end + + def to_proc + lambda { |arg = @value| @on_change.call arg } + end + + end + +end \ No newline at end of file diff --git a/spec/observable_spec.rb b/spec/observable_spec.rb new file mode 100644 index 0000000..d69f543 --- /dev/null +++ b/spec/observable_spec.rb @@ -0,0 +1,55 @@ +require "spec_helper" + +describe 'React::Observable' do + + it "can be given a block which will be called to notify of a change" do + tested = false + observer = React::Observable.new(nil) { |new_value| expect(new_value).to eq(:testing_a_change); tested = true } + observer.call(:testing_a_change) + expect(tested).to be_truthy + end + + it "will return the new_value as the value of the call" do + observer = React::Observable.new(nil) { } + expect(observer.call(:testing_a_change)).to eq(:testing_a_change) + end + + it "can respond to to_proc by providing a lambda wrapping the provided block" do + tested = false + observer = React::Observable.new(nil) { |new_value| expect(new_value).to eq(:testing_a_change); tested = true} + observer.to_proc.call :testing_a_change + expect(tested).to be_truthy + end + + it "will return the value of its block when used as a proc call" do + observer = React::Observable.new(nil) { :some_other_value } + expect(observer.to_proc.call).to eq(:some_other_value) + end + + it "will provide the current value to the block if the proc is called without any parameters" do + observer = React::Observable.new(:current_value) { |new_value| new_value} + expect(observer.to_proc.call).to eq(:current_value) + end + + it "will update the current value if directly called" do + observer = React::Observable.new(:current_value) { |new_value| new_value} + observer.call(:new_value) + expect(observer.to_proc.call).to eq(:new_value) + end + + it "will not update the current value if called via a proc" do + observer = React::Observable.new(:current_value) { |new_value| new_value} + observer.to_proc.call(:new_value) + expect(observer.to_proc.call).to eq(:current_value) + end + + it "will forward any other messages to its current value, and then call the block with the result of the message" do + tested = false + observer = React::Observable.new([]) { |new_value| expect(new_value).to eq([:added_a_value]); tested = true} + observer << :added_a_value + expect(tested).to be_truthy + end + +end + + diff --git a/upgradetodos.txt b/upgradetodos.txt new file mode 100644 index 0000000..dbaa480 --- /dev/null +++ b/upgradetodos.txt @@ -0,0 +1,42 @@ + +observable_spec.rb + +native_library_spec.rb <- no big interaction, just hooks into @@component_classes + importing native react components (like bootstrap) + + +export_component_spec.rb <- completely stand alone + export_component + + +rendering_context_spec.rb + NOTE: rendering context primarily exists so that we can say Foo::Bar in the dsl, because we have no way of knowing the caller, so we have to track it via RenderingContext + NOTE: put the component lookup method in here I think + components can be directly named in the dsl (i.e. if class Foo, then you can just say Foo) + _as_node will return the native node without pushing into the buffer + if element returned from the child block is a string it is added to the buffer + test to make sure subclassing components works + +exception_handling_spec.rb + you don't have to have render method, if you just want to export state <- not sure what is going on here, but there is a render method now in the component, that raises an error unless its subclassed + catch and print rendering method if rendering fails. + I'm figuring that there will more exception cases (like misnamed components) that can go in this file. + +state_spec.rb + states all get methods - states define methods foo, foo= and foo! + export / import states + states can be defined from within components instances (i.e. before_mount) + define_state extended syntax: strings/symbols are initialized to the default initializer (nil or provided by block), the hash gives name => value pairs + if default initializer excepts params then it is considered a state listener and will receive |state_name, current_value, new_value| on state changes + + +params_spec.rb + NOTE: probably updates validator + can require param on a single line: required_param or optional_param + can require param of type Proc + params get methods + proc params automatically call + ReactObserver params interact with the observer + + +depends on react-source <- does it have to (i.e. perhaps for react-rails to work?, if not then do a separate PR at end) \ No newline at end of file From 235723ff0b79de622e068a260253e5554c691fbe Mon Sep 17 00:00:00 2001 From: Mitch VanDuyn Date: Mon, 8 Jun 2015 20:08:28 -0400 Subject: [PATCH 2/5] base for future PRs --- upgradetodos.txt | 42 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 42 insertions(+) create mode 100644 upgradetodos.txt diff --git a/upgradetodos.txt b/upgradetodos.txt new file mode 100644 index 0000000..dbaa480 --- /dev/null +++ b/upgradetodos.txt @@ -0,0 +1,42 @@ + +observable_spec.rb + +native_library_spec.rb <- no big interaction, just hooks into @@component_classes + importing native react components (like bootstrap) + + +export_component_spec.rb <- completely stand alone + export_component + + +rendering_context_spec.rb + NOTE: rendering context primarily exists so that we can say Foo::Bar in the dsl, because we have no way of knowing the caller, so we have to track it via RenderingContext + NOTE: put the component lookup method in here I think + components can be directly named in the dsl (i.e. if class Foo, then you can just say Foo) + _as_node will return the native node without pushing into the buffer + if element returned from the child block is a string it is added to the buffer + test to make sure subclassing components works + +exception_handling_spec.rb + you don't have to have render method, if you just want to export state <- not sure what is going on here, but there is a render method now in the component, that raises an error unless its subclassed + catch and print rendering method if rendering fails. + I'm figuring that there will more exception cases (like misnamed components) that can go in this file. + +state_spec.rb + states all get methods - states define methods foo, foo= and foo! + export / import states + states can be defined from within components instances (i.e. before_mount) + define_state extended syntax: strings/symbols are initialized to the default initializer (nil or provided by block), the hash gives name => value pairs + if default initializer excepts params then it is considered a state listener and will receive |state_name, current_value, new_value| on state changes + + +params_spec.rb + NOTE: probably updates validator + can require param on a single line: required_param or optional_param + can require param of type Proc + params get methods + proc params automatically call + ReactObserver params interact with the observer + + +depends on react-source <- does it have to (i.e. perhaps for react-rails to work?, if not then do a separate PR at end) \ No newline at end of file From ba1465db4b055985953e4437c6bd5a449a0884e4 Mon Sep 17 00:00:00 2001 From: Mitch VanDuyn Date: Mon, 8 Jun 2015 20:17:36 -0400 Subject: [PATCH 3/5] updated todo notes --- upgradetodos.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/upgradetodos.txt b/upgradetodos.txt index dbaa480..a93c8c0 100644 --- a/upgradetodos.txt +++ b/upgradetodos.txt @@ -1,7 +1,7 @@ observable_spec.rb -native_library_spec.rb <- no big interaction, just hooks into @@component_classes +native_library_spec.rb <- no big interaction, just hooks into @@component_classes - but depends on RenderingContext importing native react components (like bootstrap) From a0c13e8d677fc3c6e5455c7bd93a945329538a7c Mon Sep 17 00:00:00 2001 From: Mitch VanDuyn Date: Thu, 11 Jun 2015 08:13:26 -0400 Subject: [PATCH 4/5] loosened sprockets dependency, made this base for future PRs --- Gemfile.lock | 18 ++++++----- opal/react.rb | 1 - opal/react/observable.rb | 35 ---------------------- react.rb.gemspec | 4 +-- spec/component_factory_spec.rb | 4 ++- spec/observable_spec.rb | 55 ---------------------------------- upgradetodos.txt | 8 ++++- 7 files changed, 23 insertions(+), 102 deletions(-) delete mode 100644 opal/react/observable.rb delete mode 100644 spec/observable_spec.rb diff --git a/Gemfile.lock b/Gemfile.lock index 3c6a925..878a075 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -6,7 +6,7 @@ PATH opal-activesupport (~> 0) react-jsx (~> 0.8.0) react-source (~> 0.13) - sprockets (~> 3.1) + sprockets (>= 2.2.3, < 3.1) therubyracer (~> 0) GEM @@ -16,15 +16,16 @@ GEM hike (1.2.3) json (1.8.3) libv8 (3.16.14.7) - opal (0.7.1) + multi_json (1.11.1) + opal (0.7.2) hike (~> 1.2) sourcemap (~> 0.1.0) - sprockets (>= 2.2.3, < 4.0.0) + sprockets (>= 2.2.3, < 3.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-jquery (0.3.0) + opal (~> 0.7.0) opal-rspec (0.4.2) opal (~> 0.7.0) rack (1.6.1) @@ -42,8 +43,11 @@ GEM rack-protection (~> 1.4) tilt (>= 1.3, < 3) sourcemap (0.1.1) - sprockets (3.2.0) + sprockets (2.12.3) + hike (~> 1.2) + multi_json (~> 1.0) rack (~> 1.0) + tilt (~> 1.1, != 1.3.0) therubyracer (0.12.2) libv8 (~> 3.16.14.0) ref @@ -54,7 +58,7 @@ PLATFORMS DEPENDENCIES opal-jquery (~> 0) - opal-rspec (~> 0.4.2) + opal-rspec rake (~> 10) react.rb! sinatra (~> 1) diff --git a/opal/react.rb b/opal/react.rb index 879748f..2f30dbb 100644 --- a/opal/react.rb +++ b/opal/react.rb @@ -4,4 +4,3 @@ require "react/event" require "react/component_factory" require "react/validator" -require "react/observable" diff --git a/opal/react/observable.rb b/opal/react/observable.rb deleted file mode 100644 index d8dcbbf..0000000 --- a/opal/react/observable.rb +++ /dev/null @@ -1,35 +0,0 @@ -module React - - class Observable - - # This is an internal class that is used to add other higher level features to state, and params - - def initialize(value, on_change = nil, &block) - @value = value - @on_change = on_change || block - end - - def method_missing(method_sym, *args, &block) - @value.send(method_sym, *args, &block).tap { |result| @on_change.call result } - end - - def respond_to?(method, *args) - if [:call, :to_proc].include? method - true - else - @value.respond_to? method, *args - end - end - - def call(new_value) - @on_change.call new_value - @value = new_value - end - - def to_proc - lambda { |arg = @value| @on_change.call arg } - end - - end - -end \ No newline at end of file diff --git a/react.rb.gemspec b/react.rb.gemspec index 326549f..e734e45 100644 --- a/react.rb.gemspec +++ b/react.rb.gemspec @@ -20,10 +20,10 @@ Gem::Specification.new do |s| 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', '~> 3.1' + s.add_runtime_dependency 'sprockets', '>= 2.2.3', '< 3.1' s.add_runtime_dependency 'react-source', '~> 0.13' - s.add_development_dependency 'opal-rspec', '~> 0.4.2' + s.add_development_dependency 'opal-rspec' s.add_development_dependency 'sinatra', '~> 1' s.add_development_dependency 'opal-jquery', '~> 0' s.add_development_dependency 'rake', '~> 10' diff --git a/spec/component_factory_spec.rb b/spec/component_factory_spec.rb index 71b1085..1adb458 100644 --- a/spec/component_factory_spec.rb +++ b/spec/component_factory_spec.rb @@ -2,7 +2,7 @@ describe React::ComponentFactory do describe "native_component_class" do - it "should bridge the defined life cycle methods" do + it "should bridge the defined life cycle methods and the render method" do stub_const 'Foo', Class.new Foo.class_eval do def component_will_mount; end @@ -12,6 +12,7 @@ def should_component_update?; end def component_will_update; end def component_did_update; end def component_will_unmount; end + def render; end end ctor = React::ComponentFactory.native_component_class(Foo) @@ -23,6 +24,7 @@ def component_will_unmount; end 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`) + expect(`instance.$render`).to be(`instance.render`) end end end diff --git a/spec/observable_spec.rb b/spec/observable_spec.rb deleted file mode 100644 index d69f543..0000000 --- a/spec/observable_spec.rb +++ /dev/null @@ -1,55 +0,0 @@ -require "spec_helper" - -describe 'React::Observable' do - - it "can be given a block which will be called to notify of a change" do - tested = false - observer = React::Observable.new(nil) { |new_value| expect(new_value).to eq(:testing_a_change); tested = true } - observer.call(:testing_a_change) - expect(tested).to be_truthy - end - - it "will return the new_value as the value of the call" do - observer = React::Observable.new(nil) { } - expect(observer.call(:testing_a_change)).to eq(:testing_a_change) - end - - it "can respond to to_proc by providing a lambda wrapping the provided block" do - tested = false - observer = React::Observable.new(nil) { |new_value| expect(new_value).to eq(:testing_a_change); tested = true} - observer.to_proc.call :testing_a_change - expect(tested).to be_truthy - end - - it "will return the value of its block when used as a proc call" do - observer = React::Observable.new(nil) { :some_other_value } - expect(observer.to_proc.call).to eq(:some_other_value) - end - - it "will provide the current value to the block if the proc is called without any parameters" do - observer = React::Observable.new(:current_value) { |new_value| new_value} - expect(observer.to_proc.call).to eq(:current_value) - end - - it "will update the current value if directly called" do - observer = React::Observable.new(:current_value) { |new_value| new_value} - observer.call(:new_value) - expect(observer.to_proc.call).to eq(:new_value) - end - - it "will not update the current value if called via a proc" do - observer = React::Observable.new(:current_value) { |new_value| new_value} - observer.to_proc.call(:new_value) - expect(observer.to_proc.call).to eq(:current_value) - end - - it "will forward any other messages to its current value, and then call the block with the result of the message" do - tested = false - observer = React::Observable.new([]) { |new_value| expect(new_value).to eq([:added_a_value]); tested = true} - observer << :added_a_value - expect(tested).to be_truthy - end - -end - - diff --git a/upgradetodos.txt b/upgradetodos.txt index a724897..9a39501 100644 --- a/upgradetodos.txt +++ b/upgradetodos.txt @@ -40,4 +40,10 @@ params_spec.rb ReactObserver params interact with the observer -depends on react-source <- does it have to (i.e. perhaps for react-rails to work?, if not then do a separate PR at end) \ No newline at end of file +depends on react-source <- does it have to (i.e. perhaps for react-rails to work?, if not then do a separate PR at end) + +Branch map... + +v13_base is synced with v13 BUT already has observable.rb added. (todo if possible, shuffle this around) + <- exception_handling branched fro v13_base + <- export_component branched from v13_base \ No newline at end of file From 4f56dc795b5e07a876002d94c0b8103881ce6dae Mon Sep 17 00:00:00 2001 From: Mitch VanDuyn Date: Thu, 11 Jun 2015 10:24:53 -0400 Subject: [PATCH 5/5] added exception handling for exceptions during render --- opal/react/component.rb | 8 ++++++++ opal/react/component_factory.rb | 1 + spec/exception_handling_spec.rb | 28 ++++++++++++++++++++++++++++ 3 files changed, 37 insertions(+) create mode 100644 spec/exception_handling_spec.rb diff --git a/opal/react/component.rb b/opal/react/component.rb index c18d6e7..aa7760d 100644 --- a/opal/react/component.rb +++ b/opal/react/component.rb @@ -20,6 +20,14 @@ def self.included(base) end base.extend(ClassMethods) end + + def _render_wrapper + render + rescue Exception => e + message = "Exception raised while rendering #{self.class.name}: #{e}" + `console.error(#{message})` rescue nil + React.create_element("div") # return a valid element so error does not propogate, and we can keep rendering + end def params Hash.new(`#{self}.props`).inject({}) do |memo, (k,v)| diff --git a/opal/react/component_factory.rb b/opal/react/component_factory.rb index f0c5795..6216592 100644 --- a/opal/react/component_factory.rb +++ b/opal/react/component_factory.rb @@ -22,6 +22,7 @@ def self.native_component_class(klass) optional_native_alias[:componentDidUpdate, :component_did_update] optional_native_alias[:componentWillUnmount, :component_will_unmount] native_alias :render, :render + optional_native_alias[:render, :_render_wrapper] end %x{ if (!Object.assign) { diff --git a/spec/exception_handling_spec.rb b/spec/exception_handling_spec.rb new file mode 100644 index 0000000..aa84259 --- /dev/null +++ b/spec/exception_handling_spec.rb @@ -0,0 +1,28 @@ +describe "displaying helpful warning and error messages" do + + before(:all) do + %x{ + window.old_error_console = console.error + console.error = function(m) { window.last_error_message = m; } + } + end + + after(:all) do + %x{ + console.error = window.old_error_console + } + end + + it "should print and exception message" do + stub_const 'Foo', Class.new + Foo.class_eval do + include React::Component + def render + raise "error happened" + end + end + React.render_to_static_markup(React.create_element(Foo)) + expect(`window.last_error_message`).to include("error happened") + end + +end \ No newline at end of file