diff --git a/.gitignore b/.gitignore index f51d0b1..0c72301 100644 --- a/.gitignore +++ b/.gitignore @@ -29,8 +29,15 @@ node_modules # Ignore bundler config. .bundle -spec/test_app +spec/test_app/tmp Gemfile.lock /gemfiles/*.lock /tmp + +# ignore gem +*.gem + +# ignore IDE files +.idea +.vscode diff --git a/.travis.yml b/.travis.yml index 9434934..70f23af 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,62 +1,32 @@ +dist: trusty language: ruby rvm: - - 1.9.3 - - 2.0.0 - - 2.1 - - jruby-19mode -before_script: - - phantomjs --version + - ruby env: - - PHANTOMJS_VERSION=1.9.8 -gemfile: - - gemfiles/opal_0.8_react_13.gemfile - - gemfiles/opal_0.8_react_14.gemfile - - gemfiles/opal_0.8_react_15.gemfile - - gemfiles/opal_0.9_react_13.gemfile - - gemfiles/opal_0.9_react_14.gemfile - - gemfiles/opal_0.9_react_15.gemfile -cache: - directories: - - "travis_phantomjs" -branches: - only: - - master + - DRIVER=travis HYPER_DEV_GEM_SOURCE="https://gems.ruby-hyperloop.org" TZ=Europe/Berlin + - DRIVER=beheaded HYPER_DEV_GEM_SOURCE="https://gems.ruby-hyperloop.org" TZ=Europe/Berlin before_install: - - "phantomjs --version" - - "export PATH=$PWD/travis_phantomjs/phantomjs-$PHANTOMJS_VERSION-linux-x86_64/bin:$PATH" - - "phantomjs --version" - - "if [ $(phantomjs --version) != $PHANTOMJS_VERSION ]; then rm -rf $PWD/travis_phantomjs; mkdir -p $PWD/travis_phantomjs; fi" - - "if [ $(phantomjs --version) != $PHANTOMJS_VERSION ]; then wget https://github.com/Medium/phantomjs/releases/download/v$PHANTOMJS_VERSION/phantomjs-$PHANTOMJS_VERSION-linux-x86_64.tar.bz2 -O $PWD/travis_phantomjs/phantomjs-$PHANTOMJS_VERSION-linux-x86_64.tar.bz2; fi" - - "if [ $(phantomjs --version) != $PHANTOMJS_VERSION ]; then tar -xvf $PWD/travis_phantomjs/phantomjs-$PHANTOMJS_VERSION-linux-x86_64.tar.bz2 -C $PWD/travis_phantomjs; fi" - - "phantomjs --version" - - gem install bundler -v 1.13.7 -script: - - bundle exec rake test_app - - bundle exec rake -# These two setup seems to run indefinitely long -# further investigation required. -matrix: - exclude: - - rvm: jruby-19mode - gemfile: gemfiles/opal_0.9_react_13.gemfile - - rvm: jruby-19mode - gemfile: gemfiles/opal_0.9_react_14.gemfile - - rvm: jruby-19mode - gemfile: gemfiles/opal_0.9_react_15.gemfile - include: - - rvm: 2.1 - env: PHANTOMJS_VERSION=2.1.1 - gemfile: gemfiles/opal_0.10_react_13.gemfile - - rvm: 2.1 - env: PHANTOMJS_VERSION=2.1.1 - gemfile: gemfiles/opal_0.10_react_14.gemfile - - rvm: 2.1 - env: PHANTOMJS_VERSION=2.1.1 - gemfile: gemfiles/opal_0.10_react_15.gemfile - - rvm: 2.1 - env: PHANTOMJS_VERSION=2.1.1 - gemfile: gemfiles/opal_master_react_15.gemfile - allow_failures: - - rvm: 2.1 - env: PHANTOMJS_VERSION=2.1.1 - gemfile: gemfiles/opal_master_react_15.gemfile + - if [[ "$DRIVER" == "travis" ]]; then wget https://dl.google.com/linux/direct/google-chrome-stable_current_amd64.deb; fi + - if [[ "$DRIVER" == "travis" ]]; then sudo dpkg -i google-chrome*.deb; fi + - if [[ "$DRIVER" == "beheaded" ]]; then wget https://github.com/mozilla/geckodriver/releases/download/v0.19.1/geckodriver-v0.19.1-linux64.tar.gz; fi + - if [[ "$DRIVER" == "beheaded" ]]; then tar zxf geckodriver-v0.19.1-linux64.tar.gz; fi + - if [[ "$DRIVER" == "beheaded" ]]; then sudo mv geckodriver /usr/local/bin/; fi + - gem install bundler +before_script: + - cd spec/test_app + - bundle update + - bundle exec rails db:setup + - cd ../../ + - if [[ "$DRIVER" == "travis" ]]; then chromedriver-update; fi + - if [[ "$DRIVER" == "travis" ]]; then ls -lR ~/.chromedriver-helper/; fi + - if [[ "$DRIVER" == "travis" ]]; then chromedriver --version; fi + - if [[ "$DRIVER" == "travis" ]]; then google-chrome --version; fi + - if [[ "$DRIVER" == "travis" ]]; then which chromedriver; fi + - if [[ "$DRIVER" == "travis" ]]; then which google-chrome; fi + - if [[ "$DRIVER" == "beheaded" ]]; then firefox --version; fi + - if [[ "$DRIVER" == "beheaded" ]]; then geckodriver --version; fi + - if [[ "$DRIVER" == "beheaded" ]]; then which firefox; fi + - if [[ "$DRIVER" == "beheaded" ]]; then which geckodriver; fi +script: bundle exec rspec +gemfile: + - gemfiles/opal_0_11_react-rails_2_4.gemfile diff --git a/Appraisals b/Appraisals index 2004041..4e676b2 100644 --- a/Appraisals +++ b/Appraisals @@ -27,5 +27,5 @@ appraise "opal-master-react-15" do gem 'opal', git: 'https://github.com/opal/opal.git' gem "opal-sprockets", git: 'https://github.com/opal/opal-sprockets.git' gem 'opal-rails', '~> 0.9.0' - gem 'react-rails', '~> 1.10.0', require: false + gem 'react-rails', '~> 2.4.0', require: false end diff --git a/DOCS.md b/DOCS.md index 4fbc3c2..a55dce7 100644 --- a/DOCS.md +++ b/DOCS.md @@ -1468,7 +1468,7 @@ There are also good tutorials on integrating Webpack with existing rails apps a ```ruby Hyperloop.configuration do |config| - config.prerendering = :off # :on by default + config.prerendering = :on # :off by default end ``` diff --git a/Gemfile b/Gemfile index ec6fa61..33ce79f 100644 --- a/Gemfile +++ b/Gemfile @@ -1,8 +1,4 @@ source 'https://rubygems.org' +gem "opal-jquery", git: "https://github.com/opal/opal-jquery.git", branch: "master" +gem "opal-rails", git: "https://github.com/opal/opal-rails.git", branch: "master" gemspec - -ruby ">= 1.9.3" - -group :development do - gem "appraisal" -end diff --git a/README.md b/README.md index 62fa1a5..6abf725 100644 --- a/README.md +++ b/README.md @@ -20,6 +20,7 @@ +[![Build Status](https://travis-ci.org/ruby-hyperloop/hyper-react.svg?branch=master)](https://travis-ci.org/ruby-hyperloop/hyper-react) [![Gem Version](https://badge.fury.io/rb/hyper-react.svg)](https://badge.fury.io/rb/hyper-react)

diff --git a/Rakefile b/Rakefile index 1e2c349..b2afa26 100644 --- a/Rakefile +++ b/Rakefile @@ -1,33 +1,44 @@ -require 'bundler' -Bundler.require -Bundler::GemHelper.install_tasks +# require 'bundler' +# Bundler.require +# Bundler::GemHelper.install_tasks +# +# # Store the BUNDLE_GEMFILE env, since rake or rspec seems to clean it +# # while invoking task. +# ENV['REAL_BUNDLE_GEMFILE'] = ENV['BUNDLE_GEMFILE'] +# +# require 'rspec/core/rake_task' +# require 'opal/rspec/rake_task' +# +# RSpec::Core::RakeTask.new('ruby:rspec') +# +# task :test do +# Rake::Task['ruby:rspec'].invoke +# end +# +# require 'generators/reactive_ruby/test_app/test_app_generator' +# desc "Generates a dummy app for testing" +# task :test_app do +# ReactiveRuby::TestAppGenerator.start +# puts "Setting up test app database..." +# system("bundle exec rake db:drop db:create db:migrate > #{File::NULL}") +# end +# +# task :test_prepare do +# system("./dciy_prepare.sh") +# end +# +# task default: [ :test ] -# Store the BUNDLE_GEMFILE env, since rake or rspec seems to clean it -# while invoking task. -ENV['REAL_BUNDLE_GEMFILE'] = ENV['BUNDLE_GEMFILE'] +require "bundler/gem_tasks" +require "rspec/core/rake_task" -require 'rspec/core/rake_task' -require 'opal/rspec/rake_task' +RSpec::Core::RakeTask.new(:spec) -RSpec::Core::RakeTask.new('ruby:rspec') - -Opal::RSpec::RakeTask.new('opal:rspec') do |s, task| - s.append_path 'spec/vendor' - s.index_path = 'spec/index.html.erb' - task.timeout = 80000 if task -end - -task :test do - Rake::Task['ruby:rspec'].invoke - Rake::Task['opal:rspec'].invoke -end - -require 'generators/reactive_ruby/test_app/test_app_generator' -desc "Generates a dummy app for testing" -task :test_app do - ReactiveRuby::TestAppGenerator.start - puts "Setting up test app database..." - system("bundle exec rake db:drop db:create db:migrate > #{File::NULL}") +namespace :spec do + task :prepare do + sh %{bundle update} + sh %{cd spec/test_app; bundle update} + end end -task default: [ :test ] +task :default => :spec diff --git a/config.ru b/config.ru deleted file mode 100644 index 889f95d..0000000 --- a/config.ru +++ /dev/null @@ -1,26 +0,0 @@ -require 'bundler' -Bundler.require - -require "opal/rspec" - -Opal::Config.arity_check_enabled = true - -if Opal::RSpec.const_defined?("SprocketsEnvironment") - sprockets_env = Opal::RSpec::SprocketsEnvironment.new - sprockets_env.cache = Sprockets::Cache::FileStore.new("tmp") - sprockets_env.add_spec_paths_to_sprockets - run Opal::Server.new(sprockets: sprockets_env) { |s| - s.main = 'opal/rspec/sprockets_runner' - s.debug = false - s.append_path 'spec/vendor' - s.index_path = 'spec/index.html.erb' - } -else - run Opal::Server.new { |s| - s.main = 'opal/rspec/sprockets_runner' - s.append_path 'spec' - s.append_path 'spec/vendor' - s.debug = false - s.index_path = 'spec/index.html.erb' - } -end diff --git a/dciy.toml b/dciy.toml new file mode 100644 index 0000000..d5bb652 --- /dev/null +++ b/dciy.toml @@ -0,0 +1,3 @@ +[dciy.commands] +prepare = ["bundle exec rake spec:prepare"] +cibuild = ["bundle exec rake"] diff --git a/gemfiles/opal_0.10_react_13.gemfile b/gemfiles/opal_0.10_react_13.gemfile deleted file mode 100644 index 7a8c894..0000000 --- a/gemfiles/opal_0.10_react_13.gemfile +++ /dev/null @@ -1,15 +0,0 @@ -# This file was generated by Appraisal - -source "https://rubygems.org" - -ruby ">= 1.9.3" - -gem "opal", "~> 0.10.0" -gem "opal-rails", "~> 0.9.0" -gem "react-rails", "~> 1.3.3", :require => false - -group :development do - gem "appraisal" -end - -gemspec :path => "../" diff --git a/gemfiles/opal_0.10_react_14.gemfile b/gemfiles/opal_0.10_react_14.gemfile deleted file mode 100644 index 275bf73..0000000 --- a/gemfiles/opal_0.10_react_14.gemfile +++ /dev/null @@ -1,15 +0,0 @@ -# This file was generated by Appraisal - -source "https://rubygems.org" - -ruby ">= 1.9.3" - -gem "opal", "~> 0.10.0" -gem "opal-rails", "~> 0.9.0" -gem "react-rails", "~> 1.6.2", :require => false - -group :development do - gem "appraisal" -end - -gemspec :path => "../" diff --git a/gemfiles/opal_0.10_react_15.gemfile b/gemfiles/opal_0.10_react_15.gemfile deleted file mode 100644 index 5e2fff2..0000000 --- a/gemfiles/opal_0.10_react_15.gemfile +++ /dev/null @@ -1,15 +0,0 @@ -# This file was generated by Appraisal - -source "https://rubygems.org" - -ruby ">= 1.9.3" - -gem "opal", "~> 0.10.0" -gem "opal-rails", "~> 0.9.0" -gem "react-rails", "~> 1.10.0", :require => false - -group :development do - gem "appraisal" -end - -gemspec :path => "../" diff --git a/gemfiles/opal_0.8_react_13.gemfile b/gemfiles/opal_0.8_react_13.gemfile deleted file mode 100644 index a36890c..0000000 --- a/gemfiles/opal_0.8_react_13.gemfile +++ /dev/null @@ -1,15 +0,0 @@ -# This file was generated by Appraisal - -source "https://rubygems.org" - -ruby ">= 1.9.3" - -gem "opal", "~> 0.8.0" -gem "opal-rails", "~> 0.8.1" -gem "react-rails", "~> 1.3.3", :require => false - -group :development do - gem "appraisal" -end - -gemspec :path => "../" diff --git a/gemfiles/opal_0.8_react_14.gemfile b/gemfiles/opal_0.8_react_14.gemfile deleted file mode 100644 index 5529f7d..0000000 --- a/gemfiles/opal_0.8_react_14.gemfile +++ /dev/null @@ -1,15 +0,0 @@ -# This file was generated by Appraisal - -source "https://rubygems.org" - -ruby ">= 1.9.3" - -gem "opal", "~> 0.8.0" -gem "opal-rails", "~> 0.8.1" -gem "react-rails", "~> 1.6.2", :require => false - -group :development do - gem "appraisal" -end - -gemspec :path => "../" diff --git a/gemfiles/opal_0.8_react_15.gemfile b/gemfiles/opal_0.8_react_15.gemfile deleted file mode 100644 index ded477a..0000000 --- a/gemfiles/opal_0.8_react_15.gemfile +++ /dev/null @@ -1,15 +0,0 @@ -# This file was generated by Appraisal - -source "https://rubygems.org" - -ruby ">= 1.9.3" - -gem "opal", "~> 0.8.0" -gem "opal-rails", "~> 0.8.1" -gem "react-rails", "~> 1.10.0", :require => false - -group :development do - gem "appraisal" -end - -gemspec :path => "../" diff --git a/gemfiles/opal_0.9_react_13.gemfile b/gemfiles/opal_0.9_react_13.gemfile deleted file mode 100644 index ea11d6b..0000000 --- a/gemfiles/opal_0.9_react_13.gemfile +++ /dev/null @@ -1,15 +0,0 @@ -# This file was generated by Appraisal - -source "https://rubygems.org" - -ruby ">= 1.9.3" - -gem "opal", "~> 0.9.0" -gem "opal-rails", "~> 0.9.0" -gem "react-rails", "~> 1.3.3", :require => false - -group :development do - gem "appraisal" -end - -gemspec :path => "../" diff --git a/gemfiles/opal_0.9_react_14.gemfile b/gemfiles/opal_0.9_react_14.gemfile deleted file mode 100644 index 61df4bc..0000000 --- a/gemfiles/opal_0.9_react_14.gemfile +++ /dev/null @@ -1,15 +0,0 @@ -# This file was generated by Appraisal - -source "https://rubygems.org" - -ruby ">= 1.9.3" - -gem "opal", "~> 0.9.0" -gem "opal-rails", "~> 0.9.0" -gem "react-rails", "~> 1.6.2", :require => false - -group :development do - gem "appraisal" -end - -gemspec :path => "../" diff --git a/gemfiles/opal_0.9_react_15.gemfile b/gemfiles/opal_0.9_react_15.gemfile deleted file mode 100644 index 602cb8e..0000000 --- a/gemfiles/opal_0.9_react_15.gemfile +++ /dev/null @@ -1,15 +0,0 @@ -# This file was generated by Appraisal - -source "https://rubygems.org" - -ruby ">= 1.9.3" - -gem "opal", "~> 0.9.0" -gem "opal-rails", "~> 0.9.0" -gem "react-rails", "~> 1.10.0", :require => false - -group :development do - gem "appraisal" -end - -gemspec :path => "../" diff --git a/gemfiles/opal_0_11_react-rails_2_4.gemfile b/gemfiles/opal_0_11_react-rails_2_4.gemfile new file mode 100644 index 0000000..f4d9fd8 --- /dev/null +++ b/gemfiles/opal_0_11_react-rails_2_4.gemfile @@ -0,0 +1,7 @@ +source "https://rubygems.org" +source ENV['HYPER_DEV_GEM_SOURCE'] if ENV['HYPER_DEV_GEM_SOURCE'] + +gem "opal-jquery", git: "https://github.com/opal/opal-jquery.git", branch: "master" +gem "opal-rails", git: "https://github.com/opal/opal-rails.git", branch: "master" + +gemspec :path => "../" diff --git a/gemfiles/opal_master_react_15.gemfile b/gemfiles/opal_master_react_15.gemfile deleted file mode 100644 index 2ea22d4..0000000 --- a/gemfiles/opal_master_react_15.gemfile +++ /dev/null @@ -1,16 +0,0 @@ -# This file was generated by Appraisal - -source "https://rubygems.org" - -ruby ">= 2.0.0" - -gem "opal", :git => "https://github.com/opal/opal.git" -gem "opal-sprockets", :git => "https://github.com/opal/opal-sprockets.git" -gem "opal-rails", "~> 0.9.0" -gem "react-rails", "~> 1.10.0", :require => false - -group :development do - gem "appraisal" -end - -gemspec :path => "../" diff --git a/hyper-react.gemspec b/hyper-react.gemspec index bd50967..e91132d 100644 --- a/hyper-react.gemspec +++ b/hyper-react.gemspec @@ -1,48 +1,47 @@ # -*- encoding: utf-8 -*- $:.push File.expand_path('../lib/', __FILE__) - require 'reactive-ruby/version' -Gem::Specification.new do |s| - s.name = 'hyper-react' - s.version = React::VERSION +Gem::Specification.new do |spec| + spec.name = 'hyper-react' + spec.version = React::VERSION - s.authors = ['David Chang', 'Adam Jahn', 'Mitch VanDuyn'] - s.email = 'reactrb@catprint.com' - s.homepage = 'http://ruby-hyperloop.io/gems/reactrb/' - s.summary = 'Opal Ruby wrapper of React.js library.' - s.license = 'MIT' - s.description = "Write React UI components in pure Ruby." - s.files = `git ls-files`.split("\n") - s.executables = `git ls-files -- bin/*`.split("\n").map { |f| File.basename(f) } - s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n") - s.require_paths = ['lib'] + spec.authors = ['David Chang', 'Adam Jahn', 'Mitch VanDuyn', 'Jan Biedermann'] + spec.email = ['mitch@catprint.com', 'jan@kursator.com'] + spec.homepage = 'http://ruby-hyperloop.org' + spec.summary = 'Opal Ruby wrapper of React.js library.' + spec.license = 'MIT' + spec.description = 'Write React UI components in pure Ruby.' + # spec.metadata = { + # "homepage_uri" => 'http://ruby-hyperloop.org', + # "source_code_uri" => 'https://github.com/ruby-hyperloop/hyper-component' + # } - s.add_dependency 'opal', '>= 0.8.0' - s.add_dependency 'opal-activesupport', '>= 0.2.0' - s.add_dependency 'hyper-store', '>= 0.2.1' - s.add_dependency 'hyperloop-config', '>= 0.9.7' - s.add_development_dependency 'rake', '< 11.0' - s.add_development_dependency 'rspec-rails', '3.3.3' - s.add_development_dependency 'timecop' - s.add_development_dependency 'opal-rspec' - s.add_development_dependency 'sinatra' - s.add_development_dependency 'opal-jquery' + spec.files = `git ls-files`.split("\n").reject { |f| f.match(%r{^(gemfiles|spec)/}) } + spec.executables = `git ls-files -- bin/*`.split("\n").map { |f| File.basename(f) } + spec.test_files = `git ls-files -- {test,spec,features}/*`.split("\n") + spec.require_paths = ['lib'] - # For Test Rails App - s.add_development_dependency 'rails', '4.2.4' - s.add_development_dependency 'mime-types', '< 3' - s.add_development_dependency 'opal-rails' - s.add_development_dependency 'react-rails', '<= 1.10.0' + spec.add_dependency 'hyper-store', React::VERSION + spec.add_dependency 'opal', '>= 0.11.0', '< 0.12.0' + spec.add_dependency 'opal-activesupport', '~> 0.3.1' - s.add_development_dependency 'nokogiri', '< 1.7' - s.add_development_dependency 'rubocop' - if RUBY_PLATFORM == 'java' - s.add_development_dependency 'jdbc-sqlite3' - s.add_development_dependency 'activerecord-jdbcsqlite3-adapter' - s.add_development_dependency 'therubyrhino' - else - s.add_development_dependency 'sqlite3', '1.3.10' - s.add_development_dependency 'therubyracer', '0.12.2' - end + spec.add_development_dependency 'chromedriver-helper' + spec.add_development_dependency 'hyper-spec', React::VERSION + spec.add_development_dependency 'jquery-rails' + spec.add_development_dependency 'listen' + spec.add_development_dependency 'mime-types' + spec.add_development_dependency 'nokogiri' + spec.add_development_dependency 'opal-jquery' + spec.add_development_dependency 'opal-rails', '~> 0.9.3' + spec.add_development_dependency 'opal-rspec' + spec.add_development_dependency 'rails', '>= 4.0.0' + spec.add_development_dependency 'rails-controller-testing' + spec.add_development_dependency 'rake' + spec.add_development_dependency 'react-rails', '>= 2.4.0', '< 2.5.0' + spec.add_development_dependency 'rspec-rails' + spec.add_development_dependency 'rubocop', '~> 0.51.0' + spec.add_development_dependency 'sqlite3' + spec.add_development_dependency 'mini_racer', '~> 0.1.15' + spec.add_development_dependency 'timecop', '~> 0.8.1' end diff --git a/lib/generators/reactive_ruby/test_app/templates/assets/javascripts/server_rendering.js b/lib/generators/reactive_ruby/test_app/templates/assets/javascripts/server_rendering.js new file mode 100644 index 0000000..6110b3c --- /dev/null +++ b/lib/generators/reactive_ruby/test_app/templates/assets/javascripts/server_rendering.js @@ -0,0 +1,5 @@ +//= require 'react-server' +//= require 'react_ujs' +//= require 'components' + +Opal.load('components') \ No newline at end of file diff --git a/lib/generators/reactive_ruby/test_app/test_app_generator.rb b/lib/generators/reactive_ruby/test_app/test_app_generator.rb index 41f2dd9..a7c04aa 100644 --- a/lib/generators/reactive_ruby/test_app/test_app_generator.rb +++ b/lib/generators/reactive_ruby/test_app/test_app_generator.rb @@ -28,6 +28,8 @@ def configure_test_app template 'test_application.rb.erb', "#{test_app_path}/config/application.rb", force: true template 'assets/javascripts/test_application.rb', "#{test_app_path}/app/assets/javascripts/application.rb", force: true + template 'assets/javascripts/server_rendering.js', + "#{test_app_path}/app/assets/javascripts/server_rendering.js", force: true template 'assets/javascripts/components.rb', "#{test_app_path}/app/views/components.rb", force: true template 'views/components/hello_world.rb', @@ -70,6 +72,10 @@ def configure_opal_rspec config.opal.spec_location = 'spec-opal' config.hyperloop.auto_config = false + config.react.server_renderer_options = { + files: ["server_rendering.js"] + } + config.react.server_renderer_directories = ["/app/assets/javascripts"] ] end end diff --git a/lib/hyper-react.rb b/lib/hyper-react.rb index 8dcb4ac..754762a 100644 --- a/lib/hyper-react.rb +++ b/lib/hyper-react.rb @@ -1,14 +1,12 @@ require 'hyperloop-config' Hyperloop.import 'hyper-store' -Hyperloop.import 'react/react-source-browser' +Hyperloop.import 'react/react-source-browser', client_only: true Hyperloop.import 'react/react-source-server', server_only: true -Hyperloop.import 'opal-jquery', client_only: true Hyperloop.import 'browser/delay', client_only: true -Hyperloop.import 'react_ujs', client_only: true Hyperloop.import 'hyper-react' +Hyperloop.import 'react_ujs' if RUBY_ENGINE == 'opal' - module Hyperloop class Component end @@ -28,6 +26,7 @@ class Component end require 'react/hash' require 'react/top_level' + require 'react/top_level_render' require 'react/observable' require 'react/validator' require 'react/component' @@ -41,7 +40,7 @@ class Component require 'react/rendering_context' require 'react/state' require 'react/object' - require "react/ext/opal-jquery/element" + require 'react/ext/opal-jquery/element' require 'reactive-ruby/isomorphic_helpers' require 'rails-helpers/top_level_rails_component' require 'reactive-ruby/version' @@ -53,17 +52,11 @@ def self.inherited(child) end end React::Component.deprecation_warning( - 'components.rb', + 'component.rb', "Requiring 'hyper-react' is deprecated. Use gem 'hyper-component', and require 'hyper-component' instead." ) unless defined? Hyperloop::Component::VERSION else require 'opal' - # rubocop:disable Lint/HandleExceptions - begin - require 'opal-jquery' - rescue LoadError - end - # rubocop:enable Lint/HandleExceptions require 'hyper-store' require 'opal-activesupport' @@ -73,5 +66,5 @@ def self.inherited(child) require 'reactive-ruby/serializers' Opal.append_path File.expand_path('../', __FILE__).untaint - require "react/react-source" + require 'react/react-source' end diff --git a/lib/rails-helpers/top_level_rails_component.rb b/lib/rails-helpers/top_level_rails_component.rb index 4e25068..27e37af 100644 --- a/lib/rails-helpers/top_level_rails_component.rb +++ b/lib/rails-helpers/top_level_rails_component.rb @@ -3,7 +3,7 @@ class TopLevelRailsComponent include Hyperloop::Component::Mixin def self.search_path - @search_path ||= [Module] + @search_path ||= [Object] end export_component @@ -16,24 +16,45 @@ def self.search_path def render paths_searched = [] - if params.component_name.start_with? "::" - paths_searched << params.component_name.gsub(/^\:\:/,"") - component = params.component_name.gsub(/^\:\:/,"").split("::").inject(Module) { |scope, next_const| scope.const_get(next_const, false) } rescue nil - return present component, params.render_params if component && component.method_defined?(:render) + component = nil + if params.component_name.start_with?('::') + # if absolute path of component is given, look it up and fail if not found + paths_searched << params.component_name + component = begin + Object.const_get(params.component_name) + rescue NameError + nil + end else - self.class.search_path.each do |path| - # try each path + params.controller + params.component_name - paths_searched << "#{path.name + '::' unless path == Module}#{params.controller}::#{params.component_name}" - component = "#{params.controller}::#{params.component_name}".split("::").inject(path) { |scope, next_const| scope.const_get(next_const, false) } rescue nil - return present component, params.render_params if component && component.method_defined?(:render) + # if relative path is given, look it up like this + # 1) we check each path + controller-name + component-name + # 2) if we can't find it there we check each path + component-name + # if we can't find it we just try const_get + # so (assuming controller name is Home) + # ::Foo::Bar will only resolve to some component named ::Foo::Bar + # but Foo::Bar will check (in this order) ::Home::Foo::Bar, ::Components::Home::Foo::Bar, ::Foo::Bar, ::Components::Foo::Bar + self.class.search_path.each do |scope| + paths_searched << "#{scope.name}::#{params.controller}::#{params.component_name}" + component = begin + scope.const_get(params.controller, false).const_get(params.component_name, false) + rescue NameError + nil + end + break if component != nil end - self.class.search_path.each do |path| - # then try each path + params.component_name - paths_searched << "#{path.name + '::' unless path == Module}#{params.component_name}" - component = "#{params.component_name}".split("::").inject(path) { |scope, next_const| scope.const_get(next_const, false) } rescue nil - return present component, params.render_params if component && component.method_defined?(:render) + unless component + self.class.search_path.each do |scope| + paths_searched << "#{scope.name}::#{params.component_name}" + component = begin + scope.const_get(params.component_name, false) + rescue NameError + nil + end + break if component != nil + end end end + return React::RenderingContext.render(component, params.render_params) if component && component.method_defined?(:render) raise "Could not find component class '#{params.component_name}' for params.controller '#{params.controller}' in any component directory. Tried [#{paths_searched.join(", ")}]" end end diff --git a/lib/react-sources/react-server.js b/lib/react-sources/react-server.js deleted file mode 100644 index 4236cb8..0000000 --- a/lib/react-sources/react-server.js +++ /dev/null @@ -1,2 +0,0 @@ -// A placeholder file to prevent file not found error of requireing -// `react-server` in react/react-source diff --git a/lib/react/api.rb b/lib/react/api.rb index c3ab079..16aef16 100644 --- a/lib/react/api.rb +++ b/lib/react/api.rb @@ -27,8 +27,7 @@ def self.eval_native_react_component(name) (`!!#{component}.prototype.isReactComponent` || `!!#{component}.prototype.render`) is_functional_component = `typeof #{component} === "function"` - is_not_using_react_v13 = `!Opal.global.React.version.match(/0\.13/)` - unless is_component_class || (is_not_using_react_v13 && is_functional_component) + unless is_component_class || is_functional_component raise 'does not appear to be a native react component' end component @@ -46,56 +45,84 @@ def self.create_native_react_class(type) render_fn = (type.method_defined? :_render_wrapper) ? :_render_wrapper : :render # this was hashing type.to_s, not sure why but .to_s does not work as it Foo::Bar::View.to_s just returns "View" @@component_classes[type] ||= %x{ - React.createClass({ - displayName: #{type.name}, - propTypes: #{type.respond_to?(:prop_types) ? type.prop_types.to_n : `{}`}, - getDefaultProps: function(){ + class extends React.Component { + constructor(props) { + super(props); + this.mixins = #{type.respond_to?(:native_mixins) ? type.native_mixins : `[]`}; + this.statics = #{type.respond_to?(:static_call_backs) ? type.static_call_backs.to_n : `{}`}; + this.state = {}; + this.__opalInstanceInitializedState = false; + this.__opalInstanceSyncSetState = true; + this.__opalInstance = #{type.new(`this`)}; + this.__opalInstanceInitializedState = true; + this.__opalInstanceSyncSetState = false; + } + static get displayName() { + return #{type.name}; + } + static get defaultProps() { return #{type.respond_to?(:default_props) ? type.default_props.to_n : `{}`}; - }, - mixins: #{type.respond_to?(:native_mixins) ? type.native_mixins : `[]`}, - statics: #{type.respond_to?(:static_call_backs) ? type.static_call_backs.to_n : `{}`}, - componentWillMount: function() { - var instance = this._getOpalInstance.apply(this); - return #{`instance`.component_will_mount if type.method_defined? :component_will_mount}; - }, - componentDidMount: function() { - var instance = this._getOpalInstance.apply(this); - return #{`instance`.component_did_mount if type.method_defined? :component_did_mount}; - }, - componentWillReceiveProps: function(next_props) { - var instance = this._getOpalInstance.apply(this); - return #{`instance`.component_will_receive_props(Hash.new(`next_props`)) if type.method_defined? :component_will_receive_props}; - }, - shouldComponentUpdate: function(next_props, next_state) { - var instance = this._getOpalInstance.apply(this); - return #{`instance`.should_component_update?(Hash.new(`next_props`), Hash.new(`next_state`)) if type.method_defined? :should_component_update?}; - }, - componentWillUpdate: function(next_props, next_state) { - var instance = this._getOpalInstance.apply(this); - return #{`instance`.component_will_update(Hash.new(`next_props`), Hash.new(`next_state`)) if type.method_defined? :component_will_update}; - }, - componentDidUpdate: function(prev_props, prev_state) { - var instance = this._getOpalInstance.apply(this); - return #{`instance`.component_did_update(Hash.new(`prev_props`), Hash.new(`prev_state`)) if type.method_defined? :component_did_update}; - }, - componentWillUnmount: function() { - var instance = this._getOpalInstance.apply(this); - return #{`instance`.component_will_unmount if type.method_defined? :component_will_unmount}; - }, - _getOpalInstance: function() { - if (this.__opalInstance == undefined) { - var instance = #{type.new(`this`)}; - } else { - var instance = this.__opalInstance; + } + static get propTypes() { + return #{type.respond_to?(:prop_types) ? type.prop_types.to_n : `{}`}; + } + componentWillMount() { + if (#{type.method_defined? :component_will_mount}) { + this.__opalInstanceSyncSetState = true; + this.__opalInstance.$component_will_mount(); + this.__opalInstanceSyncSetState = false; + } + } + componentDidMount() { + this.__opalInstance.is_mounted = true + if (#{type.method_defined? :component_did_mount}) { + this.__opalInstanceSyncSetState = false; + this.__opalInstance.$component_did_mount(); + } + } + componentWillReceiveProps(next_props) { + if (#{type.method_defined? :component_will_receive_props}) { + this.__opalInstanceSyncSetState = true; + this.__opalInstance.$component_will_receive_props(Opal.Hash.$new(next_props)); + this.__opalInstanceSyncSetState = false; + } + } + shouldComponentUpdate(next_props, next_state) { + if (#{type.method_defined? :should_component_update?}) { + this.__opalInstanceSyncSetState = false; + return this.__opalInstance["$should_component_update?"](Opal.Hash.$new(next_props), Opal.Hash.$new(next_state)); + } else { return true; } + } + componentWillUpdate(next_props, next_state) { + if (#{type.method_defined? :component_will_update}) { + this.__opalInstanceSyncSetState = false; + this.__opalInstance.$component_will_update(Opal.Hash.$new(next_props), Opal.Hash.$new(next_state)); } - this.__opalInstance = instance; - return instance; - }, - render: function() { - var instance = this._getOpalInstance.apply(this); - return #{`instance`.send(render_fn).to_n}; } - }) + componentDidUpdate(prev_props, prev_state) { + if (#{type.method_defined? :component_did_update}) { + this.__opalInstanceSyncSetState = false; + this.__opalInstance.$component_did_update(Opal.Hash.$new(prev_props), Opal.Hash.$new(prev_state)); + } + } + componentWillUnmount() { + this.__opalInstance.is_mounted = false; + if (#{type.method_defined? :component_will_unmount}) { + this.__opalInstanceSyncSetState = false; + this.__opalInstance.$component_will_unmount(); + } + } + componentDidCatch(error, info) { + if (#{type.method_defined? :component_did_catch}) { + this.__opalInstanceSyncSetState = false; + this.__opalInstance.$component_did_catch(error, Opal.Hash.$new(info)); + } + } + render() { + this.__opalInstanceSyncSetState = false; + return this.__opalInstance.$send(render_fn).$to_n(); + } + } } end @@ -103,13 +130,14 @@ def self.create_element(type, properties = {}, &block) params = [] # Component Spec, Normal DOM, String or Native Component - if @@component_classes[type] - params << @@component_classes[type] - elsif type.kind_of?(Class) + ncc = @@component_classes[type] + if ncc + params << ncc + elsif type.is_a?(Class) params << create_native_react_class(type) - elsif React::Component::Tags::HTML_TAGS.include?(type) + elsif block_given? || React::Component::Tags::HTML_TAGS.include?(type) params << type - elsif type.is_a? String + elsif type.is_a?(String) return React::Element.new(type) else raise "#{type} not implemented" @@ -121,9 +149,12 @@ def self.create_element(type, properties = {}, &block) # Children Nodes if block_given? - [yield].flatten.each do |ele| - params << ele.to_n - end + a = [yield].flatten + %x{ + for(var i=0, l=a.length; i e - self.class.process_exception(e, self) end def component_did_mount @@ -63,31 +64,23 @@ def component_did_mount run_callback(:after_mount) React::State.update_states_to_observe end - rescue Exception => e - self.class.process_exception(e, self) end def component_will_receive_props(next_props) # need to rethink how this works in opal-react, or if its actually that useful within the react.rb environment # for now we are just using it to clear processed_params - React::State.set_state_context_to(self) { self.run_callback(:before_receive_props, Hash.new(next_props)) } - rescue Exception => e - self.class.process_exception(e, self) + React::State.set_state_context_to(self) { self.run_callback(:before_receive_props, next_props) } end def component_will_update(next_props, next_state) - React::State.set_state_context_to(self) { self.run_callback(:before_update, Hash.new(next_props), Hash.new(next_state)) } - rescue Exception => e - self.class.process_exception(e, self) + React::State.set_state_context_to(self) { self.run_callback(:before_update, next_props, next_state) } end def component_did_update(prev_props, prev_state) React::State.set_state_context_to(self) do - self.run_callback(:after_update, Hash.new(prev_props), Hash.new(prev_state)) + self.run_callback(:after_update, prev_props, prev_state) React::State.update_states_to_observe end - rescue Exception => e - self.class.process_exception(e, self) end def component_will_unmount @@ -95,8 +88,20 @@ def component_will_unmount self.run_callback(:before_unmount) React::State.remove end - rescue Exception => e - self.class.process_exception(e, self) + end + + def component_did_catch(error, info) + React::State.set_state_context_to(self) do + if self.class.callbacks_for(:after_error) == [] + if `typeof error.$backtrace === "function"` + `console.error(error.$backtrace().$join("\n"))` + else + `console.error(error, info)` + end + else + self.run_callback(:after_error, error, info) + end + end end attr_reader :waiting_on_resources @@ -104,8 +109,12 @@ def component_will_unmount def update_react_js_state(object, name, value) if object name = "#{object.class}.#{name}" unless object == self + # Date.now() has only millisecond precision, if several notifications of + # observer happen within a millisecond, updates may get lost. + # to mitigate this the Math.random() appends some random number + # this way notifactions will happen as expected by the rest of hyperloop set_state( - '***_state_updated_at-***' => Time.now.to_f, + '***_state_updated_at-***' => `Date.now() + Math.random()`, name => value ) else @@ -113,6 +122,10 @@ def update_react_js_state(object, name, value) end end + def set_state_synchronously? + @native.JS[:__opalInstanceSyncSetState] + end + def render raise 'no render defined' end unless method_defined?(:render) @@ -124,10 +137,6 @@ def _render_wrapper element.waiting_on_resources if element.respond_to? :waiting_on_resources element end - # rubocop:disable Lint/RescueException # we want to catch all exceptions regardless - rescue Exception => e - # rubocop:enable Lint/RescueException - self.class.process_exception(e, self) end def watch(value, &on_change) diff --git a/lib/react/component/api.rb b/lib/react/component/api.rb index 921465d..525a0cc 100644 --- a/lib/react/component/api.rb +++ b/lib/react/component/api.rb @@ -2,17 +2,11 @@ module React module Component module API def dom_node - if !(`typeof ReactDOM === 'undefined' || typeof ReactDOM.findDOMNode === 'undefined'`) - `ReactDOM.findDOMNode(#{self}.native)` # v0.14.0 - elsif !(`typeof React.findDOMNode === 'undefined'`) - `React.findDOMNode(#{self}.native)` # v0.13.0 - else - `#{self}.native.getDOMNode` # v0.12.0 - end + `ReactDOM.findDOMNode(#{self}.native)` # react >= v0.15.0 end def mounted? - `#{self}.native.isMounted()` + `(#{self}.is_mounted === undefined) ? false : #{self}.is_mounted` end def force_update! @@ -20,30 +14,55 @@ def force_update! end def set_props(prop, &block) - set_or_replace_state_or_prop(prop, 'setProps', &block) - end - - def set_props!(prop, &block) - set_or_replace_state_or_prop(prop, 'replaceProps', &block) + raise "set_props: setProps() is no longer supported by react" end + alias :set_props! :set_props def set_state(state, &block) set_or_replace_state_or_prop(state, 'setState', &block) end def set_state!(state, &block) - set_or_replace_state_or_prop(state, 'replaceState', &block) + set_or_replace_state_or_prop(state, 'setState', &block) + `#{self}.native.forceUpdate()` end private def set_or_replace_state_or_prop(state_or_prop, method, &block) raise "No native ReactComponent associated" unless @native - %x{ - #{@native}[#{method}](#{state_or_prop.shallow_to_n}, function(){ - #{block.call if block} - }); - } + `var state_prop_n = #{state_or_prop.shallow_to_n}` + # the state object is initalized when the ruby component is instanciated + # this is detected by self.native.__opalInstanceInitializedState + # which is set in the netive component constructor in react/api.rb + # the setState update callback is not called when initalizing initial state + if block + %x{ + if (#{@native}.__opalInstanceInitializedState === true) { + #{@native}[method](state_prop_n, function(){ + #{block.call} + }); + } else { + for (var sp in state_prop_n) { + if (state_prop_n.hasOwnProperty(sp)) { + #{@native}.state[sp] = state_prop_n[sp]; + } + } + } + } + else + %x{ + if (#{@native}.__opalInstanceInitializedState === true) { + #{@native}[method](state_prop_n); + } else { + for (var sp in state_prop_n) { + if (state_prop_n.hasOwnProperty(sp)) { + #{@native}.state[sp] = state_prop_n[sp]; + } + } + } + } + end end end end diff --git a/lib/react/component/class_methods.rb b/lib/react/component/class_methods.rb index 23793f5..9993906 100644 --- a/lib/react/component/class_methods.rb +++ b/lib/react/component/class_methods.rb @@ -16,17 +16,6 @@ def backtrace(*args) @backtrace_off = @dont_catch_exceptions || (args[0] == :off) end - def process_exception(e, component, reraise = @dont_catch_exceptions) - unless @dont_catch_exceptions - message = ["Exception raised while rendering #{component}: #{e.message}"] - if e.backtrace && e.backtrace.length > 1 && !@backtrace_off - append_backtrace(message, e.backtrace) - end - `console.error(#{message.join("\n")})` - end - raise e if reraise - end - def append_backtrace(message_array, backtrace) message_array << " #{backtrace[0]}" backtrace[1..-1].each { |line| message_array << line } @@ -63,8 +52,7 @@ def prop_types _componentValidator: %x{ function(props, propName, componentName) { var errors = #{validator.validate(Hash.new(`props`))}; - var error = new Error(#{"In component `#{name}`\n" + `errors`.join("\n")}); - return #{`errors`.count > 0 ? `error` : `undefined`}; + return #{`errors`.count > 0 ? `new Error(#{"In component `#{name}`\n" + `errors`.join("\n")})` : `undefined`}; } } } @@ -184,6 +172,10 @@ def add_item_to_tree(current_tree, new_item) current_tree end end + + def to_n + React::API.class_eval('@@component_classes')[self] + end end end end diff --git a/lib/react/component/should_component_update.rb b/lib/react/component/should_component_update.rb index fea8e93..554e228 100644 --- a/lib/react/component/should_component_update.rb +++ b/lib/react/component/should_component_update.rb @@ -23,14 +23,13 @@ module Component # the need for needs_update? # module ShouldComponentUpdate - def should_component_update?(native_next_props, native_next_state) + def should_component_update?(next_props, next_state) State.set_state_context_to(self, false) do - next_params = Hash.new(native_next_props) # rubocop:disable Style/DoubleNegation # we must return true/false to js land if respond_to?(:needs_update?) - !!call_needs_update(next_params, native_next_state) + !!call_needs_update(next_props, next_state) else - !!(props_changed?(next_params) || native_state_changed?(native_next_state)) + (props_changed?(next_props) || native_state_changed?(next_state)) end # rubocop:enable Style/DoubleNegation end @@ -39,14 +38,13 @@ def should_component_update?(native_next_props, native_next_state) # create opal hashes for next params and state, and attach # the changed? method to each hash - def call_needs_update(next_params, native_next_state) + def call_needs_update(next_params, next_state) component = self next_params.define_singleton_method(:changed?) do component.props_changed?(self) end - next_state = Hash.new(native_next_state) next_state.define_singleton_method(:changed?) do - component.native_state_changed?(native_next_state) + component.native_state_changed?(next_state) end needs_update?(next_params, next_state) end @@ -55,6 +53,10 @@ def call_needs_update(next_params, native_next_state) # We can rapidly check for state changes comparing the incoming state time_stamp # with the current time stamp. + # we receive a Opal Ruby Hash here, always, so the Hash is either empty or filled + # Hash is converted to native object + # if the Hash was empty, the Object has no keys + # Different versions of react treat empty state differently, so we first # convert anything that looks like an empty state to "false" for consistency. @@ -64,30 +66,33 @@ def call_needs_update(next_params, native_next_state) # Otherwise we check time stamps # rubocop:disable Metrics/MethodLength # for effeciency we want this to be one method - def native_state_changed?(next_state) - %x{ - var current_state = #{@native}.state - var normalized_next_state = - !#{next_state} || Object.keys(#{next_state}).length === 0 || #{nil} == #{next_state} ? - false : #{next_state} - var normalized_current_state = - !current_state || Object.keys(current_state).length === 0 || #{nil} == current_state ? - false : current_state - if (!normalized_current_state != !normalized_next_state) return(true) - if (!normalized_current_state && !normalized_next_state) return(false) - if (!normalized_current_state['***_state_updated_at-***'] || - !normalized_next_state['***_state_updated_at-***']) return(true) - return (normalized_current_state['***_state_updated_at-***'] != - normalized_next_state['***_state_updated_at-***']) - } + def native_state_changed?(next_state_hash) + # next_state = next_state_hash.to_n + # %x{ + # var current_state = #{@native}.state + # var normalized_next_state = + # !next_state || Object.keys(next_state).length === 0 ? false : next_state + # var normalized_current_state = + # !current_state || Object.keys(current_state).length === 0 ? false : current_state + # if (!normalized_current_state != !normalized_next_state) return(true) + # if (!normalized_current_state && !normalized_next_state) return(false) + # if (!normalized_current_state['***_state_updated_at-***'] && + # !normalized_next_state['***_state_updated_at-***']) return(false) + # if (!normalized_current_state['***_state_updated_at-***'] || + # !normalized_next_state['***_state_updated_at-***']) return(true) + # return (normalized_current_state['***_state_updated_at-***'] != + # normalized_next_state['***_state_updated_at-***']) + # } + state_hash = Hash.new(`#{@native}.state`) + next_state_hash != state_hash end # rubocop:enable Metrics/MethodLength # Do a shallow compare on the two hashes. Starting in 0.9 we will do a deep compare. ??? - def props_changed?(next_params) - (props.keys.sort != next_params.keys.sort) || - next_params.detect { |k, v| `#{v} != #{@native}.props[#{k}]` } + def props_changed?(next_props) + props = Hash.new(`#{@native}.props`) + next_props != props end end end diff --git a/lib/react/component/tags.rb b/lib/react/component/tags.rb index ba50118..92e8ade 100644 --- a/lib/react/component/tags.rb +++ b/lib/react/component/tags.rb @@ -1,15 +1,3 @@ -# class HtmlTagWrapper -# def initialize(name) -# @name = name -# end -# def to_s -# @name -# end -# def method_missing(n) -# -# end - - module React module Component # contains the name of all HTML tags, and the mechanism to register a component @@ -28,36 +16,27 @@ module Tags radialGradient rect stop svg text tspan) # the present method is retained as a legacy behavior - def present(component, *params, &children) React::RenderingContext.render(component, *params, &children) end # define each predefined tag as an instance method - - - - HTML_TAGS.each do |tag| - define_method(tag) do |*params, &children| - if tag == 'p' + if tag == 'p' + define_method(tag) do |*params, &children| if children || params.count == 0 || (params.count == 1 && params.first.is_a?(Hash)) React::RenderingContext.render(tag, *params, &children) else Kernel.p(*params) end - else - React::RenderingContext.render(tag, *params, &children) end - end - if tag != :div - alias_method tag.upcase, tag - const_set tag.upcase, tag else - alias_method tag.upcase, tag - #const_set tag.upcase, React.create_element(tag) - #Object.const_set tag.upcase, Class.new(HtmlTagWrapper) + define_method(tag) do |*params, &children| + React::RenderingContext.render(tag, *params, &children) + end end + alias_method tag.upcase, tag + const_set tag.upcase, tag end def self.html_tag_class_for(tag) diff --git a/lib/react/config/server.rb b/lib/react/config/server.rb index e65cc6b..81c2798 100644 --- a/lib/react/config/server.rb +++ b/lib/react/config/server.rb @@ -18,6 +18,6 @@ def default_config end end module Hyperloop - define_setting :prerendering, :on + define_setting :prerendering, :off end end diff --git a/lib/react/element.rb b/lib/react/element.rb index e57592b..707421e 100644 --- a/lib/react/element.rb +++ b/lib/react/element.rb @@ -36,7 +36,7 @@ def initialize(native_element, type = nil, properties = {}, block = nil) def on(*event_names, &block) event_names.each { |event_name| merge_event_prop!(event_name, &block) } - @native = `React.cloneElement(#{to_n}, #{properties.shallow_to_n})` + @native = `React.cloneElement(#{@native}, #{@properties.shallow_to_n})` self end @@ -50,8 +50,8 @@ def render(props = {}, &new_block) else props = API.convert_props(props) React::RenderingContext.render( - Element.new(`React.cloneElement(#{to_n}, #{props.shallow_to_n})`, - type, properties.merge(props), block), + Element.new(`React.cloneElement(#{@native}, #{props.shallow_to_n})`, + type, @properties.merge(props), block), ) end end @@ -62,12 +62,8 @@ def render(props = {}, &new_block) def delete React::RenderingContext.delete(self) end - # Deprecated version of delete method - - def as_node - React::RenderingContext.as_node(self) - end + alias as_node delete # Any other method applied to an element will be treated as class name (haml style) thus # div.foo.bar(id: :fred) is the same as saying div(class: "foo bar", id: :fred) @@ -98,7 +94,7 @@ def self.haml_class_name(class_name) def build_new_properties(class_name, args) class_name = self.class.haml_class_name(class_name) - new_props = properties.dup + new_props = @properties.dup new_props[:className] = "\ #{class_name} #{new_props[:className]} #{args.delete(:class)} #{args.delete(:className)}\ ".split(' ').uniq.join(' ') @@ -127,7 +123,6 @@ def merge_event_prop!(event_name, &block) elsif @type.instance_variable_get('@native_import') merge_component_event_prop! name, &block else - merge_deprecated_component_event_prop! event_name, &block merge_component_event_prop! "on_#{event_name}", &block end end @@ -151,19 +146,5 @@ def merge_component_event_prop!(prop_name) } ) end - - def merge_deprecated_component_event_prop!(event_name) - prop_name = "_on#{event_name.event_camelize}" - fn = %x{function(){#{ - React::Component.deprecation_warning( - type, - "In future releases React::Element#on('#{event_name}') will no longer respond "\ - "to the '#{prop_name}' emitter.\n"\ - "Rename your emitter param to 'on_#{event_name}' or use .on('<#{prop_name}>')" - )} - return #{yield(*Array(`arguments`))} - }} - @properties.merge!(prop_name => fn) - end end end diff --git a/lib/react/ext/hash.rb b/lib/react/ext/hash.rb index 5a5dddc..735ff32 100644 --- a/lib/react/ext/hash.rb +++ b/lib/react/ext/hash.rb @@ -1,7 +1,7 @@ class Hash def shallow_to_n hash = `{}` - self.map do |key, value| + self.each do |key, value| `hash[#{key}] = #{value}` end hash diff --git a/lib/react/ext/string.rb b/lib/react/ext/string.rb index 4f979c1..75eff10 100644 --- a/lib/react/ext/string.rb +++ b/lib/react/ext/string.rb @@ -1,6 +1,6 @@ class String def event_camelize - `#{self}.replace(/(^|_)([^_]+)/g, function(match, pre, word, index) { + `return #{self}.replace(/(^|_)([^_]+)/g, function(match, pre, word, index) { var capitalize = true; return capitalize ? word.substr(0,1).toUpperCase()+word.substr(1) : word; })` diff --git a/lib/react/native_library.rb b/lib/react/native_library.rb index 56c677e..93ba661 100644 --- a/lib/react/native_library.rb +++ b/lib/react/native_library.rb @@ -46,7 +46,7 @@ def const_missing(const_name) end def method_missing(method, *args, &block) - component_class = get_const(method) if const_defined?(method) + component_class = const_get(method) if const_defined?(method, false) component_class ||= import_const_from_native(self, method, false) raise 'could not import a react component named: '\ "#{scope_native_name method}" unless component_class diff --git a/lib/react/object.rb b/lib/react/object.rb index efeb700..02f7b92 100644 --- a/lib/react/object.rb +++ b/lib/react/object.rb @@ -12,4 +12,19 @@ def const_missing(const_name) React::Component::Tags.html_tag_class_for(const_name) || raise(e) end end + + def to_key + object_id + end +end +class Number + def to_key + self + end +end + +class Boolean + def to_key + self + end end diff --git a/lib/react/react-source-server.rb b/lib/react/react-source-server.rb index 4980be8..9a214e3 100644 --- a/lib/react/react-source-server.rb +++ b/lib/react/react-source-server.rb @@ -1,3 +1,3 @@ if RUBY_ENGINE == 'opal' - require "react-server.js" + require 'react-server.js' end diff --git a/lib/react/react-source.rb b/lib/react/react-source.rb index a5320ca..20fe099 100644 --- a/lib/react/react-source.rb +++ b/lib/react/react-source.rb @@ -11,10 +11,6 @@ else require "react/config" require "react/rails/asset_variant" - react_directory = React::Rails::AssetVariant.new( - addons: true, - variant: React::Config.config[:environment].to_sym - ).react_directory + react_directory = React::Rails::AssetVariant.new(React::Config.config).react_directory Opal.append_path react_directory.untaint - Opal.append_path File.expand_path('../../react-sources/', __FILE__).untaint end diff --git a/lib/react/ref_callback.rb b/lib/react/ref_callback.rb index 4ce57e5..d0bc0df 100644 --- a/lib/react/ref_callback.rb +++ b/lib/react/ref_callback.rb @@ -14,10 +14,10 @@ def self.convert_props(properties) props.map do |key, value| if key == "ref" && value.is_a?(Proc) new_proc = Proc.new do |native_inst| - if `#{native_inst}._getOpalInstance !== undefined` - value.call(`#{native_inst}._getOpalInstance()`) - elsif `React.findDOMNode !== undefined && #{native_inst}.nodeType === undefined` - value.call(`React.findDOMNode(#{native_inst})`) + if `#{native_inst}.__opalInstance !== undefined && #{native_inst}.__opalInstance !== null` + value.call(`#{native_inst}.__opalInstance`) + elsif `ReactDOM.findDOMNode !== undefined && #{native_inst}.nodeType === undefined` + value.call(`ReactDOM.findDOMNode(#{native_inst})`) # react >= v0.15.`) else value.call(native_inst) end diff --git a/lib/react/rendering_context.rb b/lib/react/rendering_context.rb index e1ea0c9..2e23206 100644 --- a/lib/react/rendering_context.rb +++ b/lib/react/rendering_context.rb @@ -15,20 +15,21 @@ def render(name, *args, &block) run_child_block(name.nil?, &block) if name buffer = @buffer.dup - React.create_element(name, *args) { buffer }.tap do |element| + React::API.create_element(name, *args) { buffer }.tap do |element| element.waiting_on_resources = saved_waiting_on_resources || !!buffer.detect { |e| e.waiting_on_resources if e.respond_to?(:waiting_on_resources) } - element.waiting_on_resources ||= buffer.last.is_a?(String) && waiting_on_resources + element.waiting_on_resources ||= waiting_on_resources if buffer.last.is_a?(String) end elsif @buffer.last.is_a? React::Element @buffer.last.tap { |element| element.waiting_on_resources ||= saved_waiting_on_resources } else - @buffer.last.to_s.span.tap { |element| element.waiting_on_resources = saved_waiting_on_resources } + buffer_s = @buffer.last.to_s + React::RenderingContext.render(:span) { buffer_s }.tap { |element| element.waiting_on_resources = saved_waiting_on_resources } end end elsif name.is_a? React::Element element = name else - element = React.create_element(name, *args) + element = React::API.create_element(name, *args) element.waiting_on_resources = waiting_on_resources end @buffer << element @@ -46,12 +47,11 @@ def build return_val end - def as_node(element) + def delete(element) @buffer.delete(element) element end - - alias delete as_node + alias as_node delete def rendered?(element) @buffer.include? element @@ -64,7 +64,7 @@ def replace(e1, e2) def remove_nodes_from_args(args) args[0].each do |key, value| begin - value.as_node if value.is_a?(Element) + value.delete if value.is_a?(Element) # deletes Element from buffer rescue Exception end end if args[0] && args[0].is_a?(Hash) @@ -90,11 +90,13 @@ def remove_nodes_from_args(args) # so we insure that is the case, and also check to make sure that element in the buffer # is the element returned - def run_child_block(is_outer_scope) result = yield - result = result.to_s.span if result.try :acts_as_string? || result.is_a?(String) - @buffer << result if result.is_a?(String) || (result.is_a?(React::Element) && @buffer.empty?) + if result.respond_to?(:acts_as_string?) && result.acts_as_string? + @buffer << result.to_s + elsif result.is_a?(String) || (result.is_a?(React::Element) && @buffer.empty?) + @buffer << result + end raise_render_error(result) if is_outer_scope && @buffer != [result] end @@ -117,28 +119,28 @@ def improper_render(message, solution) end end end +end - class ::Object - [:span, :td, :th, :while_loading].each do |tag| - define_method(tag) do |*args, &block| - args.unshift(tag) - return send(*args, &block) if is_a? React::Component - React::RenderingContext.render(*args) { to_s } - end - end - - def para(*args, &block) - args.unshift(:p) +class Object + [:span, :td, :th, :while_loading].each do |tag| + define_method(tag) do |*args, &block| + args.unshift(tag) return send(*args, &block) if is_a? React::Component React::RenderingContext.render(*args) { to_s } end + end - def br - return send(:br) if is_a? React::Component - React::RenderingContext.render(:span) do - React::RenderingContext.render(to_s) - React::RenderingContext.render(:br) - end + def para(*args, &block) + args.unshift(:p) + return send(*args, &block) if is_a? React::Component + React::RenderingContext.render(*args) { to_s } + end + + def br + return send(:br) if is_a? React::Component + React::RenderingContext.render(:span) do + React::RenderingContext.render(to_s) + React::RenderingContext.render(:br) end end end diff --git a/lib/react/server.rb b/lib/react/server.rb index 3bea64c..e5f3f7d 100644 --- a/lib/react/server.rb +++ b/lib/react/server.rb @@ -3,8 +3,6 @@ module Server def self.render_to_string(element) if !(`typeof ReactDOMServer === 'undefined'`) React::RenderingContext.build { `ReactDOMServer.renderToString(#{element.to_n})` } # v0.15+ - elsif !(`typeof React.renderToString === 'undefined'`) - React::RenderingContext.build { `React.renderToString(#{element.to_n})` } else raise "renderToString is not defined. In React >= v15 you must import it with ReactDOMServer" end @@ -13,8 +11,6 @@ def self.render_to_string(element) def self.render_to_static_markup(element) if !(`typeof ReactDOMServer === 'undefined'`) React::RenderingContext.build { `ReactDOMServer.renderToStaticMarkup(#{element.to_n})` } # v0.15+ - elsif !(`typeof React.renderToString === 'undefined'`) - React::RenderingContext.build { `React.renderToStaticMarkup(#{element.to_n})` } else raise "renderToStaticMarkup is not defined. In React >= v15 you must import it with ReactDOMServer" end diff --git a/lib/react/state_wrapper.rb b/lib/react/state_wrapper.rb index 818dde6..804b772 100644 --- a/lib/react/state_wrapper.rb +++ b/lib/react/state_wrapper.rb @@ -12,9 +12,9 @@ def []=(state, new_value) alias pre_component_method_missing method_missing def method_missing(method, *args) - if method =~ /\!$/ && __from__.respond_to?(:deprecation_warning) - __from__.deprecation_warning("The mutator 'state.#{method}' has been deprecated. Use 'mutate.#{method.gsub(/\!$/,'')}' instead.") - __from__.mutate.__send__(method.gsub(/\!$/,''), *args) + if method.end_with?('!') && __from__.respond_to?(:deprecation_warning) + __from__.deprecation_warning("The mutator 'state.#{method}' has been deprecated. Use 'mutate.#{method.sub(/\!$/,'')}' instead.") + __from__.mutate.__send__(method.chop, *args) else pre_component_method_missing(method, *args) end diff --git a/lib/react/test/utils.rb b/lib/react/test/utils.rb index 93de371..beef361 100644 --- a/lib/react/test/utils.rb +++ b/lib/react/test/utils.rb @@ -1,24 +1,70 @@ module React module Test class Utils - `var ReactTestUtils = React.addons.TestUtils` + def self.render_component_into_document(component, args = {}) + element = React.create_element(component, args) + render_into_document(element) + end - def self.render_into_document(element, options = {}) + def self.render_into_document(element) raise "You should pass a valid React::Element" unless React.is_valid_element?(element) - native_instance = `ReactTestUtils.renderIntoDocument(#{element.to_n})` + dom_el = `document.body.querySelector('div[data-react-class="React.TopLevelRailsComponent"]').appendChild(document.createElement('div'))` + React.render(element, dom_el) + end + + def self.simulate_click(element) + # element must be a component or a dom node or a element + el = if `typeof element.nodeType !== "undefined"` + element + elsif element.is_a? React::Component + element.dom_node + elsif element.is_a? React::Element + `ReactDOM.findDOMNode(#{element.to_n}.native)` + else + element + end + %x{ + var evob = new MouseEvent('click', { + view: window, + bubbles: true, + cancelable: true + }); + el.dispatchEvent(evob); + } + end - if `#{native_instance}._getOpalInstance !== undefined` - `#{native_instance}._getOpalInstance()` - elsif `ReactTestUtils.isDOMComponent(#{native_instance}) && React.findDOMNode !== undefined` - `React.findDOMNode(#{native_instance})` - else - native_instance - end + def self.simulate_keydown(element, key_name = "Enter") + # element must be a component or a dom node or a element + el = if `typeof element.nodeType !== "undefined"` + element + elsif element.is_a? React::Component + element.dom_node + elsif element.is_a? React::Element + `ReactDOM.findDOMNode(#{element.to_n}.native)` + else + element + end + %x{ + var evob = new KeyboardEvent('keydown', { key: key_name, bubbles: true, cancelable: true }); + el.dispatchEvent(evob); + } end - def self.simulate(event, element, params = {}) - simulator = Native(`ReactTestUtils.Simulate`) - simulator[event.to_s].call(`element.$dom_node === undefined` ? element : element.dom_node, params) + def self.simulate_submit(element) + # element must be a component or a dom node or a element + el = if `typeof element.nodeType !== "undefined"` + element + elsif element.is_a? React::Component + element.dom_node + elsif element.is_a? React::Element + `ReactDOM.findDOMNode(#{element.to_n}.native)` + else + element + end + %x{ + var evob = new Event('submit', { bubbles: true, cancelable: true }); + el.dispatchEvent(evob); + } end end end diff --git a/lib/react/top_level.rb b/lib/react/top_level.rb index a671ed0..a9fded8 100644 --- a/lib/react/top_level.rb +++ b/lib/react/top_level.rb @@ -64,8 +64,6 @@ def self.render(element, container) container = `container.$$class ? container[0] : container` if !(`typeof ReactDOM === 'undefined'`) component = Native(`ReactDOM.render(#{element.to_n}, container, function(){#{yield if block_given?}})`) # v0.15+ - elsif !(`typeof React.renderToString === 'undefined'`) - component = Native(`React.render(#{element.to_n}, container, function(){#{yield if block_given?}})`) else raise "render is not defined. In React >= v15 you must import it with ReactDOM" end @@ -87,8 +85,6 @@ def self.render_to_string(element) %x{ console.error("Warning: `React.render_to_string` is deprecated in favor of `React::Server.render_to_string`."); } if !(`typeof ReactDOMServer === 'undefined'`) React::RenderingContext.build { `ReactDOMServer.renderToString(#{element.to_n})` } # v0.15+ - elsif !(`typeof React.renderToString === 'undefined'`) - React::RenderingContext.build { `React.renderToString(#{element.to_n})` } else raise "renderToString is not defined. In React >= v15 you must import it with ReactDOMServer" end @@ -98,8 +94,6 @@ def self.render_to_static_markup(element) %x{ console.error("Warning: `React.render_to_static_markup` is deprecated in favor of `React::Server.render_to_static_markup`."); } if !(`typeof ReactDOMServer === 'undefined'`) React::RenderingContext.build { `ReactDOMServer.renderToStaticMarkup(#{element.to_n})` } # v0.15+ - elsif !(`typeof React.renderToString === 'undefined'`) - React::RenderingContext.build { `React.renderToStaticMarkup(#{element.to_n})` } else raise "renderToStaticMarkup is not defined. In React >= v15 you must import it with ReactDOMServer" end @@ -108,8 +102,6 @@ def self.render_to_static_markup(element) def self.unmount_component_at_node(node) if !(`typeof ReactDOM === 'undefined'`) `ReactDOM.unmountComponentAtNode(node.$$class ? node[0] : node)` # v0.15+ - elsif !(`typeof React.renderToString === 'undefined'`) - `React.unmountComponentAtNode(node.$$class ? node[0] : node)` else raise "unmountComponentAtNode is not defined. In React >= v15 you must import it with ReactDOM" end diff --git a/lib/react/top_level_render.rb b/lib/react/top_level_render.rb index d7aa07f..34867e0 100644 --- a/lib/react/top_level_render.rb +++ b/lib/react/top_level_render.rb @@ -1,27 +1,26 @@ module React def self.render(element, container) + raise "ReactDOM.render is not defined. In React >= v15 you must import it with ReactDOM" if (`typeof ReactDOM === 'undefined'`) + container = `container.$$class ? container[0] : container` - cb = %x{ - function(){ - setTimeout(function(){ - #{yield if block_given?} - }, 0) + if block_given? + cb = %x{ + function(){ + setTimeout(function(){ + #{yield} + }, 0) + } } - } - - if !(`typeof ReactDOM === 'undefined'`) - native = `ReactDOM.render(#{element.to_n}, container, cb)` # v0.15+ - elsif !(`typeof React.renderToString === 'undefined'`) - native = `React.render(#{element.to_n}, container, cb)` + native = `ReactDOM.render(#{element.to_n}, container, cb)` else - raise "render is not defined. In React >= v15 you must import it with ReactDOM" + native = `ReactDOM.render(#{element.to_n}, container)` end - - if `#{native}._getOpalInstance !== undefined` - `#{native}._getOpalInstance()` - elsif `React.findDOMNode !== undefined && #{native}.nodeType === undefined` - `React.findDOMNode(#{native})` + + if `#{native}.__opalInstance !== undefined && #{native}.__opalInstance !== null` + `#{native}.__opalInstance` + elsif `ReactDOM.findDOMNode !== undefined && #{native}.nodeType === undefined` + `ReactDOM.findDOMNode(#{native})` else native end diff --git a/lib/reactive-ruby/component_loader.rb b/lib/reactive-ruby/component_loader.rb index 89738da..f28bac5 100644 --- a/lib/reactive-ruby/component_loader.rb +++ b/lib/reactive-ruby/component_loader.rb @@ -19,11 +19,13 @@ def load!(file = components) return true if loaded? self.load(file) ensure - raise "No react.rb components found in #{components}.rb" unless loaded? + raise "No HyperReact components found in #{components}" unless loaded? end def loaded? - !!v8_context.eval('Opal.React') + !!v8_context.eval('Opal.React !== undefined') + rescue ::ExecJS::Error + false end private @@ -35,16 +37,7 @@ def components end def opal(file) - if Opal::Processor.respond_to?(:load_asset_code) - Opal::Processor.load_asset_code(assets, file) - else - Opal::Sprockets.load_asset(file, assets) - end - rescue # What exception is being caught here? - end - - def assets - ::Rails.application.assets + Opal::Sprockets.load_asset(file) end end end diff --git a/lib/reactive-ruby/isomorphic_helpers.rb b/lib/reactive-ruby/isomorphic_helpers.rb index 40b7cfb..712b861 100644 --- a/lib/reactive-ruby/isomorphic_helpers.rb +++ b/lib/reactive-ruby/isomorphic_helpers.rb @@ -8,15 +8,17 @@ def self.included(base) if RUBY_ENGINE != 'opal' def self.load_context(ctx, controller, name = nil) - puts "************************** React Server Context Initialized #{name} *********************************************" @context = Context.new("#{controller.object_id}-#{Time.now.to_i}", ctx, controller, name) + @context.load_opal_context + ::Rails.logger.debug "************************** React Server Context Initialized #{name} #{Time.now.to_f} *********************************************" + @context end else def self.load_context(unique_id = nil, name = nil) # can be called on the client to force re-initialization for testing purposes if !unique_id || !@context || @context.unique_id != unique_id if on_opal_server? - `console.history = []` rescue nil + `console.history = []` rescue nil message = "************************ React Prerendering Context Initialized #{name} ***********************" else message = "************************ React Browser Context Initialized ****************************" @@ -28,6 +30,10 @@ def self.load_context(unique_id = nil, name = nil) end end + def self.context + @context + end + def self.log(message, message_type = :info) message = [message] unless message.is_a? Array @@ -95,6 +101,11 @@ class Context attr_reader :controller attr_reader :unique_id + def self.define_isomorphic_method(method_name, &block) + @@ctx_methods ||= {} + @@ctx_methods[method_name] = block + end + def self.before_first_mount_blocks @before_first_mount_blocks ||= [] end @@ -103,27 +114,39 @@ def self.prerender_footer_blocks @prerender_footer_blocks ||= [] end - def initialize(unique_id, ctx = nil, controller = nil, name = nil) + def initialize(unique_id, ctx = nil, controller = nil, cname = nil) @unique_id = unique_id + @cname = cname if RUBY_ENGINE != 'opal' @controller = controller @ctx = ctx - ctx["ServerSideIsomorphicMethods"] = self - send_to_opal(:load_context, @unique_id, name) + if defined? @@ctx_methods + @@ctx_methods.each do |method_name, block| + @ctx.attach("ServerSideIsomorphicMethod.#{method_name}", proc{|args| block.call(args.to_json)}) + end + end end Hyperloop::Application::Boot.run(context: self) self.class.before_first_mount_blocks.each { |block| block.call(self) } end + def load_opal_context + send_to_opal(:load_context, @unique_id, @cname) + end + def eval(js) @ctx.eval(js) if @ctx end - def send_to_opal(method, *args) + def send_to_opal(method_name, *args) return unless @ctx args = [1] if args.length == 0 ::ReactiveRuby::ComponentLoader.new(@ctx).load! - @ctx.eval("Opal.React.$const_get('IsomorphicHelpers').$#{method}(#{args.collect { |arg| "'#{arg}'"}.join(', ')})") + method_args = args.collect do |arg| + quarg = "#{arg}".tr('"', "'") + "\"#{quarg}\"" + end.join(', ') + @ctx.eval("Opal.React.$const_get('IsomorphicHelpers').$#{method_name}(#{method_args})") end def self.register_before_first_mount_block(&block) @@ -147,7 +170,7 @@ def initialize(name, block, context, *args) @name = name @context = context block.call(self, *args) - @result ||= send_to_server(*args) if IsomorphicHelpers.on_opal_server? + @result ||= send_to_server(*args) end def when_on_client(&block) @@ -156,8 +179,8 @@ def when_on_client(&block) def send_to_server(*args) if IsomorphicHelpers.on_opal_server? - args_as_json = args.to_json - @result = [JSON.parse(`Opal.global.ServerSideIsomorphicMethods[#{@name}](#{args_as_json})`)] + method_string = "ServerSideIsomorphicMethod." + @name + "(" + args.to_json + ")" + @result = [JSON.parse(`eval(method_string)`)] end end @@ -184,16 +207,16 @@ def controller end def before_first_mount(&block) - React::IsomorphicHelpers::Context.register_before_first_mount_block &block + React::IsomorphicHelpers::Context.register_before_first_mount_block(&block) end def prerender_footer(&block) - React::IsomorphicHelpers::Context.register_prerender_footer_block &block + React::IsomorphicHelpers::Context.register_prerender_footer_block(&block) end if RUBY_ENGINE != 'opal' def isomorphic_method(name, &block) - React::IsomorphicHelpers::Context.send(:define_method, name) do |args_as_json| + React::IsomorphicHelpers::Context.send(:define_isomorphic_method, name) do |args_as_json| React::IsomorphicHelpers::IsomorphicProcCall.new(name, block, self, *JSON.parse(args_as_json)).result end end diff --git a/lib/reactive-ruby/rails.rb b/lib/reactive-ruby/rails.rb index fe8cb15..ad8b287 100644 --- a/lib/reactive-ruby/rails.rb +++ b/lib/reactive-ruby/rails.rb @@ -1,5 +1,6 @@ require 'action_view' require 'react-rails' +require 'reactive-ruby/server_rendering/hyper_asset_container' require 'reactive-ruby/server_rendering/contextual_renderer' require 'reactive-ruby/rails/component_mount' require 'reactive-ruby/rails/railtie' diff --git a/lib/reactive-ruby/rails/component_mount.rb b/lib/reactive-ruby/rails/component_mount.rb index 8b29755..3a948f6 100644 --- a/lib/reactive-ruby/rails/component_mount.rb +++ b/lib/reactive-ruby/rails/component_mount.rb @@ -8,7 +8,9 @@ def setup(controller) end def react_component(name, props = {}, options = {}, &block) - options = context_initializer_options(options, name) if options[:prerender] + if options[:prerender] || [:on, 'on', true].include?(Hyperloop.prerendering) + options = context_initializer_options(options, name) + end props = serialized_props(props, name, controller) super(top_level_name, props, options, &block).gsub("\n","") .gsub(/(