├── .codeclimate.yml ├── .gitignore ├── .rubocop.yml ├── .travis.yml ├── Appraisals ├── CHANGELOG.md ├── DOCS.md ├── Gemfile ├── Gemfile.lock ├── LICENSE ├── README.md ├── Rakefile ├── UPGRADING.md ├── component-name-lookup.md ├── dciy.toml ├── hyper-react.gemspec ├── lib ├── generators │ └── reactive_ruby │ │ └── test_app │ │ ├── templates │ │ ├── assets │ │ │ └── javascripts │ │ │ │ ├── components.rb │ │ │ │ ├── server_rendering.js │ │ │ │ └── test_application.rb │ │ ├── boot.rb.erb │ │ ├── script │ │ │ └── rails │ │ ├── test_application.rb.erb │ │ └── views │ │ │ ├── components │ │ │ ├── hello_world.rb │ │ │ └── todo.rb │ │ │ └── layouts │ │ │ └── test_layout.html.erb │ │ └── test_app_generator.rb ├── hyper-react.rb ├── rails-helpers │ └── top_level_rails_component.rb ├── react │ ├── api.rb │ ├── callbacks.rb │ ├── children.rb │ ├── component.rb │ ├── component │ │ ├── api.rb │ │ ├── base.rb │ │ ├── class_methods.rb │ │ ├── dsl_instance_methods.rb │ │ ├── params.rb │ │ ├── props_wrapper.rb │ │ ├── should_component_update.rb │ │ └── tags.rb │ ├── config.rb │ ├── element.rb │ ├── event.rb │ ├── ext │ │ ├── hash.rb │ │ ├── opal-jquery │ │ │ └── element.rb │ │ └── string.rb │ ├── native_library.rb │ ├── object.rb │ ├── react-source-browser.rb │ ├── react-source-server.rb │ ├── react-source.rb │ ├── ref_callback.rb │ ├── rendering_context.rb │ ├── server.rb │ ├── state_wrapper.rb │ ├── test.rb │ ├── test │ │ ├── dsl.rb │ │ ├── matchers │ │ │ └── render_html_matcher.rb │ │ ├── rspec.rb │ │ ├── session.rb │ │ └── utils.rb │ ├── to_key.rb │ ├── top_level.rb │ ├── top_level_render.rb │ └── validator.rb ├── reactive-ruby │ ├── component_loader.rb │ ├── isomorphic_helpers.rb │ ├── rails.rb │ ├── rails │ │ ├── component_mount.rb │ │ ├── controller_helper.rb │ │ └── railtie.rb │ ├── serializers.rb │ ├── server_rendering │ │ ├── contextual_renderer.rb │ │ └── hyper_asset_container.rb │ └── version.rb └── reactrb │ └── auto-import.rb ├── logo1.png ├── logo2.png ├── logo3.png ├── path_release_steps.md └── spec ├── controller_helper_spec.rb ├── index.html.erb ├── react ├── builtin_tags_spec.rb ├── callbacks_spec.rb ├── children_spec.rb ├── component │ └── base_spec.rb ├── component_spec.rb ├── dsl_spec.rb ├── element_spec.rb ├── event_spec.rb ├── native_library_spec.rb ├── observable_spec.rb ├── opal_jquery_extensions_spec.rb ├── param_declaration_spec.rb ├── react_spec.rb ├── refs_callback_spec.rb ├── server_spec.rb ├── state_spec.rb ├── test │ ├── dsl_spec.rb │ ├── matchers │ │ └── render_html_matcher_spec.rb │ ├── rspec_spec.rb │ ├── session_spec.rb │ └── utils_spec.rb ├── to_key_spec.rb ├── top_level_component_spec.rb ├── tutorial │ └── tutorial_spec.rb └── validator_spec.rb ├── reactive-ruby ├── component_loader_spec.rb ├── isomorphic_helpers_spec.rb ├── rails │ ├── asset_pipeline_spec.rb │ └── component_mount_spec.rb └── server_rendering │ └── contextual_renderer_spec.rb ├── spec_helper.rb ├── test_app ├── README.md ├── Rakefile ├── app │ ├── assets │ │ ├── config │ │ │ └── manifest.js │ │ ├── images │ │ │ └── .keep │ │ ├── javascripts │ │ │ ├── application.rb │ │ │ ├── cable.js │ │ │ ├── channels │ │ │ │ └── .keep │ │ │ └── server_rendering.js │ │ └── stylesheets │ │ │ └── application.css │ ├── channels │ │ └── application_cable │ │ │ ├── channel.rb │ │ │ └── connection.rb │ ├── controllers │ │ ├── application_controller.rb │ │ └── concerns │ │ │ └── .keep │ ├── helpers │ │ └── application_helper.rb │ ├── jobs │ │ └── application_job.rb │ ├── mailers │ │ └── application_mailer.rb │ ├── models │ │ ├── application_record.rb │ │ └── concerns │ │ │ └── .keep │ └── views │ │ ├── components.rb │ │ ├── components │ │ ├── hello_world.rb │ │ └── todo.rb │ │ └── layouts │ │ ├── application.html.erb │ │ ├── explicit_layout.html.erb │ │ ├── mailer.html.erb │ │ ├── mailer.text.erb │ │ └── test_layout.html.erb ├── bin │ ├── bundle │ ├── rails │ ├── rake │ ├── setup │ ├── update │ └── yarn ├── config.ru ├── config │ ├── application.rb │ ├── boot.rb │ ├── cable.yml │ ├── database.yml │ ├── environment.rb │ ├── environments │ │ ├── development.rb │ │ ├── production.rb │ │ └── test.rb │ ├── initializers │ │ ├── application_controller_renderer.rb │ │ ├── assets.rb │ │ ├── backtrace_silencers.rb │ │ ├── cookies_serializer.rb │ │ ├── filter_parameter_logging.rb │ │ ├── inflections.rb │ │ ├── mime_types.rb │ │ └── wrap_parameters.rb │ ├── locales │ │ └── en.yml │ ├── puma.rb │ ├── routes.rb │ ├── secrets.yml │ └── spring.rb ├── db │ ├── schema.rb │ └── seeds.rb ├── lib │ └── assets │ │ └── .keep ├── log │ └── .keep ├── package.json └── public │ ├── 404.html │ ├── 422.html │ ├── 500.html │ ├── apple-touch-icon-precomposed.png │ ├── apple-touch-icon.png │ └── favicon.ico └── vendor ├── es5-shim.min.js └── jquery-2.2.4.min.js /.codeclimate.yml: -------------------------------------------------------------------------------- 1 | --- 2 | engines: 3 | duplication: 4 | enabled: true 5 | config: 6 | languages: 7 | - ruby 8 | - javascript 9 | - python 10 | - php 11 | fixme: 12 | enabled: true 13 | rubocop: 14 | enabled: true 15 | ratings: 16 | paths: 17 | - "**.inc" 18 | - "**.js" 19 | - "**.jsx" 20 | - "**.module" 21 | - "**.php" 22 | - "**.py" 23 | - "**.rb" 24 | exclude_paths: 25 | - example/**/* 26 | - lib/sources/**/* 27 | - spec/ 28 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | 5 | # Runtime data 6 | pids 7 | *.pid 8 | *.seed 9 | 10 | # Directory for instrumented libs generated by jscoverage/JSCover 11 | lib-cov 12 | 13 | # Coverage directory used by tools like istanbul 14 | coverage 15 | 16 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 17 | .grunt 18 | 19 | # node-waf configuration 20 | .lock-wscript 21 | 22 | # Compiled binary addons (http://nodejs.org/api/addons.html) 23 | build/Release 24 | 25 | # Dependency directory 26 | # https://www.npmjs.org/doc/misc/npm-faq.html#should-i-check-my-node_modules-folder-into-git- 27 | node_modules 28 | 29 | # Ignore bundler config. 30 | .bundle 31 | 32 | spec/test_app/tmp 33 | spec/test_app/db 34 | 35 | /gemfiles/*.lock 36 | /tmp 37 | 38 | # ignore gem 39 | *.gem 40 | 41 | # ignore IDE files 42 | .idea 43 | .vscode 44 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | dist: trusty 2 | language: ruby 3 | cache: bundler 4 | rvm: 5 | - 2.4.4 6 | - 2.5.1 7 | - ruby-head 8 | env: 9 | - DRIVER=google-chrome TZ=Europe/Berlin 10 | matrix: 11 | fast_finish: true 12 | allow_failures: 13 | - rvm: ruby-head 14 | before_install: 15 | - if [[ "$DRIVER" == "google-chrome" ]]; then wget -q -O - https://dl-ssl.google.com/linux/linux_signing_key.pub | sudo apt-key add -; fi 16 | - if [[ "$DRIVER" == "google-chrome" ]]; then echo "deb http://dl.google.com/linux/chrome/deb/ stable main" | sudo tee /etc/apt/sources.list.d/google-chrome.list; fi 17 | - if [[ "$DRIVER" == "google-chrome" ]]; then sudo apt-get update -qq && sudo apt-get install -qq -y google-chrome-stable; fi 18 | - gem install bundler 19 | before_script: 20 | - cd spec/test_app 21 | - bundle install --jobs=3 --retry=3 22 | - bundle exec rails db:setup 23 | - cd ../../ 24 | - if [[ "$DRIVER" == "google-chrome" ]]; then bundle exec chromedriver-update; fi 25 | - if [[ "$DRIVER" == "google-chrome" ]]; then ls -lR ~/.chromedriver-helper/; fi 26 | - if [[ "$DRIVER" == "google-chrome" ]]; then bundle exec chromedriver --version; fi 27 | - if [[ "$DRIVER" == "google-chrome" ]]; then google-chrome --version; fi 28 | - if [[ "$DRIVER" == "google-chrome" ]]; then which google-chrome; fi 29 | script: bundle exec rspec 30 | -------------------------------------------------------------------------------- /Appraisals: -------------------------------------------------------------------------------- 1 | opal_versions = ['0.8', '0.9', '0.10'] 2 | react_versions_map = { 3 | '13' => '~> 1.3.3', 4 | '14' => '~> 1.6.2', 5 | '15' => '~> 1.10.0' 6 | } 7 | opal_rails_versions_map = { 8 | '0.8' => '~> 0.8.1', 9 | '0.9' => '~> 0.9.0', 10 | '0.10' => '~> 0.9.0', 11 | } 12 | 13 | opal_versions.each do |opal_v| 14 | react_versions_map.each do |react_v, react_rails_v| 15 | appraise "opal-#{opal_v}-react-#{react_v}" do 16 | ruby ">= 1.9.3" 17 | gem 'opal', "~> #{opal_v}.0" 18 | gem 'opal-rails', opal_rails_versions_map[opal_v] 19 | gem 'react-rails', react_rails_v, require: false 20 | end 21 | end 22 | end 23 | 24 | 25 | appraise "opal-master-react-15" do 26 | ruby '>= 2.0.0' 27 | gem 'opal', git: 'https://github.com/opal/opal.git' 28 | gem "opal-sprockets", git: 'https://github.com/opal/opal-sprockets.git' 29 | gem 'opal-rails', '~> 0.9.4' 30 | gem 'react-rails', '~> 2.4.0', require: false 31 | end 32 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change Log 2 | 3 | All notable changes to this project will be documented in this file starting with v0.8.6. 4 | This project *tries* to adhere to [Semantic Versioning](http://semver.org/), even before v1.0. 5 | 6 | Changes are grouped as follows: 7 | - **Added** for new features. 8 | - **Changed** for changes in existing functionality. 9 | - **Deprecated** for once-stable features to be removed in upcoming releases. 10 | - **Removed** for deprecated features removed in this release. 11 | - **Fixed** for any bug fixes. 12 | - **Security** to invite users to upgrade in case of vulnerabilities. 13 | 14 | 20 | 21 | ## [0.12.0] - Unreleased 22 | 23 | ### Added 24 | 25 | - `React::Server` is provided as a module wrapping the original `ReactDOMServer` API, require `react/server` to use it. (#186) 26 | - `React::Config` is introduced, `environment` is the only config option provided for now. See [#204](https://github.com/ruby-hyperloop/hyper-react/issues/204) for usage details. 27 | 28 | ### Changed 29 | 30 | - State syntax is now consistent with Hyperloop::Store, old syntax is deprecated. (#209, #97) 31 | 32 | ### Deprecated 33 | 34 | - Current ref callback behavior is deprecated. Require `"react/ref_callback"` to get the updated behavior. (#188) 35 | - `React.render_to_string` & `React.render_to_static_markup` is deprecated, use `React::Server.render_to_string` & `React::Server.render_to_static_markup` instead. (#186) 36 | - `react/react-source` is deprecated, use `react/react-source-browser` or `react/react-source-server` instead. For most usecase, `react/react-source-browser` is sufficient. If you are using the built-in server side rendering feature, the actual `ReactDOMServer` is already provided by the `react-rails` gem. Therefore, unless you are building a custom server side rendering mechanism, it's not suggested to use `react/react-source-server` in browser code. (#186) 37 | 38 | ### Removed 39 | 40 | - `react-latest` & `react-v1x` is removed. Use `react/react-source-browser` or `react/react-source-server` instead. 41 | - Support for Ruby < 2.0 is removed. (#201) 42 | 43 | ### Fixed 44 | 45 | - [NativeLibrary] Passing native JS object as props will raise exception. (#195) 46 | - Returns better error message if result of rendering block is not suitable (#207) 47 | - Batch all state changes and execute *after* rendering cycle (#206, #178) (Code is now moved to Hyper::Store) 48 | You can revert to the old behavior by defining the `React::State::ALWAYS_UPDATE_STATE_AFTER_RENDER = false` 49 | - Memory Leak in render context fixed (#192) 50 | 51 | 52 | ## [0.11.0] - 2016-12-13 53 | 54 | ### Changed 55 | 56 | - The whole opal-activesuppport is not loaded by default now. This gave us about 18% size reduction on the built file. If your code rely on any of the module which is not required by hyper-react, you need to require it yourself. (#135) 57 | 58 | ### Deprecated 59 | 60 | - Current `React.render` behavior is deprecated. Require `"react/top_level_render"` to get the updated behavior. (#187) 61 | - `React.is_valid_element` is deprecated in favor of `React.is_valid_element?`. 62 | - `expect(component).to render('
')` is now deprecated in favor of `expect(component).to render_static_html('
')`, which is much clearer. 63 | 64 | ### Fixed 65 | 66 | - `ReferenceError: window is not defined` error in prerender context with react-rails v1.10.0. (#196) 67 | - State might not be updated using `React::Observable` from a param. (#175) 68 | - Arity checking failed for `_react_param_conversion` & `React::Element#initialize` (#167) 69 | 70 | 71 | ## [0.10.0] - 2016-10-30 72 | 73 | ### Changed 74 | 75 | - This gem is now renamed to `hyper-react`, see [UPGRADING](UPGRADING.md) for details. 76 | 77 | ### Fixed 78 | 79 | - ReactJS functional stateless component could not be imported from `NativeLibrary`. Note that functional component is only supported in React v14+. (#162) 80 | - Prerender log got accumulated between reqeusts. (#176) 81 | 82 | ## [0.9.0] - 2016-10-19 83 | 84 | ### Added 85 | 86 | - `react/react-source` is the suggested way to include ReactJS sources now. Simply require `react/react-source` immediately before the `require "reactrb"` in your Opal code will make it work. 87 | 88 | ### Deprecated 89 | 90 | - `react-latest` & `react-v1x` is deprecated. Use `react/react-source` instead. 91 | 92 | ### Removed 93 | 94 | - `opal-browser` is removed from runtime dependency. (#133) You will have to add `gem 'opal-browser'` to your gemfile (recommended) or remove all references to opal-browser from your manifest files. 95 | 96 | ### Fixed 97 | 98 | - `$window#on` in `opal-jquery` is broken. (#166) 99 | - `Element#render` trigger unnecessary re-mounts when called multiple times. (#170) 100 | - Gets rid of react warnings about updating state during render (#155) 101 | - Multiple HAML classes (i.e. div.foo.bar) was not working (regression introduced in 0.8.8) 102 | - Don't send nil (null) to form components as the value string (#157) 103 | - Process `params` (props) correctly when using `Element#on` or `Element#render` (#158) 104 | - Deprecate shallow param compare (#156) 105 | 106 | 107 | ## [0.8.8] - 2016-07-13 108 | 109 | ### Added 110 | 111 | - More helpful error messages on render failures (#152) 112 | - `Element#on('')` subscribes to `my_event_name` (#153) 113 | 114 | ### Changed 115 | 116 | - `Element#on(:event)` subscribes to `on_event` for reactrb components and `onEvent` for native components. (#153) 117 | 118 | ### Deprecated 119 | 120 | - `Element#on(:event)` subscription to `_onEvent` is deprecated. Once you have changed params named `_on...` to `on_...` you can `require 'reactrb/new-event-name-convention.rb'` to avoid spurious react warning messages. (#153) 121 | 122 | 123 | ### Fixed 124 | 125 | - The `Element['#container'].render...` method generates a spurious react error (#154) 126 | 127 | 128 | 129 | 130 | ## [0.8.7] - 2016-07-08 131 | 132 | 133 | ### Fixed 134 | 135 | - Opal 0.10.x compatibility 136 | 137 | 138 | ## [0.8.6] - 2016-06-30 139 | 140 | 141 | ### Fixed 142 | 143 | - Method missing within a component was being reported as `incorrect const name` (#151) 144 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | gem "opal-jquery", git: "https://github.com/opal/opal-jquery.git", branch: "master" 3 | gem 'hyperloop-config', git: 'https://github.com/ruby-hyperloop/hyperloop-config.git', branch: 'edge' 4 | gem 'puma', '~> 3.11.0' # As of adding, version 3.12.0 isn't working so we are locking 5 | gemspec 6 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2015 Yi-Cheng Chang (http://github.com/zetachang) 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in 11 | all copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | THE SOFTWARE. 20 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |
2 | 3 |

4 | 5 | 6 | 7 | 8 | 9 |

10 | 11 |

The Complete Isomorphic Ruby Framework

12 | 13 |
14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | [![Build Status](https://travis-ci.org/ruby-hyperloop/hyper-react.svg?branch=master)](https://travis-ci.org/ruby-hyperloop/hyper-react) 24 | [![Gem Version](https://badge.fury.io/rb/hyper-react.svg)](https://badge.fury.io/rb/hyper-react) 25 | 26 |

27 | Hyper-components 28 |

29 | 30 |
31 | 32 | ## Hyper-React GEM is part of Hyperloop GEMS family 33 | 34 | Hyper-react GEM comes with the Hyperloop GEM. 35 | 36 | But if you want to install it separately, please install the [Hyper-component GEM](https://github.com/ruby-hyperloop/hyper-component). 37 | 38 | ## Community 39 | 40 | #### Getting Help 41 | Please **do not post** usage questions to GitHub Issues. For these types of questions use our [Gitter chatroom](https://gitter.im/ruby-hyperloop/chat) or [StackOverflow](http://stackoverflow.com/questions/tagged/hyperloop). 42 | 43 | #### Submitting Bugs and Enhancements 44 | [GitHub Issues](https://github.com/ruby-hyperloop/hyperloop/issues) is for suggesting enhancements and reporting bugs. Before submiting a bug make sure you do the following: 45 | * Check out our [contributing guide](https://github.com/ruby-hyperloop/hyperloop/blob/master/CONTRIBUTING.md) for info on our release cycle. 46 | 47 | ## License 48 | 49 | Hyperloop is released under the [MIT License](http://www.opensource.org/licenses/MIT). 50 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | # require 'bundler' 2 | # Bundler.require 3 | # Bundler::GemHelper.install_tasks 4 | # 5 | # # Store the BUNDLE_GEMFILE env, since rake or rspec seems to clean it 6 | # # while invoking task. 7 | # ENV['REAL_BUNDLE_GEMFILE'] = ENV['BUNDLE_GEMFILE'] 8 | # 9 | # require 'rspec/core/rake_task' 10 | # require 'opal/rspec/rake_task' 11 | # 12 | # RSpec::Core::RakeTask.new('ruby:rspec') 13 | # 14 | # task :test do 15 | # Rake::Task['ruby:rspec'].invoke 16 | # end 17 | # 18 | # require 'generators/reactive_ruby/test_app/test_app_generator' 19 | # desc "Generates a dummy app for testing" 20 | # task :test_app do 21 | # ReactiveRuby::TestAppGenerator.start 22 | # puts "Setting up test app database..." 23 | # system("bundle exec rake db:drop db:create db:migrate > #{File::NULL}") 24 | # end 25 | # 26 | # task :test_prepare do 27 | # system("./dciy_prepare.sh") 28 | # end 29 | # 30 | # task default: [ :test ] 31 | 32 | require "bundler/gem_tasks" 33 | require "rspec/core/rake_task" 34 | 35 | RSpec::Core::RakeTask.new(:spec) 36 | 37 | namespace :spec do 38 | task :prepare do 39 | sh %{bundle update} 40 | sh %{cd spec/test_app; bundle update} 41 | end 42 | end 43 | 44 | task :default => :spec 45 | -------------------------------------------------------------------------------- /UPGRADING.md: -------------------------------------------------------------------------------- 1 | ## Upgrading to hyper-react from Reactrb 2 | 3 | Follow these steps to upgrade: 4 | 5 | 1. Replace `reactrb` with `hyper-react` both in **Gemfile** and any `require`s in your code. 6 | 2. To include the React.js source, the suggested way is to add `require 'react/react-source'` before `require 'hyper-react'`. This will use the copy of React.js source from `react-rails` gem. 7 | 8 | ## Upgrading to Reactrb 9 | 10 | The original gem `react.rb` was superceeded by `reactive-ruby`, which has had over 15,000 downloads. This name has now been superceeded by `reactrb` (see #144 for detailed discussion on why.) 11 | 12 | Going forward the name `reactrb` will be used consistently as the organization name, the gem name, the domain name, the twitter handle, etc. 13 | 14 | The first initial version of `reactrb` is 0.8.x. 15 | 16 | It is very unlikely that there will be any more releases of the `reactive-ruby` gem, so users should upgrade to `reactrb`. 17 | 18 | There are no syntactic or semantic breaking changes between `reactrb` v 0.8.x and 19 | previous versions, however the `reactrb` gem does *not* include the react-js source as previous versions did. This allows you to pick the react js source compatible with other gems and react js components you may be using. 20 | 21 | Follow these steps to upgrade: 22 | 23 | 1. Replace `reactive-ruby` with `reactrb` both in **Gemfile** and any `require`s in your code. 24 | 2. To include the React.js source, the suggested way is to add `require 'react/react-source'` before `require 'reactrb'`. This will use the copy of React.js source from `react-rails` gem. 25 | -------------------------------------------------------------------------------- /component-name-lookup.md: -------------------------------------------------------------------------------- 1 | #### Notes on how component names are looked up 2 | 3 | Given: 4 | 5 | ```ruby 6 | 7 | class Blat < React::Component::Base 8 | 9 | render do 10 | Bar() 11 | Foo::Bar() 12 | end 13 | 14 | end 15 | 16 | class Bar < React::Component::Base 17 | end 18 | 19 | module Foo 20 | 21 | class Bar < React::Component::Base 22 | 23 | render do 24 | Blat() 25 | Baz() 26 | end 27 | end 28 | 29 | class Baz < React::Component::Base 30 | end 31 | 32 | end 33 | ``` 34 | 35 | The problem is that method lookup is different than constant lookup. We can prove it by running this code: 36 | 37 | ```ruby 38 | def try_it(test, &block) 39 | puts "trying #{test}" 40 | result = yield 41 | puts "success#{': '+result.to_s if result}" 42 | rescue Exception => e 43 | puts "failed: #{e}" 44 | ensure 45 | puts "---------------------------------" 46 | end 47 | 48 | module Boom 49 | 50 | Bar = 12 51 | 52 | def self.Bar 53 | puts " Boom::Bar says hi" 54 | end 55 | 56 | class Baz 57 | def doit 58 | try_it("Bar()") { Bar() } 59 | try_it("Boom::Bar()") {Boom::Bar()} 60 | try_it("Bar") { Bar } 61 | try_it("Boom::Bar") { Boom::Bar } 62 | end 63 | end 64 | end 65 | 66 | 67 | 68 | Boom::Baz.new.doit 69 | ``` 70 | 71 | which prints: 72 | 73 | ```text 74 | trying Bar() 75 | failed: Bar: undefined method `Bar' for # 76 | --------------------------------- 77 | trying Boom::Bar() 78 | Boom::Bar says hi 79 | success 80 | --------------------------------- 81 | trying Bar 82 | success: 12 83 | --------------------------------- 84 | trying Boom::Bar 85 | success: 12 86 | --------------------------------- 87 | ``` 88 | 89 | [try-it](http://opalrb.org/try/?code:def%20try_it(test%2C%20%26block)%0A%20%20puts%20%22trying%20%23%7Btest%7D%22%0A%20%20result%20%3D%20yield%0A%20%20puts%20%22success%23%7B%27%3A%20%27%2Bresult.to_s%20if%20result%7D%22%0Arescue%20Exception%20%3D%3E%20e%0A%20%20puts%20%22failed%3A%20%23%7Be%7D%22%0Aensure%0A%20%20puts%20%22---------------------------------%22%0Aend%0A%0Amodule%20Boom%0A%20%20%0A%20%20Bar%20%3D%2012%0A%20%20%0A%20%20def%20self.Bar%0A%20%20%20%20puts%20%22%20%20%20Boom%3A%3ABar%20says%20hi%22%0A%20%20end%0A%0A%20%20class%20Baz%0A%20%20%20%20def%20doit%0A%20%20%20%20%20%20try_it(%22Bar()%22)%20%7B%20Bar()%20%7D%0A%20%20%20%20%20%20try_it(%22Boom%3A%3ABar()%22)%20%7BBoom%3A%3ABar()%7D%0A%20%20%20%20%20%20try_it(%22Bar%22)%20%7B%20Bar%20%7D%0A%20%20%20%20%20%20try_it(%22Boom%3A%3ABar%22)%20%7B%20Boom%3A%3ABar%20%7D%0A%20%20%20%20end%0A%20%20end%0Aend%0A%20%20%0A%0A%0ABoom%3A%3ABaz.new.doit) 90 | 91 | 92 | What we need to do is: 93 | 94 | 1. when defining a component class `Foo`, also define in the same scope that Foo is being defined a method `self.Foo` that will accept Foo's params and child block, and render it. 95 | 96 | 2. As long as a name is qualified with at least one scope (i.e. `ModName::Foo()`) everything will work out, but if we say just `Foo()` then the only way I believe out of this is to handle it via method_missing, and let method_missing do a const_get on the method_name (which will return the class) and then render that component. 97 | 98 | #### details 99 | 100 | To define `self.Foo` in the same scope level as the class `Foo`, we need code like this: 101 | 102 | ```ruby 103 | def register_component_dsl_method(component) 104 | split_name = component.name && component.name.split('::') 105 | return unless split_name && split_name.length > 2 106 | component_name = split_name.last 107 | parent = split_name.inject([Module]) { |nesting, next_const| nesting + [nesting.last.const_get(next_const)] }[-2] 108 | class << parent 109 | define_method component_name do |*args, &block| 110 | React::RenderingContext.render(name, *args, &block) 111 | end 112 | define_method "#{component_name}_as_node" do |*args, &block| 113 | React::Component.deprecation_warning("..._as_node is deprecated. Render component and then use the .node method instead") 114 | send(component_name, *args, &block).node 115 | end 116 | end 117 | end 118 | 119 | module React 120 | module Component 121 | def self.included(base) 122 | ... 123 | register_component_dsl_method(base.name) 124 | end 125 | end 126 | end 127 | ``` 128 | 129 | The component's method_missing function will look like this: 130 | 131 | ```ruby 132 | def method_missing(name, *args, &block) 133 | if name =~ /_as_node$/ 134 | React::Component.deprecation_warning("..._as_node is deprecated. Render component and then use the .node method instead") 135 | method_missing(name.gsub(/_as_node$/,""), *args, &block).node 136 | else 137 | component = const_get name if defined? name 138 | React::RenderingContext.render(nil, component, *args, &block) 139 | end 140 | end 141 | ``` 142 | 143 | ### other related issues 144 | 145 | The Kernel#p method conflicts with the

tag. However the p method can be invoked on any object so we are going to go ahead and use it, and deprecate the para method. 146 | -------------------------------------------------------------------------------- /dciy.toml: -------------------------------------------------------------------------------- 1 | [dciy.commands] 2 | prepare = ["rake spec:prepare"] 3 | cibuild = ["bundle exec rake"] 4 | -------------------------------------------------------------------------------- /hyper-react.gemspec: -------------------------------------------------------------------------------- 1 | # -*- encoding: utf-8 -*- 2 | $:.push File.expand_path('../lib/', __FILE__) 3 | require 'reactive-ruby/version' 4 | 5 | Gem::Specification.new do |spec| 6 | spec.name = 'hyper-react' 7 | spec.version = React::VERSION 8 | 9 | spec.authors = ['David Chang', 'Adam Jahn', 'Mitch VanDuyn', 'Jan Biedermann'] 10 | spec.email = ['mitch@catprint.com', 'jan@kursator.com'] 11 | spec.homepage = 'http://ruby-hyperloop.org' 12 | spec.summary = 'Opal Ruby wrapper of React.js library.' 13 | spec.license = 'MIT' 14 | spec.description = 'Write React UI components in pure Ruby.' 15 | # spec.metadata = { 16 | # "homepage_uri" => 'http://ruby-hyperloop.org', 17 | # "source_code_uri" => 'https://github.com/ruby-hyperloop/hyper-component' 18 | # } 19 | 20 | spec.files = `git ls-files`.split("\n").reject { |f| f.match(%r{^(gemfiles|spec)/}) } 21 | spec.test_files = `git ls-files -- {test,spec,features}/*`.split("\n") 22 | spec.require_paths = ['lib'] 23 | 24 | spec.add_dependency 'hyper-store', React::VERSION 25 | spec.add_dependency 'opal', '>= 0.11.0', '< 0.12.0' 26 | spec.add_dependency 'opal-activesupport', '~> 0.3.1' 27 | spec.add_dependency 'hyperloop-config', React::VERSION 28 | spec.add_dependency 'mini_racer', '~> 0.1.15' 29 | # https://github.com/discourse/mini_racer/issues/92 30 | spec.add_dependency 'libv8', '~> 6.3.0' 31 | spec.add_dependency 'react-rails', '>= 2.4.0', '< 2.5.0' 32 | 33 | spec.add_development_dependency 'bundler', '~> 1.16.0' 34 | spec.add_development_dependency 'chromedriver-helper' 35 | spec.add_development_dependency 'hyper-spec', React::VERSION 36 | spec.add_development_dependency 'jquery-rails' 37 | spec.add_development_dependency 'listen' 38 | spec.add_development_dependency 'mime-types' 39 | spec.add_development_dependency 'nokogiri' 40 | spec.add_development_dependency 'opal-jquery' 41 | spec.add_development_dependency 'opal-rails', '~> 0.9.4' 42 | spec.add_development_dependency 'opal-rspec' 43 | spec.add_development_dependency 'puma' 44 | spec.add_development_dependency 'pry' 45 | spec.add_development_dependency 'rails', '>= 4.0.0' 46 | spec.add_development_dependency 'rails-controller-testing' 47 | spec.add_development_dependency 'rake' 48 | spec.add_development_dependency 'rspec-rails' 49 | spec.add_development_dependency 'rubocop', '~> 0.51.0' 50 | spec.add_development_dependency 'sqlite3' 51 | spec.add_development_dependency 'timecop', '~> 0.8.1' 52 | end 53 | -------------------------------------------------------------------------------- /lib/generators/reactive_ruby/test_app/templates/assets/javascripts/components.rb: -------------------------------------------------------------------------------- 1 | require 'opal' 2 | require 'hyper-react' 3 | require_tree './components' 4 | -------------------------------------------------------------------------------- /lib/generators/reactive_ruby/test_app/templates/assets/javascripts/server_rendering.js: -------------------------------------------------------------------------------- 1 | //= require 'react-server' 2 | //= require 'react_ujs' 3 | //= require 'components' 4 | 5 | Opal.load('components') -------------------------------------------------------------------------------- /lib/generators/reactive_ruby/test_app/templates/assets/javascripts/test_application.rb: -------------------------------------------------------------------------------- 1 | require 'components' 2 | require 'react_ujs' 3 | -------------------------------------------------------------------------------- /lib/generators/reactive_ruby/test_app/templates/boot.rb.erb: -------------------------------------------------------------------------------- 1 | require 'rubygems' 2 | gemfile = File.expand_path("<%= gemfile_path %>", __FILE__) 3 | 4 | ENV['BUNDLE_GEMFILE'] = gemfile 5 | require 'bundler' 6 | Bundler.setup 7 | -------------------------------------------------------------------------------- /lib/generators/reactive_ruby/test_app/templates/script/rails: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | APP_PATH = File.expand_path('../../config/application', __FILE__) 4 | require File.expand_path('../../config/boot', __FILE__) 5 | require 'rails/commands' 6 | -------------------------------------------------------------------------------- /lib/generators/reactive_ruby/test_app/templates/test_application.rb.erb: -------------------------------------------------------------------------------- 1 | <% if defined? application_definition %> 2 | require 'rails/all' 3 | require File.expand_path('../boot', __FILE__) 4 | 5 | # Require the gems listed in Gemfile, including any gems 6 | # you've limited to :test, :development, or :production. 7 | Bundler.require(*Rails.groups(assets: %w(development test))) 8 | 9 | require 'opal-rails' 10 | require 'hyper-react' 11 | 12 | <%= application_definition %> 13 | <% end %> 14 | -------------------------------------------------------------------------------- /lib/generators/reactive_ruby/test_app/templates/views/components/hello_world.rb: -------------------------------------------------------------------------------- 1 | module Components 2 | class HelloWorld 3 | include React::Component 4 | 5 | def render 6 | div do 7 | "Hello, World!".span 8 | end 9 | end 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /lib/generators/reactive_ruby/test_app/templates/views/components/todo.rb: -------------------------------------------------------------------------------- 1 | module Components 2 | class Todo 3 | include React::Component 4 | export_component 5 | 6 | params do 7 | requires :todo 8 | end 9 | 10 | def render 11 | li { "#{params[:todo]}" } 12 | end 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /lib/generators/reactive_ruby/test_app/templates/views/layouts/test_layout.html.erb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ruby-hyperloop/hyper-react/579326fd76ee09c38742b4c26160ae2106581671/lib/generators/reactive_ruby/test_app/templates/views/layouts/test_layout.html.erb -------------------------------------------------------------------------------- /lib/generators/reactive_ruby/test_app/test_app_generator.rb: -------------------------------------------------------------------------------- 1 | require 'rails/generators/rails/app/app_generator' 2 | 3 | module ReactiveRuby 4 | class TestAppGenerator < ::Rails::Generators::Base 5 | def self.source_paths 6 | paths = self.superclass.source_paths 7 | paths << File.expand_path('../templates', __FILE__) 8 | paths.flatten 9 | end 10 | 11 | def remove_existing_app 12 | remove_dir(test_app_path) if File.directory?(test_app_path) 13 | end 14 | 15 | def generate_test_app 16 | opts = options.dup 17 | opts[:database] = 'sqlite3' if opts[:database].blank? 18 | opts[:force] = true 19 | opts[:skip_bundle] = true 20 | 21 | puts "Generating Test Rails Application..." 22 | invoke ::Rails::Generators::AppGenerator, 23 | [ File.expand_path(test_app_path, destination_root) ], opts 24 | end 25 | 26 | def configure_test_app 27 | template 'boot.rb.erb', "#{test_app_path}/config/boot.rb", force: true 28 | template 'test_application.rb.erb', "#{test_app_path}/config/application.rb", force: true 29 | template 'assets/javascripts/test_application.rb', 30 | "#{test_app_path}/app/assets/javascripts/application.rb", force: true 31 | template 'assets/javascripts/server_rendering.js', 32 | "#{test_app_path}/app/assets/javascripts/server_rendering.js", force: true 33 | template 'assets/javascripts/components.rb', 34 | "#{test_app_path}/app/views/components.rb", force: true 35 | template 'views/components/hello_world.rb', 36 | "#{test_app_path}/app/views/components/hello_world.rb", force: true 37 | template 'views/components/todo.rb', 38 | "#{test_app_path}/app/views/components/todo.rb", force: true 39 | template 'views/layouts/test_layout.html.erb', 40 | "#{test_app_path}/app/views/layouts/test_layout.html.erb", force: true 41 | template 'views/layouts/test_layout.html.erb', 42 | "#{test_app_path}/app/views/layouts/explicit_layout.html.erb", force: true 43 | end 44 | 45 | def clean_superfluous_files 46 | inside test_app_path do 47 | remove_file '.gitignore' 48 | remove_file 'doc' 49 | remove_file 'Gemfile' 50 | remove_file 'lib/tasks' 51 | remove_file 'app/assets/images/rails.png' 52 | remove_file 'app/assets/javascripts/application.js' 53 | remove_file 'public/index.html' 54 | remove_file 'public/robots.txt' 55 | remove_file 'README.rdoc' 56 | remove_file 'test' 57 | remove_file 'vendor' 58 | remove_file 'spec' 59 | end 60 | end 61 | 62 | def configure_opal_rspec 63 | inject_into_file "#{test_app_path}/config/application.rb", 64 | after: /class Application < Rails::Application/, verbose: true do 65 | %Q[ 66 | config.opal.method_missing = true 67 | config.opal.optimized_operators = true 68 | config.opal.arity_check = false 69 | config.opal.const_missing = true 70 | config.opal.dynamic_require_severity = :ignore 71 | config.opal.enable_specs = true 72 | config.opal.spec_location = 'spec-opal' 73 | config.hyperloop.auto_config = false 74 | 75 | config.react.server_renderer_options = { 76 | files: ["server_rendering.js"] 77 | } 78 | config.react.server_renderer_directories = ["/app/assets/javascripts"] 79 | ] 80 | end 81 | end 82 | 83 | protected 84 | 85 | def application_definition 86 | @application_definition ||= begin 87 | test_application_contents 88 | end 89 | end 90 | alias :store_application_definition! :application_definition 91 | 92 | private 93 | 94 | def test_app_path 95 | 'spec/test_app' 96 | end 97 | 98 | def test_application_path 99 | File.expand_path("#{test_app_path}/config/application.rb", 100 | destination_root) 101 | end 102 | 103 | def test_application_contents 104 | return unless File.exists?(test_application_path) && !options[:pretend] 105 | contents = File.read(test_application_path) 106 | contents[(contents.index("module #{module_name}"))..-1] 107 | end 108 | 109 | def module_name 110 | 'TestApp' 111 | end 112 | 113 | def gemfile_path 114 | '../../../../Gemfile' 115 | end 116 | end 117 | end 118 | -------------------------------------------------------------------------------- /lib/hyper-react.rb: -------------------------------------------------------------------------------- 1 | require 'hyperloop-config' 2 | Hyperloop.import 'hyper-store' 3 | Hyperloop.js_import 'react/react-source-browser', client_only: true, defines: ['ReactDOM', 'React'] 4 | Hyperloop.js_import 'react/react-source-server', server_only: true, defines: 'React' 5 | Hyperloop.import 'browser/delay', client_only: true 6 | Hyperloop.import 'hyper-react' 7 | Hyperloop.js_import 'react_ujs', defines: 'ReactRailsUJS' 8 | 9 | if RUBY_ENGINE == 'opal' 10 | module Hyperloop 11 | class Component 12 | end 13 | end 14 | require 'native' 15 | require 'react/observable' 16 | require 'react/validator' 17 | require 'react/element' 18 | require 'react/api' 19 | require 'react/component' 20 | require 'react/component/dsl_instance_methods' 21 | require 'react/component/should_component_update' 22 | require 'react/component/tags' 23 | require 'react/component/base' 24 | require 'react/event' 25 | require 'react/rendering_context' 26 | require 'react/state' 27 | require 'react/object' 28 | require 'react/to_key' 29 | require 'react/ext/opal-jquery/element' 30 | require 'reactive-ruby/isomorphic_helpers' 31 | require 'react/top_level' 32 | require 'react/top_level_render' 33 | require 'rails-helpers/top_level_rails_component' 34 | require 'reactive-ruby/version' 35 | module Hyperloop 36 | class Component 37 | def self.inherited(child) 38 | child.include(Mixin) 39 | end 40 | end 41 | end 42 | React::Component.deprecation_warning( 43 | 'component.rb', 44 | "Requiring 'hyper-react' is deprecated. Use gem 'hyper-component', and require 'hyper-component' instead." 45 | ) unless defined? Hyperloop::Component::VERSION 46 | else 47 | require 'opal' 48 | 49 | require 'hyper-store' 50 | require 'opal-activesupport' 51 | require 'reactive-ruby/version' 52 | require 'reactive-ruby/rails' if defined?(Rails) 53 | require 'reactive-ruby/isomorphic_helpers' 54 | require 'reactive-ruby/serializers' 55 | 56 | Opal.append_path File.expand_path('../', __FILE__).untaint 57 | require 'react/react-source' 58 | end 59 | -------------------------------------------------------------------------------- /lib/rails-helpers/top_level_rails_component.rb: -------------------------------------------------------------------------------- 1 | module React 2 | class TopLevelRailsComponent 3 | include Hyperloop::Component::Mixin 4 | 5 | def self.search_path 6 | @search_path ||= [Object] 7 | end 8 | 9 | export_component 10 | 11 | param :component_name 12 | param :controller 13 | param :render_params 14 | 15 | backtrace :off 16 | 17 | def render 18 | paths_searched = [] 19 | component = nil 20 | if params.component_name.start_with?('::') 21 | # if absolute path of component is given, look it up and fail if not found 22 | paths_searched << params.component_name 23 | component = begin 24 | Object.const_get(params.component_name) 25 | rescue NameError 26 | nil 27 | end 28 | else 29 | # if relative path is given, look it up like this 30 | # 1) we check each path + controller-name + component-name 31 | # 2) if we can't find it there we check each path + component-name 32 | # if we can't find it we just try const_get 33 | # so (assuming controller name is Home) 34 | # ::Foo::Bar will only resolve to some component named ::Foo::Bar 35 | # but Foo::Bar will check (in this order) ::Home::Foo::Bar, ::Components::Home::Foo::Bar, ::Foo::Bar, ::Components::Foo::Bar 36 | self.class.search_path.each do |scope| 37 | paths_searched << "#{scope.name}::#{params.controller}::#{params.component_name}" 38 | component = begin 39 | scope.const_get(params.controller, false).const_get(params.component_name, false) 40 | rescue NameError 41 | nil 42 | end 43 | break if component != nil 44 | end 45 | unless component 46 | self.class.search_path.each do |scope| 47 | paths_searched << "#{scope.name}::#{params.component_name}" 48 | component = begin 49 | scope.const_get(params.component_name, false) 50 | rescue NameError 51 | nil 52 | end 53 | break if component != nil 54 | end 55 | end 56 | end 57 | return React::RenderingContext.render(component, params.render_params) if component && component.method_defined?(:render) 58 | raise "Could not find component class '#{params.component_name}' for params.controller '#{params.controller}' in any component directory. Tried [#{paths_searched.join(", ")}]" 59 | end 60 | end 61 | end 62 | 63 | class Module 64 | def add_to_react_search_path(replace_search_path = nil) 65 | if replace_search_path 66 | React::TopLevelRailsComponent.search_path = [self] 67 | elsif !React::TopLevelRailsComponent.search_path.include? self 68 | React::TopLevelRailsComponent.search_path << self 69 | end 70 | end 71 | end 72 | 73 | module Components 74 | add_to_react_search_path 75 | end 76 | -------------------------------------------------------------------------------- /lib/react/callbacks.rb: -------------------------------------------------------------------------------- 1 | require 'hyperloop-config' 2 | 3 | module React 4 | module Callbacks 5 | def self.included(base) 6 | base.extend(ClassMethods) 7 | end 8 | 9 | def run_callback(name, *args) 10 | self.class.callbacks_for(name).each do |callback| 11 | if callback.is_a?(Proc) 12 | instance_exec(*args, &callback) 13 | else 14 | send(callback, *args) 15 | end 16 | end 17 | end 18 | 19 | module ClassMethods 20 | def define_callback(callback_name) 21 | wrapper_name = "_#{callback_name}_callbacks" 22 | define_singleton_method(wrapper_name) do 23 | Hyperloop::Context.set_var(self, "@#{wrapper_name}", force: true) { [] } 24 | end 25 | define_singleton_method(callback_name) do |*args, &block| 26 | send(wrapper_name).concat(args) 27 | send(wrapper_name).push(block) if block_given? 28 | end 29 | end 30 | 31 | def callbacks_for(callback_name) 32 | wrapper_name = "_#{callback_name}_callbacks" 33 | if superclass.respond_to? :callbacks_for 34 | superclass.callbacks_for(callback_name) 35 | else 36 | [] 37 | end + send(wrapper_name) 38 | end 39 | end 40 | end 41 | end 42 | -------------------------------------------------------------------------------- /lib/react/children.rb: -------------------------------------------------------------------------------- 1 | module React 2 | class Children 3 | include Enumerable 4 | 5 | def initialize(children) 6 | @children = children 7 | end 8 | 9 | def render 10 | each(&:render) 11 | end 12 | 13 | def to_proc 14 | -> () { render } 15 | end 16 | 17 | def each(&block) 18 | return to_enum(__callee__) { length } unless block_given? 19 | return [] unless length > 0 20 | collection = [] 21 | %x{ 22 | React.Children.forEach(#{@children}, function(context){ 23 | #{ 24 | element = React::Element.new(`context`) 25 | block.call(element) 26 | collection << element 27 | } 28 | }) 29 | } 30 | collection 31 | end 32 | 33 | def length 34 | @length ||= `React.Children.count(#{@children})` 35 | end 36 | alias_method :size, :length 37 | end 38 | end 39 | -------------------------------------------------------------------------------- /lib/react/component/api.rb: -------------------------------------------------------------------------------- 1 | module React 2 | module Component 3 | module API 4 | def dom_node 5 | `ReactDOM.findDOMNode(#{self}.native)` # react >= v0.15.0 6 | end 7 | 8 | def mounted? 9 | `(#{self}.is_mounted === undefined) ? false : #{self}.is_mounted` 10 | end 11 | 12 | def force_update! 13 | `#{self}.native.forceUpdate()` 14 | self 15 | end 16 | 17 | def set_props(prop, &block) 18 | raise "set_props: setProps() is no longer supported by react" 19 | end 20 | alias :set_props! :set_props 21 | 22 | def set_state(state, &block) 23 | set_or_replace_state_or_prop(state, 'setState', &block) 24 | end 25 | 26 | def set_state!(state, &block) 27 | set_or_replace_state_or_prop(state, 'setState', &block) 28 | `#{self}.native.forceUpdate()` 29 | end 30 | 31 | private 32 | 33 | def set_or_replace_state_or_prop(state_or_prop, method, &block) 34 | raise "No native ReactComponent associated" unless @native 35 | `var state_prop_n = #{state_or_prop.shallow_to_n}` 36 | # the state object is initalized when the ruby component is instantiated 37 | # this is detected by self.native.__opalInstanceInitializedState 38 | # which is set in the native component constructor in react/api.rb 39 | # the setState update callback is not called when initalizing initial state 40 | if block 41 | %x{ 42 | if (#{@native}.__opalInstanceInitializedState === true) { 43 | #{@native}[method](state_prop_n, function(){ 44 | block.$call(); 45 | }); 46 | } else { 47 | for (var sp in state_prop_n) { 48 | if (state_prop_n.hasOwnProperty(sp)) { 49 | #{@native}.state[sp] = state_prop_n[sp]; 50 | } 51 | } 52 | } 53 | } 54 | else 55 | %x{ 56 | if (#{@native}.__opalInstanceInitializedState === true) { 57 | #{@native}[method](state_prop_n); 58 | } else { 59 | for (var sp in state_prop_n) { 60 | if (state_prop_n.hasOwnProperty(sp)) { 61 | #{@native}.state[sp] = state_prop_n[sp]; 62 | } 63 | } 64 | } 65 | } 66 | end 67 | end 68 | end 69 | end 70 | end 71 | -------------------------------------------------------------------------------- /lib/react/component/base.rb: -------------------------------------------------------------------------------- 1 | module React 2 | module Component 3 | class Base 4 | def self.inherited(child) 5 | # note this is turned off during old style testing: See the spec_helper 6 | unless child.to_s == "React::Component::HyperTestDummy" 7 | React::Component.deprecation_warning child, "The class name React::Component::Base has been deprecated. Use Hyperloop::Component instead." 8 | end 9 | child.include(ComponentNoNotice) 10 | end 11 | end 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /lib/react/component/dsl_instance_methods.rb: -------------------------------------------------------------------------------- 1 | require "react/children" 2 | 3 | module React 4 | module Component 5 | module DslInstanceMethods 6 | def children 7 | Children.new(`#{@native}.props.children`) 8 | end 9 | 10 | def params 11 | @params ||= self.class.props_wrapper.new(self) 12 | end 13 | 14 | def props 15 | Hash.new(`#{@native}.props`) 16 | end 17 | 18 | def refs 19 | Hash.new(`#{@native}.refs`) 20 | end 21 | end 22 | end 23 | end 24 | -------------------------------------------------------------------------------- /lib/react/component/params.rb: -------------------------------------------------------------------------------- 1 | module React 2 | module Component 3 | module Params 4 | end 5 | end 6 | end 7 | -------------------------------------------------------------------------------- /lib/react/component/props_wrapper.rb: -------------------------------------------------------------------------------- 1 | module React 2 | module Component 3 | 4 | class PropsWrapper 5 | attr_reader :component 6 | 7 | def self.define_param(name, param_type) 8 | if param_type == Observable 9 | define_method("#{name}") do 10 | value_for(name) 11 | end 12 | define_method("#{name}!") do |*args| 13 | current_value = value_for(name) 14 | if args.count > 0 15 | props[name].call args[0] 16 | current_value 17 | else 18 | # rescue in case we in middle of render... What happens during a 19 | # render that causes exception? 20 | # Where does `dont_update_state` come from? 21 | props[name].call current_value unless @dont_update_state rescue nil 22 | props[name] 23 | end 24 | end 25 | elsif param_type == Proc 26 | define_method("#{name}") do |*args, &block| 27 | props[name].call(*args, &block) if props[name] 28 | end 29 | else 30 | define_method("#{name}") do 31 | fetch_from_cache(name) do 32 | if param_type.respond_to? :_react_param_conversion 33 | param_type._react_param_conversion props[name], nil 34 | elsif param_type.is_a?(Array) && 35 | param_type[0].respond_to?(:_react_param_conversion) 36 | props[name].collect do |param| 37 | param_type[0]._react_param_conversion param, nil 38 | end 39 | else 40 | props[name] 41 | end 42 | end 43 | end 44 | end 45 | end 46 | 47 | def self.define_all_others(name) 48 | define_method("#{name}") do 49 | @_all_others_cache ||= yield(props) 50 | end 51 | end 52 | 53 | 54 | def initialize(component) 55 | @component = component 56 | end 57 | 58 | def [](prop) 59 | props[prop] 60 | end 61 | 62 | 63 | def _reset_all_others_cache 64 | @_all_others_cache = nil 65 | end 66 | 67 | private 68 | 69 | def fetch_from_cache(name) 70 | last, value = cache[name] 71 | return value if last.equal?(props[name]) 72 | yield.tap do |value| 73 | cache[name] = [props[name], value] 74 | end 75 | end 76 | 77 | def cache 78 | @cache ||= Hash.new { |h, k| h[k] = [] } 79 | end 80 | 81 | def props 82 | component.props 83 | end 84 | 85 | def value_for(name) 86 | self[name].instance_variable_get("@value") if self[name] 87 | end 88 | end 89 | end 90 | end 91 | -------------------------------------------------------------------------------- /lib/react/component/should_component_update.rb: -------------------------------------------------------------------------------- 1 | module React 2 | module Component 3 | # 4 | # React assumes all components should update, unless a component explicitly overrides 5 | # the shouldComponentUpdate method. Reactrb does an explicit check doing a shallow 6 | # compare of params, and using a timestamp to determine if state has changed. 7 | 8 | # If needed components can provide their own #needs_update? method which will be 9 | # passed the next params and state opal hashes. 10 | 11 | # Attached to these hashes is a #changed? method that returns whether the hash contains 12 | # changes as calculated by the base mechanism. This way implementations of #needs_update? 13 | # can use the base comparison mechanism as needed. 14 | 15 | # For example 16 | # def needs_update?(next_params, next_state) 17 | # # use a special comparison method 18 | # return false if next_state.changed? || next_params.changed? 19 | # # do some other special checks 20 | # end 21 | 22 | # Note that beginning in 0.9 we will use standard ruby compare on all params further reducing 23 | # the need for needs_update? 24 | # 25 | module ShouldComponentUpdate 26 | def should_component_update?(next_props, next_state) 27 | State.set_state_context_to(self, false) do 28 | # rubocop:disable Style/DoubleNegation # we must return true/false to js land 29 | if respond_to?(:needs_update?) 30 | !!call_needs_update(next_props, next_state) 31 | else 32 | (props_changed?(next_props) || native_state_changed?(next_state)) 33 | end 34 | # rubocop:enable Style/DoubleNegation 35 | end 36 | end 37 | 38 | # create opal hashes for next params and state, and attach 39 | # the changed? method to each hash 40 | 41 | def call_needs_update(next_params, next_state) 42 | component = self 43 | next_params.define_singleton_method(:changed?) do 44 | component.props_changed?(self) 45 | end 46 | next_state.define_singleton_method(:changed?) do 47 | component.native_state_changed?(next_state) 48 | end 49 | needs_update?(next_params, next_state) 50 | end 51 | 52 | # Whenever state changes, reactrb updates a timestamp on the state object. 53 | # We can rapidly check for state changes comparing the incoming state time_stamp 54 | # with the current time stamp. 55 | 56 | # we receive a Opal Ruby Hash here, always, so the Hash is either empty or filled 57 | # Hash is converted to native object 58 | # if the Hash was empty, the Object has no keys 59 | 60 | # Different versions of react treat empty state differently, so we first 61 | # convert anything that looks like an empty state to "false" for consistency. 62 | 63 | # Then we test if one state is empty and the other is not, then we return false. 64 | # Then we test if both states are empty we return true. 65 | # If either state does not have a time stamp then we have to assume a change. 66 | # Otherwise we check time stamps 67 | 68 | # rubocop:disable Metrics/MethodLength # for effeciency we want this to be one method 69 | def native_state_changed?(next_state_hash) 70 | # next_state = next_state_hash.to_n 71 | # %x{ 72 | # var current_state = #{@native}.state 73 | # var normalized_next_state = 74 | # !next_state || Object.keys(next_state).length === 0 ? false : next_state 75 | # var normalized_current_state = 76 | # !current_state || Object.keys(current_state).length === 0 ? false : current_state 77 | # if (!normalized_current_state != !normalized_next_state) return(true) 78 | # if (!normalized_current_state && !normalized_next_state) return(false) 79 | # if (!normalized_current_state['***_state_updated_at-***'] && 80 | # !normalized_next_state['***_state_updated_at-***']) return(false) 81 | # if (!normalized_current_state['***_state_updated_at-***'] || 82 | # !normalized_next_state['***_state_updated_at-***']) return(true) 83 | # return (normalized_current_state['***_state_updated_at-***'] != 84 | # normalized_next_state['***_state_updated_at-***']) 85 | # } 86 | state_hash = Hash.new(`#{@native}.state`) 87 | next_state_hash != state_hash 88 | end 89 | # rubocop:enable Metrics/MethodLength 90 | 91 | # Do a shallow compare on the two hashes. Starting in 0.9 we will do a deep compare. ??? 92 | 93 | def props_changed?(next_props) 94 | props = Hash.new(`#{@native}.props`) 95 | next_props != props 96 | end 97 | end 98 | end 99 | end 100 | -------------------------------------------------------------------------------- /lib/react/component/tags.rb: -------------------------------------------------------------------------------- 1 | module React 2 | module Component 3 | # contains the name of all HTML tags, and the mechanism to register a component 4 | # class as a new tag 5 | module Tags 6 | HTML_TAGS = %w(a abbr address area article aside audio b base bdi bdo big blockquote body br 7 | button canvas caption cite code col colgroup data datalist dd del details dfn 8 | dialog div dl dt em embed fieldset figcaption figure footer form h1 h2 h3 h4 h5 9 | h6 head header hr html i iframe img input ins kbd keygen label legend li link 10 | main map mark menu menuitem meta meter nav noscript object ol optgroup option 11 | output p param picture pre progress q rp rt ruby s samp script section select 12 | small source span strong style sub summary sup table tbody td textarea tfoot th 13 | thead time title tr track u ul var video wbr) + 14 | # The SVG Tags 15 | %w(circle clipPath defs ellipse g line linearGradient mask path pattern polygon polyline 16 | radialGradient rect stop svg text tspan) 17 | 18 | # the present method is retained as a legacy behavior 19 | def present(component, *params, &children) 20 | React::RenderingContext.render(component, *params, &children) 21 | end 22 | 23 | # define each predefined tag (upcase) as an instance method and a constant 24 | # deprecated: define each predefined tag (downcase) as the alias of the instance method 25 | 26 | HTML_TAGS.each do |tag| 27 | 28 | define_method(tag.upcase) do |*params, &children| 29 | React::RenderingContext.render(tag, *params, &children) 30 | end 31 | 32 | const_set tag.upcase, tag 33 | 34 | # deprecated: remove 35 | if tag == 'p' 36 | define_method(tag) do |*params, &children| 37 | if children || params.count == 0 || (params.count == 1 && params.first.is_a?(Hash)) 38 | React::RenderingContext.render(tag, *params, &children) 39 | else 40 | Kernel.p(*params) 41 | end 42 | end 43 | else 44 | alias_method tag, tag.upcase 45 | end 46 | # end of deprecated code 47 | end 48 | 49 | # this is used for haml style (i.e. DIV.foo.bar) class tags which is deprecated 50 | def self.html_tag_class_for(tag) 51 | downcased_tag = tag.downcase 52 | if tag =~ /[A-Z]+/ && HTML_TAGS.include?(downcased_tag) 53 | Object.const_set tag, React.create_element(downcased_tag) 54 | end 55 | end 56 | 57 | # use method_missing to look up component names in the form of "Foo(..)" 58 | # where there is no preceeding scope. 59 | 60 | def method_missing(name, *params, &children) 61 | component = find_component(name) 62 | return React::RenderingContext.render(component, *params, &children) if component 63 | Object.method_missing(name, *params, &children) 64 | end 65 | 66 | # install methods with the same name as the component in the parent class/module 67 | # thus component names in the form Foo::Bar(...) will work 68 | 69 | class << self 70 | def included(component) 71 | name, parent = find_name_and_parent(component) 72 | tag_names_module = Module.new do 73 | define_method name do |*params, &children| 74 | React::RenderingContext.render(component, *params, &children) 75 | end 76 | # handle deprecated _as_node style 77 | define_method "#{name}_as_node" do |*params, &children| 78 | React::RenderingContext.build_only(component, *params, &children) 79 | end 80 | end 81 | parent.extend(tag_names_module) 82 | end 83 | 84 | private 85 | 86 | def find_name_and_parent(component) 87 | split_name = component.name && component.name.split('::') 88 | if split_name && split_name.length > 1 89 | [split_name.last, split_name.inject([Module]) { |a, e| a + [a.last.const_get(e)] }[-2]] 90 | end 91 | end 92 | end 93 | 94 | private 95 | 96 | def find_component(name) 97 | component = lookup_const(name) 98 | if component && !component.method_defined?(:render) 99 | raise "#{name} does not appear to be a react component." 100 | end 101 | component 102 | end 103 | 104 | def lookup_const(name) 105 | return nil unless name =~ /^[A-Z]/ 106 | #html_tag = React::Component::Tags.html_tag_class(name) 107 | #return html_tag if html_tag 108 | scopes = self.class.name.to_s.split('::').inject([Module]) do |nesting, next_const| 109 | nesting + [nesting.last.const_get(next_const)] 110 | end.reverse 111 | scope = scopes.detect { |s| s.const_defined?(name) } 112 | scope.const_get(name) if scope 113 | end 114 | end 115 | end 116 | end 117 | -------------------------------------------------------------------------------- /lib/react/config.rb: -------------------------------------------------------------------------------- 1 | if RUBY_ENGINE != 'opal' 2 | module Hyperloop 3 | define_setting :prerendering, :off 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /lib/react/element.rb: -------------------------------------------------------------------------------- 1 | require 'react/ext/string' 2 | 3 | module React 4 | # 5 | # Wraps the React Native element class 6 | # 7 | # adds the #on method to add event handlers to the element 8 | # 9 | # adds the #render method to place elements in the DOM and 10 | # #delete (alias/deprecated #as_node) method to remove elements from the DOM 11 | # 12 | # handles the haml style class notation so that 13 | # div.bar.blat becomes div(class: "bar blat") 14 | # by using method missing 15 | # 16 | class Element 17 | include Native 18 | 19 | alias_native :element_type, :type 20 | alias_native :props, :props 21 | 22 | attr_reader :type 23 | attr_reader :properties 24 | attr_reader :block 25 | 26 | attr_accessor :waiting_on_resources 27 | 28 | def initialize(native_element, type = nil, properties = {}, block = nil) 29 | @type = type 30 | @properties = (`typeof #{properties} === 'undefined'` ? nil : properties) || {} 31 | @block = block 32 | @native = native_element 33 | end 34 | 35 | # Attach event handlers. 36 | 37 | def on(*event_names, &block) 38 | event_names.each { |event_name| merge_event_prop!(event_name, &block) } 39 | @native = `React.cloneElement(#{@native}, #{@properties.shallow_to_n})` 40 | self 41 | end 42 | 43 | # Render element into DOM in the current rendering context. 44 | # Used for elements that are not yet in DOM, i.e. they are provided as children 45 | # or they have been explicitly removed from the rendering context using the delete method. 46 | 47 | def render(props = {}, &new_block) 48 | if props.empty? 49 | React::RenderingContext.render(self) 50 | else 51 | props = API.convert_props(props) 52 | React::RenderingContext.render( 53 | Element.new(`React.cloneElement(#{@native}, #{props.shallow_to_n})`, 54 | type, @properties.merge(props), block), 55 | ) 56 | end 57 | end 58 | 59 | # Delete (remove) element from rendering context, the element may later be added back in 60 | # using the render method. 61 | 62 | def delete 63 | React::RenderingContext.delete(self) 64 | end 65 | # Deprecated version of delete method 66 | alias as_node delete 67 | 68 | # Any other method applied to an element will be treated as class name (haml style) thus 69 | # div.foo.bar(id: :fred) is the same as saying div(class: "foo bar", id: :fred) 70 | # 71 | # single underscores become dashes, and double underscores become a single underscore 72 | # 73 | # params may be provide to each class (but typically only to the last for easy reading.) 74 | 75 | def method_missing(class_name, args = {}, &new_block) 76 | return dup.render.method_missing(class_name, args, &new_block) unless rendered? 77 | React::RenderingContext.replace( 78 | self, 79 | RenderingContext.build do 80 | RenderingContext.render(type, build_new_properties(class_name, args), &new_block) 81 | end 82 | ) 83 | end 84 | 85 | def rendered? 86 | React::RenderingContext.rendered? self 87 | end 88 | 89 | def self.haml_class_name(class_name) 90 | class_name.gsub(/__|_/, '__' => '_', '_' => '-') 91 | end 92 | 93 | private 94 | 95 | def build_new_properties(class_name, args) 96 | class_name = self.class.haml_class_name(class_name) 97 | new_props = @properties.dup 98 | new_props[:className] = "\ 99 | #{class_name} #{new_props[:className]} #{args.delete(:class)} #{args.delete(:className)}\ 100 | ".split(' ').uniq.join(' ') 101 | new_props.merge! args 102 | end 103 | 104 | # built in events, events going to native components, and events going to reactrb 105 | 106 | # built in events will have their event param translated to the Event wrapper 107 | # and the name will camelcased and have on prefixed, so :click becomes onClick. 108 | # 109 | # events emitting from native components are assumed to have the same camel case and 110 | # on prefixed. 111 | # 112 | # events emitting from reactrb components will just have on_ prefixed. So 113 | # :play_button_pushed attaches to the :on_play_button_pushed param 114 | # 115 | # in all cases the default name convention can be overriden by wrapping in <...> brackets. 116 | # So on("") will attach to the "MyEvent" param. 117 | 118 | def merge_event_prop!(event_name, &block) 119 | if event_name =~ /^<(.+)>$/ 120 | merge_component_event_prop! event_name.gsub(/^<(.+)>$/, '\1'), &block 121 | elsif React::Event::BUILT_IN_EVENTS.include?(name = "on#{event_name.event_camelize}") 122 | merge_built_in_event_prop! name, &block 123 | elsif @type.instance_variable_get('@native_import') 124 | merge_component_event_prop! name, &block 125 | else 126 | merge_component_event_prop! "on_#{event_name}", &block 127 | end 128 | end 129 | 130 | def merge_built_in_event_prop!(prop_name) 131 | @properties.merge!( 132 | prop_name => %x{ 133 | function(){ 134 | var react_event = arguments[0]; 135 | var all_args; 136 | var other_args; 137 | if (arguments.length > 1) { 138 | all_args = Array.prototype.slice.call(arguments); 139 | other_args = all_args.slice(1, arguments.length); 140 | return #{yield(React::Event.new(`react_event`), *(`other_args`))}; 141 | } else { 142 | return #{yield(React::Event.new(`react_event`))}; 143 | } 144 | } 145 | } 146 | ) 147 | end 148 | 149 | def merge_component_event_prop!(prop_name) 150 | @properties.merge!( 151 | prop_name => %x{ 152 | function(){ 153 | return #{yield(*Array(`arguments`))} 154 | } 155 | } 156 | ) 157 | end 158 | end 159 | end 160 | -------------------------------------------------------------------------------- /lib/react/event.rb: -------------------------------------------------------------------------------- 1 | module React 2 | class Event 3 | include Native 4 | alias_native :bubbles, :bubbles 5 | alias_native :cancelable, :cancelable 6 | alias_native :current_target, :currentTarget 7 | alias_native :default_prevented, :defaultPrevented 8 | alias_native :event_phase, :eventPhase 9 | alias_native :is_trusted?, :isTrusted 10 | alias_native :native_event, :nativeEvent 11 | alias_native :target, :target 12 | alias_native :timestamp, :timeStamp 13 | alias_native :event_type, :type 14 | alias_native :prevent_default, :preventDefault 15 | alias_native :stop_propagation, :stopPropagation 16 | # Clipboard 17 | alias_native :clipboard_data, :clipboardData 18 | # Keyboard 19 | alias_native :alt_key, :altKey 20 | alias_native :char_code, :charCode 21 | alias_native :ctrl_key, :ctrlKey 22 | alias_native :get_modifier_state, :getModifierState 23 | alias_native :key, :key 24 | alias_native :key_code, :keyCode 25 | alias_native :locale, :locale 26 | alias_native :location, :location 27 | alias_native :meta_key, :metaKey 28 | alias_native :repeat, :repeat 29 | alias_native :shift_key, :shiftKey 30 | alias_native :which, :which 31 | # Focus 32 | alias_native :related_target, :relatedTarget 33 | # Mouse 34 | # aliased above: alias_native :alt_key, :altKey 35 | alias_native :button, :button 36 | alias_native :buttons, :buttons 37 | alias_native :client_x, :clientX 38 | alias_native :client_y, :clientY 39 | # aliased above: alias_native :ctrl_key, :ctrlKey 40 | alias_native :get_modifier_state, :getModifierState 41 | # aliased above: alias_native :meta_key, :metaKey 42 | alias_native :page_x, :pageX 43 | alias_native :page_y, :pageY 44 | # aliased above: alias_native :related_target, :relatedTarget 45 | alias_native :screen_x, :screen_x 46 | alias_native :screen_y, :screen_y 47 | # aliased above: alias_native :shift_key, :shift_key 48 | # Touch 49 | # aliased above: alias_native :alt_key, :altKey 50 | alias_native :changed_touches, :changedTouches 51 | # aliased above: alias_native :ctrl_key, :ctrlKey 52 | # aliased above: alias_native :get_modifier_state, :getModifierState 53 | # aliased above: alias_native :meta_key, :metaKey 54 | # aliased above: alias_native :shift_key, :shiftKey 55 | alias_native :target_touches, :targetTouches 56 | alias_native :touches, :touches 57 | # UI 58 | alias_native :detail, :detail 59 | alias_native :view, :view 60 | # Wheel 61 | alias_native :delta_mode, :deltaMode 62 | alias_native :delta_x, :deltaX 63 | alias_native :delta_y, :deltaY 64 | alias_native :delta_z, :deltaZ 65 | 66 | BUILT_IN_EVENTS = %w{onCopy onCut onPaste onKeyDown onKeyPress onKeyUp 67 | onFocus onBlur onChange onInput onSubmit onClick onContextMenu onDoubleClick onDrag 68 | onDragEnd onDragEnter onDragExit onDragLeave onDragOver onDragStart onDrop 69 | onMouseDown onMouseEnter onMouseLeave onMouseMove onMouseOut onMouseOver 70 | onMouseUp onSelect onTouchCancel onTouchEnd onTouchMove onTouchStart onScroll onWheel} 71 | 72 | def initialize(native_event) 73 | @native = native_event 74 | end 75 | end 76 | end 77 | -------------------------------------------------------------------------------- /lib/react/ext/hash.rb: -------------------------------------------------------------------------------- 1 | class Hash 2 | def shallow_to_n 3 | hash = `{}` 4 | self.each do |key, value| 5 | `hash[#{key}] = #{value}` 6 | end 7 | hash 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /lib/react/ext/opal-jquery/element.rb: -------------------------------------------------------------------------------- 1 | Element.instance_eval do 2 | def self.find(selector) 3 | selector = begin 4 | selector.dom_node 5 | rescue 6 | selector 7 | end if `#{selector}.$dom_node !== undefined` 8 | `$(#{selector})` 9 | end 10 | 11 | def self.[](selector) 12 | find(selector) 13 | end 14 | 15 | define_method :render do |container = nil, params = {}, &block| 16 | if `#{self.to_n}._reactrb_component_class === undefined` 17 | `#{self.to_n}._reactrb_component_class = #{Class.new(Hyperloop::Component)}` 18 | end 19 | klass = `#{self.to_n}._reactrb_component_class` 20 | klass.class_eval do 21 | render(container, params, &block) 22 | end 23 | 24 | React.render(React.create_element(`#{self.to_n}._reactrb_component_class`), self) 25 | end 26 | 27 | # mount_components is useful for dynamically generated page segments for example 28 | # see react-rails documentation for more details 29 | 30 | %x{ 31 | $.fn.mount_components = function() { 32 | this.each(function(e) { ReactRailsUJS.mountComponents(e[0]) }) 33 | return this; 34 | } 35 | } 36 | Element.expose :mount_components 37 | end if Object.const_defined?('Element') 38 | -------------------------------------------------------------------------------- /lib/react/ext/string.rb: -------------------------------------------------------------------------------- 1 | class String 2 | def event_camelize 3 | `return #{self}.replace(/(^|_)([^_]+)/g, function(match, pre, word, index) { 4 | var capitalize = true; 5 | return capitalize ? word.substr(0,1).toUpperCase()+word.substr(1) : word; 6 | })` 7 | end 8 | end -------------------------------------------------------------------------------- /lib/react/native_library.rb: -------------------------------------------------------------------------------- 1 | module React 2 | # NativeLibrary handles importing JS libraries. Importing native components is handled 3 | # by the React::Base. It also provides several methods used by auto-import.rb 4 | 5 | # A NativeLibrary is simply a wrapper that holds the name of the native js library. 6 | # It responds to const_missing and method_missing by looking up objects within the js library. 7 | # If the object is a react component it is wrapped by a reactrb component class, otherwise 8 | # a nested NativeLibrary is returned. 9 | 10 | # Two macros are provided: imports (for naming the native library) and renames which allows 11 | # the members of a library to be given different names within the ruby name space. 12 | 13 | # Public methods used by auto-import.rb are import_const_from_native and find_and_render_component 14 | class NativeLibrary 15 | class << self 16 | def imports(native_name) 17 | @native_prefix = "#{native_name}." 18 | self 19 | end 20 | 21 | def rename(rename_list) 22 | # rename_list is a hash in the form: native_name => ruby_name, native_name => ruby_name 23 | rename_list.each do |js_name, ruby_name| 24 | native_name = lookup_native_name(js_name) 25 | if lookup_native_name(js_name) 26 | create_component_wrapper(self, native_name, ruby_name) || 27 | create_library_wrapper(self, native_name, ruby_name) 28 | else 29 | raise "class #{name} < React::NativeLibrary could not import #{js_name}. "\ 30 | "Native value #{scope_native_name(js_name)} is undefined." 31 | end 32 | end 33 | end 34 | 35 | def import_const_from_native(klass, const_name, create_library) 36 | native_name = lookup_native_name(const_name) || 37 | lookup_native_name(const_name[0].downcase + const_name[1..-1]) 38 | native_name && ( 39 | create_component_wrapper(klass, native_name, const_name) || ( 40 | create_library && 41 | create_library_wrapper(klass, native_name, const_name))) 42 | end 43 | 44 | def const_missing(const_name) 45 | import_const_from_native(self, const_name, true) || super 46 | end 47 | 48 | def method_missing(method, *args, &block) 49 | component_class = const_get(method) if const_defined?(method, false) 50 | component_class ||= import_const_from_native(self, method, false) 51 | raise 'could not import a react component named: '\ 52 | "#{scope_native_name method}" unless component_class 53 | React::RenderingContext.render(component_class, *args, &block) 54 | end 55 | 56 | private 57 | 58 | def lookup_native_name(js_name) 59 | native_name = scope_native_name(js_name) 60 | `eval(#{native_name}) !== undefined && native_name` 61 | # rubocop:disable Lint/RescueException # that is what eval raises in Opal >= 0.10. 62 | rescue Exception 63 | nil 64 | # rubocop:enable Lint/RescueException 65 | end 66 | 67 | def scope_native_name(js_name) 68 | "#{@native_prefix}#{js_name}" 69 | end 70 | 71 | def create_component_wrapper(klass, native_name, ruby_name) 72 | if React::API.native_react_component?(native_name) 73 | new_klass = klass.const_set ruby_name, Class.new 74 | new_klass.class_eval do 75 | include Hyperloop::Component::Mixin 76 | imports native_name 77 | end 78 | new_klass 79 | end 80 | end 81 | 82 | def create_library_wrapper(klass, native_name, ruby_name) 83 | klass.const_set ruby_name, Class.new(React::NativeLibrary).imports(native_name) 84 | end 85 | end 86 | end 87 | end 88 | -------------------------------------------------------------------------------- /lib/react/object.rb: -------------------------------------------------------------------------------- 1 | # Lazy load HTML tag constants in the form DIV or A 2 | # This is needed to allow for a HAML expression like this DIV.my_class 3 | class Object 4 | class << self 5 | alias _reactrb_tag_original_const_missing const_missing 6 | 7 | def const_missing(const_name) 8 | # Opal uses const_missing to initially define things, 9 | # so we always call the original, and respond to the exception 10 | _reactrb_tag_original_const_missing(const_name) 11 | rescue StandardError => e 12 | React::Component::Tags.html_tag_class_for(const_name) || raise(e) 13 | end 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /lib/react/react-source-browser.rb: -------------------------------------------------------------------------------- 1 | if RUBY_ENGINE == 'opal' 2 | require 'react.js' 3 | end 4 | -------------------------------------------------------------------------------- /lib/react/react-source-server.rb: -------------------------------------------------------------------------------- 1 | if RUBY_ENGINE == 'opal' 2 | require 'react-server.js' 3 | end 4 | -------------------------------------------------------------------------------- /lib/react/react-source.rb: -------------------------------------------------------------------------------- 1 | if RUBY_ENGINE == 'opal' 2 | %x{ 3 | var ms = [ 4 | "Warning: `react/react-source` is deprecated, ", 5 | "use `react/react-source-browser` or `react/react-source-server` instead." 6 | ] 7 | console.error(ms.join("")); 8 | } 9 | require 'react.js' 10 | require "react-server.js" 11 | else 12 | require "react/config" 13 | require "react/rails/asset_variant" 14 | variant = Hyperloop.env.production? ? 'production' : 'development' 15 | react_directory = React::Rails::AssetVariant.new({environment: variant}).react_directory 16 | Opal.append_path react_directory.untaint 17 | end 18 | -------------------------------------------------------------------------------- /lib/react/ref_callback.rb: -------------------------------------------------------------------------------- 1 | require 'react/native_library' 2 | 3 | module React 4 | module RefsCallbackExtension 5 | end 6 | 7 | class API 8 | class << self 9 | alias :orig_convert_props :convert_props 10 | end 11 | 12 | def self.convert_props(properties) 13 | props = self.orig_convert_props(properties) 14 | props.map do |key, value| 15 | if key == "ref" && value.is_a?(Proc) 16 | new_proc = Proc.new do |native_inst| 17 | if `#{native_inst} !== null && #{native_inst}.__opalInstance !== undefined && #{native_inst}.__opalInstance !== null` 18 | value.call(`#{native_inst}.__opalInstance`) 19 | elsif `#{native_inst} !== null && ReactDOM.findDOMNode !== undefined && #{native_inst}.nodeType === undefined` 20 | value.call(`ReactDOM.findDOMNode(#{native_inst})`) # react >= v0.15.`) 21 | else 22 | value.call(native_inst) 23 | end 24 | end 25 | props[key] = new_proc 26 | end 27 | end 28 | props 29 | end 30 | end 31 | end 32 | -------------------------------------------------------------------------------- /lib/react/rendering_context.rb: -------------------------------------------------------------------------------- 1 | module React 2 | class RenderingContext 3 | class << self 4 | attr_accessor :waiting_on_resources 5 | 6 | def render(name, *args, &block) 7 | was_outer_most = !@not_outer_most 8 | @not_outer_most = true 9 | remove_nodes_from_args(args) 10 | @buffer ||= [] unless @buffer 11 | if block 12 | element = build do 13 | saved_waiting_on_resources = waiting_on_resources 14 | self.waiting_on_resources = nil 15 | run_child_block(name.nil?, &block) 16 | if name 17 | buffer = @buffer.dup 18 | React::API.create_element(name, *args) { buffer }.tap do |element| 19 | element.waiting_on_resources = saved_waiting_on_resources || !!buffer.detect { |e| e.waiting_on_resources if e.respond_to?(:waiting_on_resources) } 20 | element.waiting_on_resources ||= waiting_on_resources if buffer.last.is_a?(String) 21 | end 22 | elsif @buffer.last.is_a? React::Element 23 | @buffer.last.tap { |element| element.waiting_on_resources ||= saved_waiting_on_resources } 24 | else 25 | buffer_s = @buffer.last.to_s 26 | React::RenderingContext.render(:span) { buffer_s }.tap { |element| element.waiting_on_resources = saved_waiting_on_resources } 27 | end 28 | end 29 | elsif name.is_a? React::Element 30 | element = name 31 | else 32 | element = React::API.create_element(name, *args) 33 | element.waiting_on_resources = waiting_on_resources 34 | end 35 | @buffer << element 36 | self.waiting_on_resources = nil 37 | element 38 | ensure 39 | @not_outer_most = @buffer = nil if was_outer_most 40 | end 41 | 42 | def build 43 | current = @buffer 44 | @buffer = [] 45 | return_val = yield @buffer 46 | @buffer = current 47 | return_val 48 | end 49 | 50 | def delete(element) 51 | @buffer.delete(element) 52 | element 53 | end 54 | alias as_node delete 55 | 56 | def rendered?(element) 57 | @buffer.include? element 58 | end 59 | 60 | def replace(e1, e2) 61 | @buffer[@buffer.index(e1)] = e2 62 | end 63 | 64 | def remove_nodes_from_args(args) 65 | args[0].each do |key, value| 66 | begin 67 | value.delete if value.is_a?(Element) # deletes Element from buffer 68 | rescue Exception 69 | end 70 | end if args[0] && args[0].is_a?(Hash) 71 | end 72 | 73 | # run_child_block gathers the element(s) generated by a child block. 74 | # for example when rendering this div: div { "hello".span; "goodby".span } 75 | # two child Elements will be generated. 76 | # 77 | # the final value of the block should either be 78 | # 1 an object that responds to :acts_as_string? 79 | # 2 a string, 80 | # 3 an element that is NOT yet pushed on the rendering buffer 81 | # 4 or the last element pushed on the buffer 82 | # 83 | # in case 1 we render a span 84 | # in case 2 we automatically push the string onto the buffer 85 | # in case 3 we also push the Element onto the buffer IF the buffer is empty 86 | # case 4 requires no special processing 87 | # 88 | # Once we have taken care of these special cases we do a check IF we are in an 89 | # outer rendering scope. In this case react only allows us to generate 1 Element 90 | # so we insure that is the case, and also check to make sure that element in the buffer 91 | # is the element returned 92 | 93 | def run_child_block(is_outer_scope) 94 | result = yield 95 | if result.respond_to?(:acts_as_string?) && result.acts_as_string? 96 | # hyper-mesh DummyValues respond to acts_as_string, and must 97 | # be converted to spans INSIDE the parent, otherwise the waiting_on_resources 98 | # flag will get set in the wrong context 99 | React::RenderingContext.render(:span) { result.to_s } 100 | elsif result.is_a?(String) || (result.is_a?(React::Element) && @buffer.empty?) 101 | @buffer << result 102 | end 103 | raise_render_error(result) if is_outer_scope && @buffer != [result] 104 | end 105 | 106 | # heurestically raise a meaningful error based on the situation 107 | 108 | def raise_render_error(result) 109 | improper_render 'A different element was returned than was generated within the DSL.', 110 | 'Possibly improper use of Element#delete.' if @buffer.count == 1 111 | improper_render "Instead #{@buffer.count} elements were generated.", 112 | 'Do you want to wrap your elements in a div?' if @buffer.count > 1 113 | improper_render "Instead the component #{result} was returned.", 114 | "Did you mean #{result}()?" if result.try :reactrb_component? 115 | improper_render "Instead the #{result.class} #{result} was returned.", 116 | 'You may need to convert this to a string.' 117 | end 118 | 119 | def improper_render(message, solution) 120 | raise "a component's render method must generate and return exactly 1 element or a string.\n"\ 121 | " #{message} #{solution}" 122 | end 123 | end 124 | end 125 | end 126 | 127 | class Object 128 | [:span, :td, :th, :while_loading].each do |tag| 129 | define_method(tag) do |*args, &block| 130 | args.unshift(tag) 131 | return send(*args, &block) if is_a? React::Component 132 | React::RenderingContext.render(*args) { to_s } 133 | end 134 | end 135 | 136 | def para(*args, &block) 137 | args.unshift(:p) 138 | return send(*args, &block) if is_a? React::Component 139 | React::RenderingContext.render(*args) { to_s } 140 | end 141 | 142 | def br 143 | return send(:br) if is_a? React::Component 144 | React::RenderingContext.render(:span) do 145 | React::RenderingContext.render(to_s) 146 | React::RenderingContext.render(:br) 147 | end 148 | end 149 | end 150 | -------------------------------------------------------------------------------- /lib/react/server.rb: -------------------------------------------------------------------------------- 1 | module React 2 | module Server 3 | def self.render_to_string(element) 4 | if !(`typeof ReactDOMServer === 'undefined'`) 5 | React::RenderingContext.build { `ReactDOMServer.renderToString(#{element.to_n})` } # v0.15+ 6 | else 7 | raise "renderToString is not defined. In React >= v15 you must import it with ReactDOMServer" 8 | end 9 | end 10 | 11 | def self.render_to_static_markup(element) 12 | if !(`typeof ReactDOMServer === 'undefined'`) 13 | React::RenderingContext.build { `ReactDOMServer.renderToStaticMarkup(#{element.to_n})` } # v0.15+ 14 | else 15 | raise "renderToStaticMarkup is not defined. In React >= v15 you must import it with ReactDOMServer" 16 | end 17 | end 18 | end 19 | end 20 | -------------------------------------------------------------------------------- /lib/react/state_wrapper.rb: -------------------------------------------------------------------------------- 1 | module HyperStore 2 | class StateWrapper < BaseStoreClass # < BasicObject 3 | 4 | def [](state) 5 | `#{__from__.instance_variable_get('@native')}.state[#{state}] || #{nil}` 6 | end 7 | 8 | def []=(state, new_value) 9 | `#{__from__.instance_variable_get('@native')}.state[#{state}] = new_value` 10 | end 11 | 12 | alias pre_component_method_missing method_missing 13 | 14 | def method_missing(method, *args) 15 | if method.end_with?('!') && __from__.respond_to?(:deprecation_warning) 16 | __from__.deprecation_warning("The mutator 'state.#{method}' has been deprecated. Use 'mutate.#{method.sub(/\!$/,'')}' instead.") 17 | __from__.mutate.__send__(method.chop, *args) 18 | else 19 | pre_component_method_missing(method, *args) 20 | end 21 | end 22 | end 23 | end 24 | -------------------------------------------------------------------------------- /lib/react/test.rb: -------------------------------------------------------------------------------- 1 | require 'react/test/session' 2 | require 'react/test/dsl' 3 | 4 | module React 5 | module Test 6 | class << self 7 | def current_session 8 | @current_session ||= Session.new 9 | end 10 | 11 | def reset_session! 12 | @current_session = nil 13 | end 14 | end 15 | end 16 | end 17 | -------------------------------------------------------------------------------- /lib/react/test/dsl.rb: -------------------------------------------------------------------------------- 1 | require 'react/test' 2 | 3 | module React 4 | module Test 5 | module DSL 6 | def component 7 | React::Test.current_session 8 | end 9 | 10 | Session::DSL_METHODS.each do |method| 11 | define_method method do |*args, &block| 12 | component.public_send(method, *args, &block) 13 | end 14 | end 15 | end 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /lib/react/test/matchers/render_html_matcher.rb: -------------------------------------------------------------------------------- 1 | module React 2 | module Test 3 | module Matchers 4 | class RenderHTMLMatcher 5 | def initialize(expected) 6 | @expected = expected 7 | @params = {} 8 | end 9 | 10 | def with_params(params) 11 | @params = params 12 | self 13 | end 14 | 15 | def matches?(component) 16 | @component = component 17 | @actual = render_to_html 18 | @expected == @actual 19 | end 20 | 21 | def failure_message 22 | failure_string 23 | end 24 | 25 | def failure_message_when_negated 26 | failure_string(:negative) 27 | end 28 | 29 | alias negative_failure_message failure_message_when_negated 30 | 31 | private 32 | 33 | def render_to_html 34 | element = React.create_element(@component, @params) 35 | React::Server.render_to_static_markup(element) 36 | end 37 | 38 | def failure_string(negative = false) 39 | str = "expected '#{@component.name}' with params '#{@params}' to " 40 | str = str + "not " if negative 41 | str = str + "render '#{@expected}', but '#{@actual}' was rendered." 42 | str 43 | end 44 | end 45 | 46 | def render_static_html(*args) 47 | RenderHTMLMatcher.new(*args) 48 | end 49 | 50 | def render(*args) 51 | %x{ console.error("Warning: `render` matcher is deprecated in favor of `render_static_html`."); } 52 | RenderHTMLMatcher.new(*args) 53 | end 54 | end 55 | end 56 | end 57 | -------------------------------------------------------------------------------- /lib/react/test/rspec.rb: -------------------------------------------------------------------------------- 1 | require 'react/test/dsl' 2 | require 'react/test/matchers/render_html_matcher' 3 | 4 | RSpec.configure do |config| 5 | config.include React::Test::DSL, type: :component 6 | config.include React::Test::Matchers, type: :component 7 | 8 | config.after do 9 | React::Test.reset_session! 10 | end 11 | 12 | config.before do 13 | # nothing yet 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /lib/react/test/session.rb: -------------------------------------------------------------------------------- 1 | module React 2 | module Test 3 | class Session 4 | DSL_METHODS = %i[mount instance update_params html].freeze 5 | 6 | def mount(component_klass, params = {}) 7 | @element = React.create_element(component_klass, params) 8 | instance 9 | end 10 | 11 | def instance 12 | unless @instance 13 | @container = `document.createElement('div')` 14 | @instance = React.render(@element, @container) 15 | end 16 | @instance 17 | end 18 | 19 | def update_params(params, &block) 20 | cloned_element = React::Element.new(`React.cloneElement(#{@element.to_n}, #{params.to_n})`) 21 | React.render(cloned_element, @container, &block) 22 | nil 23 | end 24 | 25 | def html 26 | html = `#@container.innerHTML` 27 | %x{ 28 | var REGEX_REMOVE_ROOT_IDS = /\s?data-reactroot="[^"]*"/g; 29 | var REGEX_REMOVE_IDS = /\s?data-reactid="[^"]+"/g; 30 | html = html.replace(REGEX_REMOVE_ROOT_IDS, ''); 31 | html = html.replace(REGEX_REMOVE_IDS, ''); 32 | } 33 | return html 34 | end 35 | end 36 | end 37 | end 38 | -------------------------------------------------------------------------------- /lib/react/test/utils.rb: -------------------------------------------------------------------------------- 1 | module React 2 | module Test 3 | class Utils 4 | def self.render_component_into_document(component, args = {}) 5 | element = React.create_element(component, args) 6 | render_into_document(element) 7 | end 8 | 9 | def self.render_into_document(element) 10 | raise "You should pass a valid React::Element" unless React.is_valid_element?(element) 11 | dom_el = `document.body.querySelector('div[data-react-class="React.TopLevelRailsComponent"]').appendChild(document.createElement('div'))` 12 | React.render(element, dom_el) 13 | end 14 | 15 | def self.simulate_click(element) 16 | # element must be a component or a dom node or a element 17 | el = if `typeof element.nodeType !== "undefined"` 18 | element 19 | elsif element.is_a? React::Component 20 | element.dom_node 21 | elsif element.is_a? React::Element 22 | `ReactDOM.findDOMNode(#{element.to_n}.native)` 23 | else 24 | element 25 | end 26 | %x{ 27 | var evob = new MouseEvent('click', { 28 | view: window, 29 | bubbles: true, 30 | cancelable: true 31 | }); 32 | el.dispatchEvent(evob); 33 | } 34 | end 35 | 36 | def self.simulate_keydown(element, key_name = "Enter") 37 | # element must be a component or a dom node or a element 38 | el = if `typeof element.nodeType !== "undefined"` 39 | element 40 | elsif element.is_a? React::Component 41 | element.dom_node 42 | elsif element.is_a? React::Element 43 | `ReactDOM.findDOMNode(#{element.to_n}.native)` 44 | else 45 | element 46 | end 47 | %x{ 48 | var evob = new KeyboardEvent('keydown', { key: key_name, bubbles: true, cancelable: true }); 49 | el.dispatchEvent(evob); 50 | } 51 | end 52 | 53 | def self.simulate_submit(element) 54 | # element must be a component or a dom node or a element 55 | el = if `typeof element.nodeType !== "undefined"` 56 | element 57 | elsif element.is_a? React::Component 58 | element.dom_node 59 | elsif element.is_a? React::Element 60 | `ReactDOM.findDOMNode(#{element.to_n}.native)` 61 | else 62 | element 63 | end 64 | %x{ 65 | var evob = new Event('submit', { bubbles: true, cancelable: true }); 66 | el.dispatchEvent(evob); 67 | } 68 | end 69 | end 70 | end 71 | end 72 | -------------------------------------------------------------------------------- /lib/react/to_key.rb: -------------------------------------------------------------------------------- 1 | # to_key method returns a suitable unique id that can be used as 2 | # a react `key`. Other classes may override to_key as needed 3 | # for example hyper_mesh returns the object id of the internal 4 | # backing record. 5 | # 6 | # to_key is automatically called on objects passed as keys for 7 | # example Foo(key: my_object) results in Foo(key: my_object.to_key) 8 | class Object 9 | def to_key 10 | object_id 11 | end 12 | end 13 | 14 | # for Number to_key can just be the number itself 15 | class Number 16 | def to_key 17 | self 18 | end 19 | end 20 | 21 | # for Boolean to_key can be true or false 22 | class Boolean 23 | def to_key 24 | self 25 | end 26 | end 27 | -------------------------------------------------------------------------------- /lib/react/top_level.rb: -------------------------------------------------------------------------------- 1 | require "native" 2 | require 'active_support/core_ext/object/try' 3 | require 'react/component/tags' 4 | require 'react/component/base' 5 | 6 | module React 7 | 8 | ATTRIBUTES = %w(accept acceptCharset accessKey action allowFullScreen allowTransparency alt 9 | async autoComplete autoPlay cellPadding cellSpacing charSet checked classID 10 | className cols colSpan content contentEditable contextMenu controls coords 11 | crossOrigin data dateTime defer dir disabled download draggable encType form 12 | formAction formEncType formMethod formNoValidate formTarget frameBorder height 13 | hidden href hrefLang htmlFor httpEquiv icon id label lang list loop manifest 14 | marginHeight marginWidth max maxLength media mediaGroup method min multiple 15 | muted name noValidate open pattern placeholder poster preload radioGroup 16 | readOnly rel required role rows rowSpan sandbox scope scrolling seamless 17 | selected shape size sizes span spellCheck src srcDoc srcSet start step style 18 | tabIndex target title type useMap value width wmode dangerouslySetInnerHTML) + 19 | #SVG ATTRIBUTES 20 | %w(clipPath cx cy d dx dy fill fillOpacity fontFamily 21 | fontSize fx fy gradientTransform gradientUnits markerEnd 22 | markerMid markerStart offset opacity patternContentUnits 23 | patternUnits points preserveAspectRatio r rx ry spreadMethod 24 | stopColor stopOpacity stroke strokeDasharray strokeLinecap 25 | strokeOpacity strokeWidth textAnchor transform version 26 | viewBox x1 x2 x xlinkActuate xlinkArcrole xlinkHref xlinkRole 27 | xlinkShow xlinkTitle xlinkType xmlBase xmlLang xmlSpace y1 y2 y) 28 | HASH_ATTRIBUTES = %w(data aria) 29 | HTML_TAGS = React::Component::Tags::HTML_TAGS 30 | 31 | def self.html_tag?(name) 32 | tags = HTML_TAGS 33 | %x{ 34 | for(var i = 0; i < tags.length; i++) { 35 | if(tags[i] === name) 36 | return true; 37 | } 38 | return false; 39 | } 40 | end 41 | 42 | def self.html_attr?(name) 43 | attrs = ATTRIBUTES 44 | %x{ 45 | for(var i = 0; i < attrs.length; i++) { 46 | if(attrs[i] === name) 47 | return true; 48 | } 49 | return false; 50 | } 51 | end 52 | 53 | def self.create_element(type, properties = {}, &block) 54 | React::API.create_element(type, properties, &block) 55 | end 56 | 57 | def self.render(element, container) 58 | %x{ 59 | console.error( 60 | "Warning: Using deprecated behavior of `React.render`,", 61 | "require \"react/top_level_render\" to get the correct behavior." 62 | ); 63 | } 64 | container = `container.$$class ? container[0] : container` 65 | if !(`typeof ReactDOM === 'undefined'`) 66 | component = Native(`ReactDOM.render(#{element.to_n}, container, function(){#{yield if block_given?}})`) # v0.15+ 67 | else 68 | raise "render is not defined. In React >= v15 you must import it with ReactDOM" 69 | end 70 | 71 | component.class.include(React::Component::API) 72 | component 73 | end 74 | 75 | def self.is_valid_element(element) 76 | %x{ console.error("Warning: `is_valid_element` is deprecated in favor of `is_valid_element?`."); } 77 | element.kind_of?(React::Element) && `React.isValidElement(#{element.to_n})` 78 | end 79 | 80 | def self.is_valid_element?(element) 81 | element.kind_of?(React::Element) && `React.isValidElement(#{element.to_n})` 82 | end 83 | 84 | def self.render_to_string(element) 85 | %x{ console.error("Warning: `React.render_to_string` is deprecated in favor of `React::Server.render_to_string`."); } 86 | if !(`typeof ReactDOMServer === 'undefined'`) 87 | React::RenderingContext.build { `ReactDOMServer.renderToString(#{element.to_n})` } # v0.15+ 88 | else 89 | raise "renderToString is not defined. In React >= v15 you must import it with ReactDOMServer" 90 | end 91 | end 92 | 93 | def self.render_to_static_markup(element) 94 | %x{ console.error("Warning: `React.render_to_static_markup` is deprecated in favor of `React::Server.render_to_static_markup`."); } 95 | if !(`typeof ReactDOMServer === 'undefined'`) 96 | React::RenderingContext.build { `ReactDOMServer.renderToStaticMarkup(#{element.to_n})` } # v0.15+ 97 | else 98 | raise "renderToStaticMarkup is not defined. In React >= v15 you must import it with ReactDOMServer" 99 | end 100 | end 101 | 102 | def self.unmount_component_at_node(node) 103 | if !(`typeof ReactDOM === 'undefined'`) 104 | `ReactDOM.unmountComponentAtNode(node.$$class ? node[0] : node)` # v0.15+ 105 | else 106 | raise "unmountComponentAtNode is not defined. In React >= v15 you must import it with ReactDOM" 107 | end 108 | end 109 | 110 | end 111 | -------------------------------------------------------------------------------- /lib/react/top_level_render.rb: -------------------------------------------------------------------------------- 1 | module React 2 | def self.render(element, container) 3 | raise "ReactDOM.render is not defined. In React >= v15 you must import it with ReactDOM" if (`typeof ReactDOM === 'undefined'`) 4 | 5 | container = `container.$$class ? container[0] : container` 6 | 7 | if block_given? 8 | cb = %x{ 9 | function(){ 10 | setTimeout(function(){ 11 | #{yield} 12 | }, 0) 13 | } 14 | } 15 | native = `ReactDOM.render(#{element.to_n}, container, cb)` 16 | else 17 | native = `ReactDOM.render(#{element.to_n}, container)` 18 | end 19 | 20 | if `#{native}.__opalInstance !== undefined && #{native}.__opalInstance !== null` 21 | `#{native}.__opalInstance` 22 | elsif `ReactDOM.findDOMNode !== undefined && #{native}.nodeType === undefined` 23 | `ReactDOM.findDOMNode(#{native})` 24 | else 25 | native 26 | end 27 | end 28 | end 29 | -------------------------------------------------------------------------------- /lib/react/validator.rb: -------------------------------------------------------------------------------- 1 | module React 2 | class Validator 3 | attr_accessor :errors 4 | attr_reader :props_wrapper 5 | private :errors, :props_wrapper 6 | 7 | def initialize(props_wrapper = Class.new(Component::PropsWrapper)) 8 | @props_wrapper = props_wrapper 9 | end 10 | 11 | def self.build(&block) 12 | self.new.build(&block) 13 | end 14 | 15 | def build(&block) 16 | instance_eval(&block) 17 | self 18 | end 19 | 20 | def requires(name, options = {}) 21 | options[:required] = true 22 | define_rule(name, options) 23 | end 24 | 25 | def optional(name, options = {}) 26 | options[:required] = false 27 | define_rule(name, options) 28 | end 29 | 30 | def all_other_params(name) 31 | @allow_undefined_props = true 32 | props_wrapper.define_all_others(name) { |props| props.reject { |name, value| rules[name] } } 33 | end 34 | 35 | def validate(props) 36 | self.errors = [] 37 | validate_undefined(props) unless allow_undefined_props? 38 | props = coerce_native_hash_values(defined_props(props)) 39 | validate_required(props) 40 | props.each do |name, value| 41 | validate_types(name, value) 42 | validate_allowed(name, value) 43 | end 44 | errors 45 | end 46 | 47 | def default_props 48 | rules 49 | .select {|key, value| value.keys.include?("default") } 50 | .inject({}) {|memo, (k,v)| memo[k] = v[:default]; memo} 51 | end 52 | 53 | private 54 | 55 | def defined_props(props) 56 | props.select { |name| rules.keys.include?(name) } 57 | end 58 | 59 | def allow_undefined_props? 60 | !!@allow_undefined_props 61 | end 62 | 63 | def rules 64 | @rules ||= { children: { required: false } } 65 | end 66 | 67 | def define_rule(name, options = {}) 68 | rules[name] = coerce_native_hash_values(options) 69 | props_wrapper.define_param(name, options[:type]) 70 | end 71 | 72 | def errors 73 | @errors ||= [] 74 | end 75 | 76 | def validate_types(prop_name, value) 77 | return unless klass = rules[prop_name][:type] 78 | if !klass.is_a?(Array) 79 | allow_nil = !!rules[prop_name][:allow_nil] 80 | type_check("`#{prop_name}`", value, klass, allow_nil) 81 | elsif klass.length > 0 82 | validate_value_array(prop_name, value) 83 | else 84 | allow_nil = !!rules[prop_name][:allow_nil] 85 | type_check("`#{prop_name}`", value, Array, allow_nil) 86 | end 87 | end 88 | 89 | def type_check(prop_name, value, klass, allow_nil) 90 | return if allow_nil && value.nil? 91 | return if value.is_a?(klass) 92 | return if klass.respond_to?(:_react_param_conversion) && 93 | klass._react_param_conversion(value, :validate_only) 94 | errors << "Provided prop #{prop_name} could not be converted to #{klass}" 95 | end 96 | 97 | def validate_allowed(prop_name, value) 98 | return unless values = rules[prop_name][:values] 99 | return if values.include?(value) 100 | errors << "Value `#{value}` for prop `#{prop_name}` is not an allowed value" 101 | end 102 | 103 | def validate_required(props) 104 | (rules.keys - props.keys).each do |name| 105 | next unless rules[name][:required] 106 | errors << "Required prop `#{name}` was not specified" 107 | end 108 | end 109 | 110 | def validate_undefined(props) 111 | (props.keys - rules.keys).each do |prop_name| 112 | errors << "Provided prop `#{prop_name}` not specified in spec" 113 | end 114 | end 115 | 116 | def validate_value_array(name, value) 117 | klass = rules[name][:type] 118 | allow_nil = !!rules[name][:allow_nil] 119 | value.each_with_index do |item, index| 120 | type_check("`#{name}`[#{index}]", Native(item), klass[0], allow_nil) 121 | end 122 | rescue NoMethodError 123 | errors << "Provided prop `#{name}` was not an Array" 124 | end 125 | 126 | def coerce_native_hash_values(hash) 127 | hash.each do |key, value| 128 | hash[key] = Native(value) 129 | end 130 | end 131 | end 132 | end 133 | -------------------------------------------------------------------------------- /lib/reactive-ruby/component_loader.rb: -------------------------------------------------------------------------------- 1 | module ReactiveRuby 2 | class ComponentLoader 3 | attr_reader :v8_context 4 | private :v8_context 5 | 6 | def initialize(v8_context) 7 | unless v8_context 8 | raise ArgumentError.new('Could not obtain ExecJS runtime context') 9 | end 10 | @v8_context = v8_context 11 | end 12 | 13 | def load(file = components) 14 | return true if loaded? 15 | !!v8_context.eval(opal(file)) 16 | end 17 | 18 | def load!(file = components) 19 | return true if loaded? 20 | self.load(file) 21 | ensure 22 | raise "No HyperReact components found in #{components}" unless loaded? 23 | end 24 | 25 | def loaded? 26 | !!v8_context.eval('Opal.React !== undefined') 27 | rescue ::ExecJS::Error 28 | false 29 | end 30 | 31 | private 32 | 33 | def components 34 | opts = ::Rails.configuration.react.server_renderer_options 35 | return opts[:files].first.gsub(/.js$/,'') if opts && opts[:files] 36 | 'components' 37 | end 38 | 39 | def opal(file) 40 | Opal::Sprockets.load_asset(file) 41 | end 42 | end 43 | end 44 | -------------------------------------------------------------------------------- /lib/reactive-ruby/rails.rb: -------------------------------------------------------------------------------- 1 | require 'action_view' 2 | require 'react-rails' 3 | require 'reactive-ruby/server_rendering/hyper_asset_container' 4 | require 'reactive-ruby/server_rendering/contextual_renderer' 5 | require 'reactive-ruby/rails/component_mount' 6 | require 'reactive-ruby/rails/railtie' 7 | require 'reactive-ruby/rails/controller_helper' 8 | require 'reactive-ruby/component_loader' 9 | -------------------------------------------------------------------------------- /lib/reactive-ruby/rails/component_mount.rb: -------------------------------------------------------------------------------- 1 | module ReactiveRuby 2 | module Rails 3 | class ComponentMount < React::Rails::ComponentMount 4 | attr_accessor :controller 5 | 6 | def setup(controller) 7 | self.controller = controller 8 | end 9 | 10 | def react_component(name, props = {}, options = {}, &block) 11 | if options[:prerender] || [:on, 'on', true].include?(Hyperloop.prerendering) 12 | options = context_initializer_options(options, name) 13 | end 14 | props = serialized_props(props, name, controller) 15 | result = super(top_level_name, props, options, &block).gsub("\n","") 16 | result = result.gsub(/()<\/div>$/,'

\1').html_safe 17 | result + footers 18 | end 19 | 20 | private 21 | 22 | def context_initializer_options(options, name) 23 | options[:prerender] = {options[:prerender] => true} unless options[:prerender].is_a? Hash 24 | existing_context_initializer = options[:prerender][:context_initializer] 25 | 26 | options[:prerender][:context_initializer] = lambda do |ctx| 27 | React::IsomorphicHelpers.load_context(ctx, controller, name) 28 | existing_context_initializer.call(ctx) if existing_context_initializer 29 | end 30 | 31 | options 32 | end 33 | 34 | def serialized_props(props, name, controller) 35 | { render_params: props, component_name: name, 36 | controller: controller.class.name.gsub(/Controller$/,"") }.react_serializer 37 | end 38 | 39 | def top_level_name 40 | 'React.TopLevelRailsComponent' 41 | end 42 | 43 | def footers 44 | React::IsomorphicHelpers.prerender_footers(controller) 45 | end 46 | end 47 | end 48 | end 49 | -------------------------------------------------------------------------------- /lib/reactive-ruby/rails/controller_helper.rb: -------------------------------------------------------------------------------- 1 | require 'action_controller' 2 | 3 | module ActionController 4 | # adds render_component helper to ActionControllers 5 | class Base 6 | def render_component(*args) 7 | @component_name = (args[0].is_a? Hash) || args.empty? ? params[:action].camelize : args.shift 8 | @render_params = args.shift || {} 9 | options = args[0] || {} 10 | render inline: '<%= react_component @component_name, @render_params %>', 11 | layout: options.key?(:layout) ? options[:layout].to_s : :default 12 | end 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /lib/reactive-ruby/rails/railtie.rb: -------------------------------------------------------------------------------- 1 | module ReactiveRuby 2 | module Rails 3 | class Railtie < ::Rails::Railtie 4 | config.before_configuration do |app| 5 | app.config.assets.enabled = true 6 | app.config.assets.paths << ::Rails.root.join('app', 'views').to_s 7 | app.config.react.server_renderer = ReactiveRuby::ServerRendering::ContextualRenderer 8 | app.config.react.view_helper_implementation = ReactiveRuby::Rails::ComponentMount 9 | ReactiveRuby::ServerRendering::ContextualRenderer.asset_container_class = ReactiveRuby::ServerRendering::HyperAssetContainer 10 | end 11 | config.after_initialize do 12 | class ::HyperloopController < ::ApplicationController 13 | def action_missing(name) 14 | render_component 15 | end 16 | end 17 | end 18 | end 19 | end 20 | end 21 | -------------------------------------------------------------------------------- /lib/reactive-ruby/serializers.rb: -------------------------------------------------------------------------------- 1 | [FalseClass, Float, Integer, NilClass, String, Symbol, Time, TrueClass].each do |klass| 2 | klass.send(:define_method, :react_serializer) do 3 | as_json 4 | end 5 | end 6 | 7 | if Gem::Version.new(RUBY_VERSION) < Gem::Version.new('2.4.0') 8 | [Bignum, Fixnum].each do |klass| 9 | klass.send(:define_method, :react_serializer) do 10 | as_json 11 | end 12 | end 13 | end 14 | 15 | BigDecimal.send(:define_method, :react_serializer) { as_json } rescue nil 16 | 17 | Array.send(:define_method, :react_serializer) do 18 | self.collect { |e| e.react_serializer }.as_json 19 | end 20 | 21 | Hash.send(:define_method, :react_serializer) do 22 | Hash[*self.collect { |key, value| [key, value.react_serializer] }.flatten(1)].as_json 23 | end 24 | -------------------------------------------------------------------------------- /lib/reactive-ruby/server_rendering/contextual_renderer.rb: -------------------------------------------------------------------------------- 1 | module ReactiveRuby 2 | module ServerRendering 3 | def self.context_instance_name 4 | '@context' 5 | end 6 | 7 | def self.context_instance_for(context) 8 | context.instance_variable_get(context_instance_name) 9 | end 10 | 11 | class ContextualRenderer < React::ServerRendering::BundleRenderer 12 | def initialize(options = {}) 13 | super(options) 14 | ComponentLoader.new(v8_context).load 15 | end 16 | 17 | def before_render(*args) 18 | # the base class clears the log history... we don't want that as it is taken 19 | # care of in IsomorphicHelpers.load_context 20 | end 21 | 22 | def render(component_name, props, prerender_options) 23 | if prerender_options.is_a?(Hash) 24 | if !v8_runtime? && prerender_options[:context_initializer] 25 | raise React::ServerRendering::PrerenderError.new(component_name, props, "you must use 'mini_racer' with the prerender[:context] option") unless v8_runtime? 26 | else 27 | prerender_options[:context_initializer].call v8_context 28 | prerender_options = prerender_options[:static] ? :static : true 29 | end 30 | end 31 | 32 | super(component_name, props, prerender_options) 33 | end 34 | 35 | private 36 | 37 | def v8_runtime? 38 | ExecJS.runtime.name == 'mini_racer (V8)' 39 | end 40 | 41 | def v8_context 42 | @v8_context ||= ReactiveRuby::ServerRendering.context_instance_for(@context) 43 | end 44 | end 45 | end 46 | end 47 | -------------------------------------------------------------------------------- /lib/reactive-ruby/server_rendering/hyper_asset_container.rb: -------------------------------------------------------------------------------- 1 | require 'react/server_rendering/environment_container' 2 | require 'react/server_rendering/manifest_container' 3 | require 'react/server_rendering/webpacker_manifest_container' 4 | 5 | module ReactiveRuby 6 | module ServerRendering 7 | class HyperTestAssetContainer 8 | def find_asset(logical_path) 9 | ::Rails.cache.read(logical_path) 10 | end 11 | end 12 | 13 | class HyperAssetContainer 14 | def initialize 15 | @ass_containers = [] 16 | if assets_precompiled? 17 | @ass_containers << React::ServerRendering::ManifestContainer.new if React::ServerRendering::ManifestContainer.compatible? 18 | else 19 | @ass_containers << React::ServerRendering::EnvironmentContainer.new if ::Rails.application.assets 20 | end 21 | if React::ServerRendering::WebpackerManifestContainer.compatible? 22 | @ass_containers << React::ServerRendering::WebpackerManifestContainer.new 23 | end 24 | @ass_containers << HyperTestAssetContainer.new if ::Rails.env.test? 25 | end 26 | 27 | def find_asset(logical_path) 28 | @ass_containers.each do |ass| 29 | begin 30 | asset = ass.find_asset(logical_path) 31 | return asset if asset && asset != '' 32 | rescue 33 | next # no asset found, try the next container 34 | end 35 | end 36 | raise "No asset found for #{logical_path}, tried: #{@ass_containers.map { |c| c.class.name }.join(', ')}" 37 | end 38 | 39 | private 40 | 41 | def assets_precompiled? 42 | !::Rails.application.config.assets.compile 43 | end 44 | end 45 | end 46 | end -------------------------------------------------------------------------------- /lib/reactive-ruby/version.rb: -------------------------------------------------------------------------------- 1 | module React 2 | VERSION = '1.0.0.lap28' 3 | end 4 | -------------------------------------------------------------------------------- /lib/reactrb/auto-import.rb: -------------------------------------------------------------------------------- 1 | # rubocop:disable Style/FileName 2 | # require 'reactrb/auto-import' to automatically 3 | # import JS libraries and components when they are detected 4 | if RUBY_ENGINE == 'opal' 5 | # modifies const and method_missing so that they will attempt 6 | # to auto import native libraries and components using React::NativeLibrary 7 | class Object 8 | class << self 9 | alias _reactrb_original_const_missing const_missing 10 | alias _reactrb_original_method_missing method_missing 11 | 12 | def const_missing(const_name) 13 | # Opal uses const_missing to initially define things, 14 | # so we always call the original, and respond to the exception 15 | _reactrb_original_const_missing(const_name) 16 | rescue StandardError => e 17 | React::NativeLibrary.import_const_from_native(Object, const_name, true) || raise(e) 18 | end 19 | 20 | def method_missing(method, *args, &block) 21 | component_class = React::NativeLibrary.import_const_from_native(self, method, false) 22 | _reactrb_original_method_missing(method, *args, &block) unless component_class 23 | React::RenderingContext.render(component_class, *args, &block) 24 | end 25 | end 26 | end 27 | end 28 | -------------------------------------------------------------------------------- /logo1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ruby-hyperloop/hyper-react/579326fd76ee09c38742b4c26160ae2106581671/logo1.png -------------------------------------------------------------------------------- /logo2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ruby-hyperloop/hyper-react/579326fd76ee09c38742b4c26160ae2106581671/logo2.png -------------------------------------------------------------------------------- /logo3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ruby-hyperloop/hyper-react/579326fd76ee09c38742b4c26160ae2106581671/logo3.png -------------------------------------------------------------------------------- /path_release_steps.md: -------------------------------------------------------------------------------- 1 | 2 | For example assuming you are releasing fix to 0.8.18 3 | 4 | 1. Checkout 0-8-stable 5 | 2. Update tests, fix the bug and commit the changes. 6 | 3. Build & Release to RubyGems (Remember the version in version.rb should already be 0.8.19) 7 | 4. Create a tag 'v0.8.19' pointing to that commit. 8 | 5. Bump the version in 0-8-stable to 0.8.20 so it will be ready for the next patch level release. 9 | 6. Commit the version bump, and do a `git push --tags` so the new tag goes up 10 | -------------------------------------------------------------------------------- /spec/controller_helper_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | require 'rails-controller-testing' 4 | Rails::Controller::Testing.install 5 | 6 | class TestController < ActionController::Base; end 7 | 8 | RSpec.describe TestController, type: :controller do 9 | render_views 10 | 11 | describe '#render_component' do 12 | controller do 13 | 14 | layout "test_layout" 15 | 16 | def index 17 | render_component 18 | end 19 | 20 | def new 21 | render_component "Index", {}, layout: :explicit_layout 22 | end 23 | end 24 | 25 | it 'renders with the default layout' do 26 | get :index 27 | expect(response).to render_template(layout: :test_layout) 28 | end 29 | 30 | it "renders with a specified layout" do 31 | get :new 32 | expect(response).to render_template(layout: :explicit_layout) 33 | end 34 | end 35 | end 36 | -------------------------------------------------------------------------------- /spec/index.html.erb: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | <%= javascript_include_tag 'vendor/es5-shim.min' %> 8 | <%= javascript_include_tag 'vendor/jquery-2.2.4.min' %> 9 | <%= javascript_include_tag @server.main %> 10 | 11 | 12 | -------------------------------------------------------------------------------- /spec/react/builtin_tags_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe 'redefining builtin tags', js: true do 4 | it "built in tags can be redefined" do 5 | mount 'Foo' do 6 | React::Component::Tags.remove_method :DIV 7 | React::Component::Tags.send(:remove_const, :DIV) 8 | 9 | class React::Component::Tags::DIV < Hyperloop::Component 10 | others :opts 11 | render do 12 | present :div, params.opts, data: {render_time: Time.now}, &children 13 | end 14 | end 15 | 16 | class Foo < Hyperloop::Component 17 | render(DIV, id: :tp) do 18 | "hello" 19 | end 20 | end 21 | end 22 | expect(Time.parse(find('#tp')['data-render-time'])).to be <= Time.now 23 | expect(page).to have_content('hello') 24 | end 25 | end 26 | -------------------------------------------------------------------------------- /spec/react/callbacks_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe 'React::Callbacks', js: true do 4 | it 'defines callback' do 5 | on_client do 6 | class Foo 7 | include React::Callbacks 8 | define_callback :before_dinner 9 | before_dinner :wash_hands 10 | 11 | def wash_hands;end 12 | end 13 | end 14 | 15 | expect_evaluate_ruby do 16 | instance = Foo.new 17 | [ instance.respond_to?(:wash_hands), instance.run_callback(:before_dinner) ] 18 | end.to eq([true, ["wash_hands"]]) 19 | end 20 | 21 | it 'defines multiple callbacks' do 22 | on_client do 23 | class Foo 24 | include React::Callbacks 25 | define_callback :before_dinner 26 | before_dinner :wash_hands, :turn_off_laptop 27 | 28 | def wash_hands;end 29 | def turn_off_laptop;end 30 | end 31 | end 32 | expect_evaluate_ruby do 33 | instance = Foo.new 34 | [ instance.respond_to?(:wash_hands), 35 | instance.respond_to?(:turn_off_laptop), 36 | instance.run_callback(:before_dinner) ] 37 | end.to eq([true, true, ["wash_hands", "turn_off_laptop" ]]) 38 | end 39 | 40 | context 'using Hyperloop::Context.reset!' do 41 | #after(:all) do 42 | # Hyperloop::Context.instance_variable_set(:@context, nil) 43 | #end 44 | it 'clears callbacks on Hyperloop::Context.reset!' do 45 | on_client do 46 | Hyperloop::Context.reset! 47 | 48 | class Foo 49 | include React::Callbacks 50 | define_callback :before_dinner 51 | 52 | before_dinner :wash_hands, :turn_off_laptop 53 | 54 | def wash_hands;end 55 | 56 | def turn_off_laptop;end 57 | end 58 | end 59 | expect_evaluate_ruby do 60 | instance = Foo.new 61 | 62 | Hyperloop::Context.reset! 63 | 64 | Foo.class_eval do 65 | before_dinner :wash_hands 66 | end 67 | 68 | instance.run_callback(:before_dinner) 69 | end.to eq(["wash_hands"]) 70 | end 71 | end 72 | 73 | it 'defines block callback' do 74 | on_client do 75 | class Foo 76 | include React::Callbacks 77 | attr_accessor :a 78 | attr_accessor :b 79 | 80 | define_callback :before_dinner 81 | 82 | before_dinner do 83 | self.a = 10 84 | end 85 | before_dinner do 86 | self.b = 20 87 | end 88 | end 89 | end 90 | expect_evaluate_ruby do 91 | foo = Foo.new 92 | foo.run_callback(:before_dinner) 93 | [ foo.a, foo.b ] 94 | end.to eq([10, 20]) 95 | end 96 | 97 | it 'defines multiple callback group' do 98 | on_client do 99 | class Foo 100 | include React::Callbacks 101 | define_callback :before_dinner 102 | define_callback :after_dinner 103 | attr_accessor :a 104 | 105 | before_dinner do 106 | self.a = 10 107 | end 108 | end 109 | end 110 | expect_evaluate_ruby do 111 | foo = Foo.new 112 | foo.run_callback(:before_dinner) 113 | foo.run_callback(:after_dinner) 114 | foo.a 115 | end.to eq(10) 116 | end 117 | 118 | it 'receives args as callback' do 119 | on_client do 120 | class Foo 121 | include React::Callbacks 122 | define_callback :before_dinner 123 | define_callback :after_dinner 124 | 125 | attr_accessor :lorem 126 | 127 | before_dinner do |a, b| 128 | self.lorem = "#{a}-#{b}" 129 | end 130 | 131 | after_dinner :eat_ice_cream 132 | def eat_ice_cream(a,b,c); end 133 | end 134 | end 135 | expect_evaluate_ruby do 136 | foo = Foo.new 137 | foo.run_callback(:before_dinner, 1, 2) 138 | res1 = foo.run_callback(:after_dinner, 4, 5, 6) 139 | [res1, foo.lorem] 140 | end.to eq([["eat_ice_cream"], '1-2']) 141 | end 142 | end 143 | -------------------------------------------------------------------------------- /spec/react/children_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe 'React::Children', js: true do 4 | describe 'with multiple child elements' do 5 | before :each do 6 | on_client do 7 | class InitTest 8 | def self.get_children 9 | component = Class.new do 10 | include React::Component 11 | def render 12 | div { 'lorem' } 13 | end 14 | end 15 | childs = [ React.create_element('a'), React.create_element('li') ] 16 | element = React.create_element(component) { childs } 17 | el_children = element.to_n.JS[:props].JS[:children] 18 | children = React::Children.new(el_children) 19 | dom_el = JS.call(:eval, "document.body.appendChild(document.createElement('div'))") 20 | React.render(element, dom_el) 21 | children 22 | end 23 | end 24 | end 25 | end 26 | 27 | it 'is enumerable' do 28 | expect_evaluate_ruby do 29 | InitTest.get_children.map { |elem| elem.element_type } 30 | end.to eq(['a', 'li']) 31 | end 32 | 33 | it 'returns an Enumerator when not providing a block' do 34 | expect_evaluate_ruby do 35 | nodes = InitTest.get_children.each 36 | [nodes.class.name, nodes.size] 37 | end.to eq(["Enumerator", 2]) 38 | end 39 | 40 | describe '#each' do 41 | it 'returns an array of elements' do 42 | expect_evaluate_ruby do 43 | nodes = InitTest.get_children.each { |elem| elem.element_type } 44 | [nodes.class.name, nodes.map(&:class)] 45 | end.to eq(["Array", ["React::Element", "React::Element"]]) 46 | end 47 | end 48 | 49 | describe '#length' do 50 | it 'returns the number of child elements' do 51 | expect_evaluate_ruby do 52 | InitTest.get_children.length 53 | end.to eq(2) 54 | end 55 | end 56 | end 57 | 58 | describe 'with single child element' do 59 | before :each do 60 | on_client do 61 | class InitTest 62 | def self.get_children 63 | component = Class.new do 64 | include React::Component 65 | def render 66 | div { 'lorem' } 67 | end 68 | end 69 | childs = [ React.create_element('a') ] 70 | element = React.create_element(component) { childs } 71 | el_children = element.to_n.JS[:props].JS[:children] 72 | children = React::Children.new(el_children) 73 | dom_el = JS.call(:eval, "document.body.appendChild(document.createElement('div'))") 74 | React.render(element, dom_el) 75 | children 76 | end 77 | end 78 | end 79 | end 80 | 81 | it 'is enumerable containing single element' do 82 | expect_evaluate_ruby do 83 | InitTest.get_children.map { |elem| elem.element_type } 84 | end.to eq(["a"]) 85 | end 86 | 87 | describe '#length' do 88 | it 'returns the number of child elements' do 89 | expect_evaluate_ruby do 90 | InitTest.get_children.length 91 | end.to eq(1) 92 | end 93 | end 94 | end 95 | 96 | describe 'with no child element' do 97 | before :each do 98 | on_client do 99 | class InitTest 100 | def self.get_children 101 | component = Class.new do 102 | include React::Component 103 | def render 104 | div { 'lorem' } 105 | end 106 | end 107 | element = React.create_element(component) 108 | el_children = element.to_n.JS[:props].JS[:children] 109 | children = React::Children.new(el_children) 110 | dom_el = JS.call(:eval, "document.body.appendChild(document.createElement('div'))") 111 | React.render(element, dom_el) 112 | children 113 | end 114 | end 115 | end 116 | end 117 | 118 | it 'is enumerable containing no elements' do 119 | expect_evaluate_ruby do 120 | InitTest.get_children.map { |elem| elem.element_type } 121 | end.to eq([]) 122 | end 123 | 124 | describe '#length' do 125 | it 'returns the number of child elements' do 126 | expect_evaluate_ruby do 127 | InitTest.get_children.length 128 | end.to eq(0) 129 | end 130 | end 131 | end 132 | 133 | describe 'other methods' do 134 | it 'responds to to_proc' do 135 | mount 'Children' do 136 | class ChildTester < Hyperloop::Component 137 | render do 138 | DIV(id: :tp, &children) 139 | end 140 | end 141 | class Children < Hyperloop::Component 142 | render do 143 | ChildTester { "one".span; "two".span; "three".span } 144 | end 145 | end 146 | end 147 | expect(page).to have_content('one') 148 | expect(page).to have_content('two') 149 | expect(page).to have_content('three') 150 | end 151 | it 'responds to render' do 152 | mount 'Children' do 153 | class ChildTester < Hyperloop::Component 154 | render do 155 | DIV(id: :tp) { children.render } 156 | end 157 | end 158 | class Children < Hyperloop::Component 159 | render do 160 | ChildTester { "one".span; "two".span; "three".span } 161 | end 162 | end 163 | end 164 | expect(page).to have_content('one') 165 | expect(page).to have_content('two') 166 | expect(page).to have_content('three') 167 | end 168 | end 169 | end 170 | -------------------------------------------------------------------------------- /spec/react/component/base_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe 'React::Component::Base', js: true do 4 | 5 | before :each do 6 | on_client do 7 | class Foo < React::Component::Base 8 | before_mount do 9 | @instance_data = ["working"] 10 | end 11 | def render 12 | @instance_data.first 13 | end 14 | end 15 | end 16 | end 17 | 18 | it 'can create a simple component class' do 19 | mount 'Foo' 20 | expect(page.body[-50..-19]).to match(/working<\/span>/) 21 | end 22 | 23 | it 'can create a simple component class that can be inherited to create another component class' do 24 | mount 'Bar' do 25 | class Bar < Foo 26 | before_mount do 27 | @instance_data << "well" 28 | end 29 | def render 30 | @instance_data.join(" ") 31 | end 32 | end 33 | end 34 | expect(page.body[-50..-19]).to match(/working well<\/span>/) 35 | end 36 | end 37 | -------------------------------------------------------------------------------- /spec/react/element_spec.rb: -------------------------------------------------------------------------------- 1 | require "spec_helper" 2 | 3 | describe 'React::Element', js: true do 4 | it 'bridges `type` of native React.Element attributes' do 5 | expect_evaluate_ruby do 6 | element = React.create_element('div') 7 | element.element_type 8 | end.to eq("div") 9 | end 10 | 11 | it 'is renderable' do 12 | expect_evaluate_ruby do 13 | element = React.create_element('span') 14 | div = JS.call(:eval, 'document.createElement("div")') 15 | React.render(element, div) 16 | div.JS[:children].JS[0].JS[:tagName] 17 | end.to eq("SPAN") 18 | end 19 | 20 | describe "Event Subscription" do 21 | it "keeps the original params" do 22 | client_option render_on: :both 23 | mount 'Foo' do 24 | class Foo 25 | include React::Component 26 | def render 27 | INPUT(value: nil, type: 'text').on(:change) {} 28 | end 29 | end 30 | end 31 | expect(page.body[-285..-233]).to match(//) 32 | end 33 | end 34 | 35 | describe 'Component Event Subscription' do 36 | 37 | it 'will subscribe to a component event param' do 38 | evaluate_ruby do 39 | class Foo < React::Component::Base 40 | param :on_event, type: Proc, default: nil, allow_nil: true 41 | def render 42 | params.on_event 43 | end 44 | end 45 | React::Test::Utils.render_into_document(React.create_element(Foo).on(:event) {'works!'}) 46 | end 47 | expect(page.body[-50..-19]).to include('works!') 48 | end 49 | 50 | it 'will subscribe to multiple component event params' do 51 | evaluate_ruby do 52 | class Foo < React::Component::Base 53 | param :on_event1, type: Proc, default: nil, allow_nil: true 54 | param :on_event2, type: Proc, default: nil, allow_nil: true 55 | def render 56 | params.on_event1+params.on_event2 57 | end 58 | end 59 | React::Test::Utils.render_into_document(React.create_element(Foo).on(:event1, :event2) {'works!'}) 60 | end 61 | expect(page.body[-60..-19]).to include('works!works!') 62 | end 63 | 64 | it 'will subscribe to a native components event param' do 65 | 66 | evaluate_ruby do 67 | "this makes sure everything is loaded" 68 | end 69 | page.execute_script('window.NativeComponent = class extends React.Component { 70 | constructor(props) { 71 | super(props); 72 | this.displayName = "HelloMessage"; 73 | } 74 | render() { return React.createElement("span", null, this.props.onEvent()); } 75 | }') 76 | evaluate_ruby do 77 | class Foo < React::Component::Base 78 | imports "NativeComponent" 79 | end 80 | React::Test::Utils.render_into_document(React.create_element(Foo).on(:event) {'works!'}) 81 | true 82 | end 83 | expect(page.body[-60..-19]).to include('works!') 84 | end 85 | 86 | it 'will subscribe to a component event param with a non-default name' do 87 | 88 | evaluate_ruby do 89 | class Foo < React::Component::Base 90 | param :my_event, type: Proc, default: nil, allow_nil: true 91 | def render 92 | params.my_event 93 | end 94 | end 95 | React::Test::Utils.render_into_document(React.create_element(Foo).on("") {'works!'}) 96 | end 97 | expect(page.body[-60..-19]).to include('works!') 98 | end 99 | end 100 | 101 | describe 'Builtin Event subscription' do 102 | it 'is subscribable through `on(:event_name)` method' do 103 | expect_evaluate_ruby do 104 | element = React.create_element("div").on(:click) { |event| RESULT_C = 'clicked' if event.is_a? React::Event } 105 | dom_node = React::Test::Utils.render_into_document(element) 106 | React::Test::Utils.simulate_click(dom_node) 107 | RESULT_C rescue 'not clicked' 108 | end.to eq('clicked') 109 | 110 | expect_evaluate_ruby do 111 | element = React.create_element("div").on(:key_down) { |event| RESULT_P = 'pressed' if event.is_a? React::Event } 112 | dom_node = React::Test::Utils.render_into_document(element) 113 | React::Test::Utils.simulate_keydown(dom_node, "Enter") 114 | RESULT_P rescue 'not pressed' 115 | end.to eq('pressed') 116 | 117 | expect_evaluate_ruby do 118 | element = React.create_element("form").on(:submit) { |event| RESULT_S = 'submitted' if event.is_a? React::Event } 119 | dom_node = React::Test::Utils.render_into_document(element) 120 | React::Test::Utils.simulate_submit(dom_node) 121 | RESULT_S rescue 'not submitted' 122 | end.to eq('submitted') 123 | end 124 | 125 | it 'returns self for `on` method' do 126 | expect_evaluate_ruby do 127 | element = React.create_element("div") 128 | element.on(:click){} == element 129 | end.to be_truthy 130 | end 131 | end 132 | end 133 | -------------------------------------------------------------------------------- /spec/react/event_spec.rb: -------------------------------------------------------------------------------- 1 | require "spec_helper" 2 | 3 | describe 'React::Event', js: true do 4 | it "should bridge attributes of native SyntheticEvent (see http://facebook.github.io/react/docs/events.html#syntheticevent)" do 5 | expect_evaluate_ruby do 6 | results = {} 7 | element = React.create_element('div').on(:click) do |event| 8 | results[:bubbles] = event.bubbles == event.to_n.JS[:bubbles] 9 | results[:cancelable] = event.cancelable == event.to_n.JS[:cancelable] 10 | results[:current_target] = event.current_target == event.to_n.JS[:currentTarget] 11 | results[:default_prevented] = event.default_prevented == event.to_n.JS[:defaultPrevented] 12 | results[:event_phase] = event.event_phase == event.to_n.JS[:eventPhase] 13 | results[:is_trusted?] = event.is_trusted? == event.to_n.JS[:isTrusted] 14 | results[:native_event] = event.native_event == event.to_n.JS[:nativeEvent] 15 | results[:target] = event.target == event.to_n.JS[:target] 16 | results[:timestamp] = event.timestamp == event.to_n.JS[:timeStamp] 17 | results[:event_type] = event.event_type == event.to_n.JS[:type] 18 | results[:prevent_default] = event.respond_to?(:prevent_default) 19 | results[:stop_propagation] = event.respond_to?(:stop_propagation) 20 | end 21 | dom_node = React::Test::Utils.render_into_document(element) 22 | React::Test::Utils.simulate_click(dom_node) 23 | results 24 | end.to eq({ 25 | 'bubbles' => true, 26 | 'cancelable' => true, 27 | 'current_target' => true, 28 | 'default_prevented' => true, 29 | 'event_phase' => true, 30 | 'is_trusted?' => true, 31 | 'native_event' => true, 32 | 'target' => true, 33 | 'timestamp' => true, 34 | 'event_type' => true, 35 | 'prevent_default' => true, 36 | 'stop_propagation' => true 37 | }) 38 | end 39 | end 40 | -------------------------------------------------------------------------------- /spec/react/observable_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe 'React::Observable', js: true do 4 | it "allows to set value on Observable" do 5 | mount 'Foo' do 6 | class Zoo 7 | include React::Component 8 | param :foo, type: React::Observable 9 | before_mount do 10 | params.foo! 4 11 | end 12 | 13 | def render 14 | nil 15 | end 16 | end 17 | 18 | class Foo 19 | include React::Component 20 | 21 | def render 22 | div do 23 | Zoo(foo: state.foo! ) 24 | span { state.foo.to_s } 25 | end 26 | end 27 | end 28 | end 29 | expect(page.body[-60..-19]).to include('4') 30 | end 31 | end 32 | -------------------------------------------------------------------------------- /spec/react/opal_jquery_extensions_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe 'opal-jquery extensions', js: true do 4 | describe 'Element' do 5 | xit 'will reuse the wrapper component class for the same Element' do 6 | evaluate_ruby do 7 | class Foo < React::Component::Base 8 | param :name 9 | def render 10 | "hello #{params.name}" 11 | end 12 | 13 | def self.rec_cnt 14 | @@rec_cnt ||= 0 15 | end 16 | before_unmount do 17 | @@rec_cnt ||= 0 18 | @@rec_cnt += 1 19 | end 20 | end 21 | end 22 | expect_evaluate_ruby do 23 | test_div = Element.new(:div) 24 | test_div.render { Foo(name: 'fred') } 25 | test_div.render { Foo(name: 'freddy') } 26 | [ Element[test_div].find('span').html, Foo.rec_cnt] 27 | end.to eq(['hello freddy', 0]) 28 | end 29 | 30 | it 'renders a top level component using render with a block' do 31 | expect_evaluate_ruby do 32 | class Foo < React::Component::Base 33 | param :name 34 | def render 35 | "hello #{params.name}" 36 | end 37 | end 38 | test_div = Element.new(:div) 39 | test_div.render { Foo(name: 'fred') } 40 | Element[test_div].find('span').html 41 | end.to eq('hello fred') 42 | end 43 | 44 | it 'renders a top level component using render with a container and params ' do 45 | expect_evaluate_ruby do 46 | test_div = Element.new(:div) 47 | test_div.render(:span, id: :render_test_span) { 'hello' } 48 | Element[test_div].find('#render_test_span').html 49 | end.to eq('hello') 50 | end 51 | 52 | it 'will find the DOM node given a react element' do 53 | expect_evaluate_ruby do 54 | class Foo < React::Component::Base 55 | def render 56 | div { 'hello' } 57 | end 58 | end 59 | Element[React::Test::Utils.render_component_into_document(Foo)].html 60 | end.to eq('hello') 61 | end 62 | 63 | it "accepts plain js object as selector" do 64 | evaluate_ruby do 65 | Element[JS.call(:eval, "(function () { return window; })();")] 66 | end 67 | expect(page.driver.browser.manage.logs.get(:browser).map { |m| m.message.gsub(/\\n/, "\n") }.to_a.join("\n")) 68 | .not_to match(/Exception|Error/) 69 | end 70 | 71 | it "can dynamically mount components" do 72 | on_client do 73 | class DynoMount < Hyperloop::Component 74 | render(DIV) { 'I got rendered' } 75 | end 76 | end 77 | mount 'MountPoint' do 78 | class MountPoint < Hyperloop::Component 79 | render(DIV) do 80 | # simulate what react-rails render_component output 81 | DIV( 82 | 'data-react-class' => 'React.TopLevelRailsComponent', 83 | 'data-react-props' => '{"render_params": {}, "component_name": "DynoMount", "controller": ""}' 84 | ) 85 | end 86 | end 87 | end 88 | evaluate_ruby do 89 | Element['body'].mount_components 90 | end 91 | expect(page).to have_content('I got rendered') 92 | end 93 | end 94 | end 95 | -------------------------------------------------------------------------------- /spec/react/refs_callback_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe 'Refs callback', js: true do 4 | before do 5 | on_client do 6 | class Foo 7 | include React::Component 8 | def self.bar 9 | @@bar 10 | end 11 | def self.bar=(club) 12 | @@bar = club 13 | end 14 | end 15 | end 16 | end 17 | 18 | it "is invoked with the actual Ruby instance" do 19 | expect_evaluate_ruby do 20 | class Bar 21 | include React::Component 22 | def render 23 | React.create_element('div') 24 | end 25 | end 26 | 27 | Foo.class_eval do 28 | def my_bar=(bars) 29 | Foo.bar = bars 30 | end 31 | 32 | def render 33 | React.create_element(Bar, ref: method(:my_bar=).to_proc) 34 | end 35 | end 36 | 37 | element = React.create_element(Foo) 38 | React::Test::Utils.render_into_document(element) 39 | begin 40 | "#{Foo.bar.class.name}" 41 | rescue 42 | "Club" 43 | end 44 | end.to eq('Bar') 45 | end 46 | 47 | it "is invoked with the actual DOM node" do 48 | # client_option raise_on_js_errors: :off 49 | expect_evaluate_ruby do 50 | Foo.class_eval do 51 | def my_div=(div) 52 | Foo.bar = div 53 | end 54 | 55 | def render 56 | React.create_element('div', ref: method(:my_div=).to_proc) 57 | end 58 | end 59 | 60 | element = React.create_element(Foo) 61 | React::Test::Utils.render_into_document(element) 62 | "#{Foo.bar.JS['nodeType']}" # avoids json serialisation errors by using "#{}" 63 | end.to eq("1") 64 | end 65 | 66 | it "works, even when the component is unmounted" do 67 | # was a bug, on unmount react calls the ref method with null instead of a dom node 68 | # callback failed then 69 | # ref is called two times, once on mount with dom_node, once on unmount with null 70 | mount "Foo" do 71 | class Unmountable < Hyperloop::Component 72 | render do 73 | DIV { "This is a Component" } 74 | end 75 | end 76 | Foo.class_eval do 77 | def ref_rec(dom_node) 78 | @@rec_cnt ||= 0 79 | @@rec_cnt += 1 80 | end 81 | def self.rec_cnt 82 | @@rec_cnt 83 | end 84 | 85 | after_mount { mutate.unmount true } 86 | 87 | render do 88 | Unmountable(ref: method(:ref_rec).to_proc) unless state.unmount 89 | end 90 | end 91 | end 92 | expect_evaluate_ruby('Foo.rec_cnt').to eq(2) 93 | end 94 | end 95 | -------------------------------------------------------------------------------- /spec/react/server_spec.rb: -------------------------------------------------------------------------------- 1 | require "spec_helper" 2 | 3 | describe 'React::Server', js: true do 4 | 5 | describe "render_to_string" do 6 | it "should render a React.Element to string" do 7 | client_option render_on: :both 8 | expect_evaluate_ruby do 9 | ele = React.create_element('span') { "lorem" } 10 | React::Server.render_to_string(ele).class.name 11 | end.to eq("String") 12 | end 13 | end 14 | 15 | describe "render_to_static_markup" do 16 | it "should render a React.Element to static markup" do 17 | client_option render_on: :both 18 | expect_evaluate_ruby do 19 | ele = React.create_element('span') { "lorem" } 20 | React::Server.render_to_static_markup(ele) 21 | end.to eq("lorem") 22 | end 23 | end 24 | end 25 | 26 | -------------------------------------------------------------------------------- /spec/react/state_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe 'React::State', js: true do 4 | it "can create dynamically initialized exported states" do 5 | expect_evaluate_ruby do 6 | class Foo 7 | include React::Component 8 | export_state(:foo) { 'bar' } 9 | end 10 | Hyperloop::Application::Boot.run 11 | Foo.foo 12 | end.to eq('bar') 13 | end 14 | 15 | # these will all require async operations and testing to see if things get 16 | # re-rendered see spec_helper the "render" test method 17 | 18 | # if Foo.foo is used during rendering then when Foo.foo changes we will 19 | # rerender 20 | it "sets up observers when exported states are read" 21 | 22 | # React::State.set_state(object, attribute, value) + 23 | # React::State.get_state(object, attribute) 24 | it "can be accessed outside of react using get/set_state" 25 | 26 | it 'ignores state updates during rendering' do 27 | client_option render_on: :both 28 | evaluate_ruby do 29 | class StateTest < React::Component::Base 30 | export_state :boom 31 | before_mount do 32 | # force boom to be on the observing list during the current rendering cycle 33 | StateTest.boom! !StateTest.boom 34 | # this is automatically called by after_mount / after_update, but we don't want 35 | # to have to setup a complicated async test, so we just force it now. 36 | # if we don't do this, then updating boom will have no effect on the first render 37 | React::State.update_states_to_observe 38 | end 39 | def render 40 | (StateTest.boom ? "Boom" : "No Boom").tap { StateTest.boom! !StateTest.boom } 41 | end 42 | end 43 | MARKUP = React::Server.render_to_static_markup(React.create_element(StateTest)) 44 | end 45 | expect_evaluate_ruby("MARKUP").to eq('Boom') 46 | expect_evaluate_ruby("StateTest.boom").to be_falsy 47 | expect(page.driver.browser.manage.logs.get(:browser).reject { |entry| 48 | entry_s = entry.to_s 49 | entry_s.include?("Deprecated feature") || entry_s.include?("Mount() on the server. This is a no-op.") 50 | }.size).to eq(0) 51 | end 52 | end 53 | -------------------------------------------------------------------------------- /spec/react/test/dsl_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | if RUBY_ENGINE == 'opal' 4 | RSpec.describe React::Test::DSL do 5 | describe 'the DSL' do 6 | let(:session) { Class.new { include React::Test::DSL }.new } 7 | 8 | before do 9 | React::Test.reset_session! 10 | 11 | stub_const 'Greeter', Class.new 12 | Greeter.class_eval do 13 | include React::Component 14 | 15 | params do 16 | optional :message 17 | optional :from 18 | end 19 | 20 | def render 21 | span { "Hello #{params.message}" } 22 | end 23 | end 24 | end 25 | 26 | it 'is possible to include it in another class' do 27 | session.mount(Greeter) 28 | expect(session.instance).to be_a(Greeter) 29 | end 30 | 31 | it "should provide a 'component' shortcut for more expressive tests" do 32 | session.component.mount(Greeter) 33 | expect(session.component.instance).to be_a(Greeter) 34 | end 35 | 36 | React::Test::Session::DSL_METHODS.each do |method| 37 | it "responds to all DSL method: #{method}" do 38 | expect(session).to respond_to(method) 39 | end 40 | end 41 | end 42 | end 43 | end 44 | -------------------------------------------------------------------------------- /spec/react/test/matchers/render_html_matcher_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | if RUBY_ENGINE == 'opal' 4 | describe React::Test::Matchers::RenderHTMLMatcher do 5 | let(:component) { 6 | Class.new do 7 | include React::Component 8 | params do 9 | optional :string 10 | end 11 | def render 12 | div do 13 | span { params.string } if params.string 14 | 'lorem' 15 | end 16 | end 17 | end 18 | } 19 | let(:expected) { '
lorem
' } 20 | let(:matcher) { described_class.new(expected) } 21 | 22 | describe '#matches?' do 23 | it 'is truthy when rendered component html equals expected html' do 24 | expect(matcher.matches?(component)).to be_truthy 25 | end 26 | 27 | it 'is falsey when rendered component html does not equal expected html' do 28 | matcher = described_class.new('foo') 29 | expect(matcher.matches?(component)).to be_falsey 30 | end 31 | end 32 | 33 | describe '#with_params' do 34 | let(:expected) { '
strlorem
' } 35 | 36 | it 'renders the component with the given params' do 37 | matcher.with_params(string: 'str') 38 | expect(matcher.matches?(component)).to be_truthy 39 | end 40 | end 41 | 42 | describe '#failure_message' do 43 | let(:expected) { '
strlorem
' } 44 | 45 | it 'includes the name of the component' do 46 | stub_const 'Foo', component 47 | matcher.matches?(Foo) 48 | expect(matcher.failure_message).to match(/expected 'Foo'/) 49 | end 50 | 51 | it 'includes the params hash' do 52 | matcher.with_params(string: 'bar') 53 | matcher.matches?(component) 54 | expect(matcher.failure_message).to match(/with params '{"string"=>"bar"}'/) 55 | end 56 | 57 | it 'includes the expected html value' do 58 | matcher.matches?(component) 59 | expect(matcher.failure_message).to match(/to render '#{expected}'/) 60 | end 61 | 62 | it 'includes the actual html value' do 63 | actual = '
lorem<\/div>' 64 | matcher.matches?(component) 65 | expect(matcher.failure_message).to match(/, but '#{actual}' was rendered/) 66 | end 67 | 68 | it 'does not include "to not render"' do 69 | matcher.matches?(component) 70 | expect(matcher.failure_message).to_not match(/to not render/) 71 | end 72 | end 73 | 74 | describe '#negative_failure_message' do 75 | let(:expected) { '
strlorem
' } 76 | 77 | it 'includes "to not render"' do 78 | matcher.matches?(component) 79 | expect(matcher.negative_failure_message).to match(/to not render/) 80 | end 81 | end 82 | end 83 | end 84 | -------------------------------------------------------------------------------- /spec/react/test/rspec_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | if RUBY_ENGINE == 'opal' 4 | RSpec.describe 'react/test/rspec', type: :component do 5 | before do 6 | stub_const 'Greeter', Class.new 7 | Greeter.class_eval do 8 | include React::Component 9 | params do 10 | optional :message 11 | optional :from 12 | end 13 | 14 | def render 15 | span { "Hello #{params.message}" } 16 | end 17 | end 18 | end 19 | 20 | it 'should include react/test in rspec' do 21 | comp = mount(Greeter) 22 | expect(component.instance).to eq(comp) 23 | end 24 | 25 | it 'includes rspec matchers' do 26 | expect(Greeter).to render_static_html( 27 | 'Hello world' 28 | ).with_params(message: 'world') 29 | end 30 | 31 | describe 'resetting the session' do 32 | it 'creates an instance of the mounted component in one example' do 33 | mount(Greeter) 34 | end 35 | 36 | it '...then is not availalbe in the next' do 37 | expect { component.instance }.to raise_error 38 | end 39 | end 40 | end 41 | 42 | RSpec.describe 'react/test/rspec', type: :other do 43 | before do 44 | stub_const 'Greeter', Class.new 45 | Greeter.class_eval do 46 | include React::Component 47 | params do 48 | optional :message 49 | optional :from 50 | end 51 | 52 | def render 53 | span { "Hello #{params.message}" } 54 | end 55 | end 56 | end 57 | 58 | it 'should not include react/test in rspec' do 59 | expect { mount(Greeter) }.to raise_error 60 | end 61 | end 62 | end 63 | -------------------------------------------------------------------------------- /spec/react/test/session_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | if RUBY_ENGINE == 'opal' 4 | RSpec.describe React::Test::Session do 5 | subject { described_class.new } 6 | before do 7 | stub_const 'Greeter', Class.new 8 | Greeter.class_eval do 9 | include React::Component 10 | 11 | params do 12 | optional :message 13 | optional :from 14 | end 15 | 16 | def render 17 | span { "Hello #{params.message}" } 18 | end 19 | end 20 | end 21 | 22 | describe '#mount' do 23 | it 'returns an instance of the mounted component' do 24 | expect(subject.mount(Greeter)).to be_a(Greeter) 25 | end 26 | 27 | it 'actualy mounts the component' do 28 | expect(subject.mount(Greeter)).to be_mounted 29 | end 30 | 31 | it 'optionaly passes params to the component' do 32 | instance = subject.mount(Greeter, message: 'world') 33 | expect(instance.params.message).to eq('world') 34 | end 35 | end 36 | 37 | describe '#instance' do 38 | it 'returns the instance of the mounted component' do 39 | instance = subject.mount(Greeter) 40 | expect(subject.instance).to eq(instance) 41 | end 42 | end 43 | 44 | describe '#html' do 45 | it 'returns the component rendered to static html' do 46 | subject.mount(Greeter, message: 'world') 47 | expect(subject.html).to eq('Hello world') 48 | end 49 | 50 | async 'returns the updated static html' do 51 | subject.mount(Greeter) 52 | subject.update_params(message: 'moon') do 53 | run_async { 54 | expect(subject.html).to eq('Hello moon') 55 | } 56 | end 57 | end 58 | end 59 | 60 | describe '#update_params' do 61 | it 'sends new params to the component' do 62 | instance = subject.mount(Greeter, message: 'world') 63 | subject.update_params(message: 'moon') 64 | expect(instance.params.message).to eq('moon') 65 | end 66 | 67 | it 'leaves unspecified params in tact' do 68 | instance = subject.mount(Greeter, message: 'world', from: 'outerspace') 69 | subject.update_params(message: 'moon') 70 | expect(instance.params.from).to eq('outerspace') 71 | end 72 | 73 | it 'causes the component to render' do 74 | instance = subject.mount(Greeter, message: 'world') 75 | expect(instance).to receive(:render) 76 | subject.update_params(message: 'moon') 77 | end 78 | end 79 | 80 | describe 'instance#force_update!' do 81 | it 'causes the component to render' do 82 | instance = subject.mount(Greeter) 83 | expect(instance).to receive(:render) 84 | subject.instance.force_update! 85 | end 86 | end 87 | end 88 | end 89 | -------------------------------------------------------------------------------- /spec/react/test/utils_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | if RUBY_ENGINE == 'opal' 4 | RSpec.describe React::Test::Utils do 5 | it 'simulates' do 6 | stub_const 'Foo', Class.new 7 | Foo.class_eval do 8 | include React::Component 9 | 10 | def render 11 | div { 'Click Me' }.on(:click) { |e| click(e) } 12 | end 13 | end 14 | 15 | instance = React::Test::Utils.render_into_document(React.create_element(Foo)) 16 | expect(instance).to receive(:click) 17 | described_class.simulate(:click, instance.dom_node) 18 | end 19 | 20 | describe "render_into_document" do 21 | it "works with native element" do 22 | expect { 23 | described_class.render_into_document(React.create_element('div')) 24 | }.to_not raise_error 25 | end 26 | end 27 | end 28 | end 29 | -------------------------------------------------------------------------------- /spec/react/to_key_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe 'to_key helper', js: true do 4 | it "has added 'to_key' method to Object and each key is different" do 5 | expect_evaluate_ruby do 6 | Object.new.to_key != Object.new.to_key 7 | end.to be_truthy 8 | end 9 | 10 | it "to_key return 'self' for String objects" do 11 | expect_evaluate_ruby do 12 | debugger 13 | "hello".to_key == "hello" 14 | end.to be_truthy 15 | end 16 | 17 | it "to_key return 'self' for Number objects" do 18 | expect_evaluate_ruby do 19 | 12.to_key == 12 20 | end.to be_truthy 21 | end 22 | 23 | it "to_key return 'self' for Boolean objects" do 24 | expect_evaluate_ruby do 25 | true.to_key == true && false.to_key == false 26 | end.to be_truthy 27 | end 28 | 29 | it "will use the use the to_key method to get the react key" do 30 | mount "TestComponent" do 31 | class MyTestClass 32 | attr_reader :to_key_called 33 | def to_key 34 | @to_key_called = true 35 | super 36 | end 37 | end 38 | class TestComponent < Hyperloop::Component 39 | before_mount { @test_object = MyTestClass.new } 40 | render do 41 | DIV(key: @test_object) { TestComponent2(test_object: @test_object) } 42 | end 43 | end 44 | class TestComponent2 < Hyperloop::Component 45 | param :test_object 46 | render do 47 | "to key was called!" if params.test_object.to_key_called 48 | end 49 | end 50 | end 51 | expect(page).to have_content('to key was called!') 52 | end 53 | end 54 | -------------------------------------------------------------------------------- /spec/react/top_level_component_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | 4 | describe 'React::TopLevelRailsComponent', js: true do 5 | before :each do 6 | on_client do 7 | module Components 8 | module Controller 9 | class Component1 10 | include React::Component 11 | def render 12 | self.class.name.to_s 13 | end 14 | end 15 | end 16 | 17 | class Component1 18 | include React::Component 19 | def render 20 | self.class.name.to_s 21 | end 22 | end 23 | 24 | class Component2 25 | include React::Component 26 | def render 27 | self.class.name.to_s 28 | end 29 | end 30 | end 31 | 32 | module Controller 33 | class SomeOtherClass # see issue #80 34 | end 35 | end 36 | 37 | class Component1 38 | include React::Component 39 | def render 40 | self.class.name.to_s 41 | end 42 | end 43 | 44 | def render_top_level(controller, component_name) 45 | params = { 46 | controller: controller, 47 | component_name: component_name, 48 | render_params: {} 49 | } 50 | component = React::Test::Utils.render_component_into_document(React::TopLevelRailsComponent, params) 51 | component.dom_node.JS[:outerHTML] 52 | end 53 | end 54 | end 55 | 56 | it 'uses the controller name to lookup a component' do 57 | expect_evaluate_ruby('render_top_level("Controller", "Component1")').to eq('Components::Controller::Component1') 58 | end 59 | 60 | it 'can find the name without matching the controller' do 61 | expect_evaluate_ruby('render_top_level("Controller", "Component2")').to eq('Components::Component2') 62 | end 63 | 64 | it 'will find the outer most matching component' do 65 | expect_evaluate_ruby('render_top_level("OtherController", "Component1")').to eq('Component1') 66 | end 67 | 68 | it 'can find the correct component when the name is fully qualified' do 69 | expect_evaluate_ruby('render_top_level("Controller", "::Components::Component1")').to eq('Components::Component1') 70 | end 71 | 72 | describe '.html_tag?' do 73 | it 'is truthy for valid html tags' do 74 | expect_evaluate_ruby('React.html_tag?("a")').to be_truthy 75 | expect_evaluate_ruby('React.html_tag?("div")').to be_truthy 76 | end 77 | 78 | it 'is truthy for valid svg tags' do 79 | expect_evaluate_ruby('React.html_tag?("svg")').to be_truthy 80 | expect_evaluate_ruby('React.html_tag?("circle")').to be_truthy 81 | end 82 | 83 | it 'is falsey for invalid tags' do 84 | expect_evaluate_ruby('React.html_tag?("tagizzle")').to be_falsey 85 | end 86 | end 87 | 88 | describe '.html_attr?' do 89 | it 'is truthy for valid html attributes' do 90 | expect_evaluate_ruby('React.html_attr?("id")').to be_truthy 91 | expect_evaluate_ruby('React.html_attr?("data")').to be_truthy 92 | end 93 | 94 | it 'is truthy for valid svg attributes' do 95 | expect_evaluate_ruby('React.html_attr?("cx")').to be_truthy 96 | expect_evaluate_ruby('React.html_attr?("strokeWidth")').to be_truthy 97 | end 98 | 99 | it 'is falsey for invalid attributes' do 100 | expect_evaluate_ruby('React.html_tag?("attrizzle")').to be_falsey 101 | end 102 | end 103 | end 104 | -------------------------------------------------------------------------------- /spec/react/tutorial/tutorial_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe 'An Example from the react.rb doc', js: true do 4 | it 'produces the correct result' do 5 | mount 'HelloMessage' do 6 | class HelloMessage 7 | include React::Component 8 | def render 9 | div { "Hello World!" } 10 | end 11 | end 12 | end 13 | expect(page).to have_xpath('//div', text: 'Hello World!') 14 | end 15 | end 16 | 17 | describe 'Adding state to a component (second tutorial example)', js: true do 18 | before :each do 19 | on_client do 20 | class HelloMessage2 21 | include React::Component 22 | define_state(:user_name) { '@catmando' } 23 | def render 24 | div { "Hello #{state.user_name}" } 25 | end 26 | end 27 | end 28 | end 29 | 30 | it "produces the correct result" do 31 | mount 'HelloMessage2' 32 | expect(page).to have_xpath('//div', text: 'Hello @catmando') 33 | end 34 | 35 | it 'renders to the document' do 36 | evaluate_ruby do 37 | ele = JS.call(:eval, "document.body.appendChild(document.createElement('div'))") 38 | React.render(React.create_element(HelloMessage2), ele) 39 | end 40 | expect(page).to have_xpath('//div', text: 'Hello @catmando') 41 | end 42 | end 43 | -------------------------------------------------------------------------------- /spec/react/validator_spec.rb: -------------------------------------------------------------------------------- 1 | require "spec_helper" 2 | 3 | describe 'React::Validator', js: true do 4 | describe '#validate' do 5 | describe "Presence validation" do 6 | it "should check if required props provided" do 7 | evaluate_ruby do 8 | VALIDATOR = React::Validator.new.build do 9 | requires :foo 10 | requires :bar 11 | end 12 | end 13 | expect_evaluate_ruby('VALIDATOR.validate({})').to eq(["Required prop `foo` was not specified", "Required prop `bar` was not specified"]) 14 | expect_evaluate_ruby('VALIDATOR.validate({foo: 1, bar: 3})').to eq([]) 15 | end 16 | 17 | it "should check if passed non specified prop" do 18 | evaluate_ruby do 19 | VALIDATOR = React::Validator.new.build do 20 | optional :foo 21 | end 22 | end 23 | expect_evaluate_ruby('VALIDATOR.validate({bar: 10})').to eq(["Provided prop `bar` not specified in spec"]) 24 | expect_evaluate_ruby('VALIDATOR.validate({foo: 10})').to eq([]) 25 | end 26 | end 27 | 28 | describe "Type validation" do 29 | it "should check if passed value with wrong type" do 30 | evaluate_ruby do 31 | VALIDATOR = React::Validator.new.build do 32 | requires :foo, type: String 33 | end 34 | end 35 | expect_evaluate_ruby('VALIDATOR.validate({foo: 10})').to eq(["Provided prop `foo` could not be converted to String"]) 36 | expect_evaluate_ruby('VALIDATOR.validate({foo: "10"})').to eq([]) 37 | end 38 | 39 | it "should check if passed value with wrong custom type" do 40 | evaluate_ruby do 41 | class Bar; end 42 | VALIDATOR = React::Validator.new.build do 43 | requires :foo, type: Bar 44 | end 45 | end 46 | expect_evaluate_ruby('VALIDATOR.validate({foo: 10})').to eq(["Provided prop `foo` could not be converted to Bar"]) 47 | expect_evaluate_ruby('VALIDATOR.validate({foo: Bar.new})').to eq([]) 48 | end 49 | 50 | it 'coerces native JS prop types to opal objects' do 51 | evaluate_ruby do 52 | VALIDATOR = React::Validator.new.build do 53 | requires :foo, type: JS.call(:eval, "(function () { return { x: 1 }; })();") 54 | end 55 | end 56 | expect_evaluate_ruby('VALIDATOR.validate({foo: `{ x: 1 }`})').to eq(["Provided prop `foo` could not be converted to [object Object]"]) 57 | end 58 | 59 | it 'coerces native JS values to opal objects' do 60 | evaluate_ruby do 61 | VALIDATOR = React::Validator.new.build do 62 | requires :foo, type: Array[Integer] 63 | end 64 | end 65 | expect_evaluate_ruby('VALIDATOR.validate({foo: `[ { x: 1 } ]`})').to eq(["Provided prop `foo`[0] could not be converted to #{Integer.name}"]) 66 | end 67 | 68 | it "should support Array[Class] validation" do 69 | evaluate_ruby do 70 | VALIDATOR = React::Validator.new.build do 71 | requires :foo, type: Array[Hash] 72 | end 73 | end 74 | expect_evaluate_ruby('VALIDATOR.validate({foo: [1,"2",3]})').to eq( 75 | [ 76 | "Provided prop `foo`[0] could not be converted to Hash", 77 | "Provided prop `foo`[1] could not be converted to Hash", 78 | "Provided prop `foo`[2] could not be converted to Hash" 79 | ] 80 | ) 81 | expect_evaluate_ruby('VALIDATOR.validate({foo: [{},{},{}]})').to eq([]) 82 | end 83 | end 84 | 85 | describe "Limited values" do 86 | it "should check if passed value is not one of the specified values" do 87 | evaluate_ruby do 88 | VALIDATOR = React::Validator.new.build do 89 | requires :foo, values: [4,5,6] 90 | end 91 | end 92 | expect_evaluate_ruby('VALIDATOR.validate({foo: 3})').to eq(["Value `3` for prop `foo` is not an allowed value"]) 93 | expect_evaluate_ruby('VALIDATOR.validate({foo: 4})').to eq([]) 94 | end 95 | end 96 | end 97 | 98 | it 'collects other params into a hash' do 99 | evaluate_ruby do 100 | PROPS = { foo: 'foo', bar: 'bar', biz: 'biz', baz: 'baz' } 101 | VALIDATOR = React::Validator.new.build do 102 | requires :foo 103 | optional :bar 104 | all_other_params :baz 105 | end 106 | class Dummy 107 | def props 108 | PROPS 109 | end 110 | end 111 | end 112 | expect_evaluate_ruby('VALIDATOR.validate(PROPS)').to eq([]) 113 | expect_evaluate_ruby('VALIDATOR.props_wrapper.new(Dummy.new).baz').to eq({ "biz" => 'biz', "baz" => 'baz' }) 114 | end 115 | 116 | describe "default_props" do 117 | it "should return specified default values" do 118 | evaluate_ruby do 119 | VALIDATOR = React::Validator.new.build do 120 | requires :foo, default: 10 121 | requires :bar 122 | optional :lorem, default: 20 123 | end 124 | end 125 | expect_evaluate_ruby('VALIDATOR.default_props').to eq({"foo" => 10, "lorem" => 20}) 126 | end 127 | end 128 | end 129 | -------------------------------------------------------------------------------- /spec/reactive-ruby/component_loader_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe ReactiveRuby::ComponentLoader do 4 | GLOBAL_WRAPPER = <<-JS 5 | #{React::ServerRendering::ExecJSRenderer::GLOBAL_WRAPPER} 6 | var console = { 7 | warn: function(s) { } 8 | }; 9 | JS 10 | 11 | let(:js) do 12 | if ::Rails.application.assets['react-server.js'] 13 | react_source = ::Rails.application.assets['react-server.js'] 14 | else 15 | react_source = ::Rails.application.assets['react.js'] 16 | end 17 | ::Rails.application.assets['components'].to_s + react_source.to_s 18 | end 19 | let(:context) { ExecJS.compile(GLOBAL_WRAPPER + js) } 20 | let(:v8_context) { ReactiveRuby::ServerRendering.context_instance_for(context) } 21 | 22 | describe '.new' do 23 | it 'raises a meaningful exception when initialized without a context' do 24 | expect { 25 | described_class.new(nil) 26 | }.to raise_error(/Could not obtain ExecJS runtime context/) 27 | end 28 | end 29 | 30 | describe '#load' do 31 | xit 'loads given asset file into context' do 32 | loader = described_class.new(v8_context) 33 | 34 | expect { 35 | loader.load('components') 36 | }.to change { !!v8_context.eval('Opal.React !== undefined') }.from(false).to(true) 37 | end 38 | 39 | xit 'is truthy upon successful load' do 40 | loader = described_class.new(v8_context) 41 | expect(loader.load('components')).to be_truthy 42 | end 43 | 44 | xit 'fails silently returning false' do 45 | loader = described_class.new(v8_context) 46 | expect(loader.load('foo')).to be_falsey 47 | end 48 | end 49 | 50 | describe '#load!' do 51 | xit 'is truthy upon successful load' do 52 | loader = described_class.new(v8_context) 53 | expect(loader.load!('components')).to be_truthy 54 | end 55 | 56 | xit 'raises an expection if loading fails' do 57 | loader = described_class.new(v8_context) 58 | expect { loader.load!('foo') }.to raise_error(/No HyperReact components/) 59 | end 60 | end 61 | 62 | describe '#loaded?' do 63 | xit 'is truthy if components file is already loaded' do 64 | loader = described_class.new(v8_context) 65 | loader.load('components') 66 | expect(loader).to be_loaded 67 | end 68 | 69 | xit 'is false if components file is not loaded' do 70 | loader = described_class.new(v8_context) 71 | expect(loader).to_not be_loaded 72 | end 73 | end 74 | end 75 | -------------------------------------------------------------------------------- /spec/reactive-ruby/isomorphic_helpers_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe React::IsomorphicHelpers do 4 | describe 'code execution context' do 5 | let(:klass) { Class.send(:include, described_class) } 6 | 7 | describe 'module class methods', :opal do 8 | it { expect(described_class).to_not be_on_opal_server } 9 | it { expect(described_class).to be_on_opal_client } 10 | end 11 | 12 | describe 'included class methods', :opal do 13 | it { expect(klass).to_not be_on_opal_server } 14 | it { expect(klass).to be_on_opal_client } 15 | end 16 | 17 | describe 'included instance methods', :opal do 18 | it { expect(klass.new).to_not be_on_opal_server } 19 | it { expect(klass.new).to be_on_opal_client } 20 | end 21 | 22 | describe 'module class methods', :ruby do 23 | it { is_expected.to_not be_on_opal_server } 24 | it { is_expected.to_not be_on_opal_client } 25 | end 26 | 27 | describe 'included class methods', :ruby do 28 | subject { klass } 29 | it { is_expected.to_not be_on_opal_server } 30 | it { is_expected.to_not be_on_opal_client } 31 | end 32 | 33 | describe 'included instance methods', :ruby do 34 | subject { klass.new } 35 | it { is_expected.to_not be_on_opal_server } 36 | it { is_expected.to_not be_on_opal_client } 37 | end 38 | end 39 | 40 | describe 'load_context', :ruby do 41 | let(:v8_context) { TestV8Context.new } 42 | let(:controller) { double('controller') } 43 | let(:name) { double('name') } 44 | 45 | it 'creates a context and sets a controller' do 46 | context = described_class.load_context(v8_context, controller, name) 47 | expect(context.controller).to eq(controller) 48 | end 49 | 50 | it 'creates a context and sets a unique_id', js: true do 51 | # this tests loads the prerender context and somehow trys evaluate_ruby, works only with above js: true 52 | # TODO this is triggered by TimeCop for some reason 53 | Timecop.freeze do 54 | stamp = Time.now.to_i 55 | context = described_class.load_context(v8_context, controller, name) 56 | expect(context.unique_id).to eq("#{ controller.object_id }-#{ stamp }") 57 | end 58 | end 59 | end 60 | 61 | describe React::IsomorphicHelpers::Context do 62 | class TestV8Context < Hash 63 | def eval(args) 64 | true 65 | end 66 | def attach(*args) 67 | true 68 | end 69 | end 70 | 71 | # Need to decouple/dry up this... 72 | def test_context(files = nil) 73 | js = ReactiveRuby::ServerRendering::ContextualRenderer::CONSOLE_POLYFILL.dup 74 | js << Opal::Builder.build('opal').to_s 75 | Array(files).each do |filename| 76 | js << ::Rails.application.assets[filename].to_s 77 | end 78 | js = "#{React::ServerRendering::ExecJSRenderer::GLOBAL_WRAPPER}#{js}" 79 | ctx = ExecJS.compile(js) 80 | ctx = ReactiveRuby::ServerRendering.context_instance_for(ctx) 81 | end 82 | 83 | def react_context 84 | if ::Rails.application.assets['react-server.js'] 85 | test_context(['server_rendering.js', 'react-server.js']) 86 | else 87 | test_context(['components', 'react.js']) 88 | end 89 | end 90 | 91 | let(:v8_context) { TestV8Context.new } 92 | let(:controller) { double('controller') } 93 | let(:name) { double('name') } 94 | before do 95 | described_class.instance_variable_set :@before_first_mount_blocks, nil 96 | end 97 | 98 | describe '#initialize' do 99 | it 'calls before mount callbacks' do 100 | string = instance_double(String) 101 | described_class.register_before_first_mount_block do 102 | string.inspect 103 | end 104 | expect(string).to receive(:inspect).once 105 | context = described_class.new('unique-id', v8_context, controller, name) 106 | end 107 | end 108 | 109 | describe '#eval' do 110 | it 'delegates to given context' do 111 | context = described_class.new('unique-id', v8_context, controller, name) 112 | js = 'true;' 113 | expect(v8_context).to receive(:eval).with(js).once 114 | context.eval(js) 115 | end 116 | end 117 | 118 | describe '#send_to_opal' do 119 | let(:opal_code) { Opal::Builder.new.build_str(ruby_code, __FILE__).to_s } 120 | let(:ruby_code) { %Q[ 121 | module React::IsomorphicHelpers 122 | def self.greet(name) 123 | "Hello, " + name + "!" 124 | end 125 | 126 | def self.valediction 127 | 'Goodbye' 128 | end 129 | end 130 | ]} 131 | 132 | it 'raises an error when react cannot be loaded' do 133 | context = described_class.new('unique-id', v8_context, controller, name) 134 | context.instance_variable_set(:@ctx, test_context) 135 | expect { 136 | context.send_to_opal(:foo) 137 | }.to raise_error(/No HyperReact components found/) 138 | end 139 | 140 | it 'executes method with args inside opal rubyracer context' do 141 | ctx = react_context 142 | context = described_class.new('unique-id', ctx, controller, name) 143 | context.eval(opal_code) 144 | result = context.send_to_opal(:greet, 'world') 145 | expect(result).to eq('Hello, world!') 146 | end 147 | 148 | it 'executes the method inside opal rubyracer context' do 149 | ctx = react_context 150 | context = described_class.new('unique-id', ctx, controller, name) 151 | context.eval(opal_code) 152 | result = context.send_to_opal(:valediction) 153 | expect(result).to eq('Goodbye') 154 | end 155 | end 156 | end 157 | end 158 | -------------------------------------------------------------------------------- /spec/reactive-ruby/rails/asset_pipeline_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | # this spec makes trouble, becasue if the assets wont get deleted, the app will use 4 | # the precompiled assets suring testing, whcih interferes with dynamically created code 5 | describe 'test_app generator' do 6 | xit "does not interfere with asset precompilation" do 7 | cmd = "cd spec/test_app; BUNDLE_GEMFILE=#{ENV['REAL_BUNDLE_GEMFILE']} bundle exec rails assets:precompile" 8 | expect(system(cmd)).to be_truthy 9 | end 10 | end 11 | 12 | describe 'assets:clobber' do 13 | xit "remove precompiled assets so tests use recent assets" do 14 | cmd = "cd spec/test_app; BUNDLE_GEMFILE=#{ENV['REAL_BUNDLE_GEMFILE']} bundle exec rails assets:clobber" 15 | expect(system(cmd)).to be_truthy 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /spec/reactive-ruby/rails/component_mount_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | RSpec.describe ReactiveRuby::Rails::ComponentMount do 4 | let(:helper) { described_class.new } 5 | 6 | before do 7 | helper.setup(ActionView::TestCase::TestController.new) 8 | end 9 | 10 | describe '#react_component' do 11 | it 'renders a div' do 12 | html = helper.react_component('Components::HelloWorld') 13 | expect(html).to match(/<\/div>/) 14 | end 15 | 16 | it 'accepts a pre-render option' do 17 | html = helper.react_component('Components::HelloWorld', {}, prerender: true) 18 | expect(html).to match(/Hello, World!<\/span><\/div>/) 19 | end 20 | 21 | it 'sets data-react-class to React.TopLevelRailsComponent' do 22 | html = helper.react_component('Components::HelloWorld') 23 | top_level_class = 'React.TopLevelRailsComponent' 24 | expect(attr_value(html, 'data-react-class')).to eq(top_level_class) 25 | end 26 | 27 | it 'sets component_name in data-react-props hash' do 28 | html = helper.react_component('Components::HelloWorld') 29 | props = react_props_for(html) 30 | 31 | expect(props['component_name']).to eq('Components::HelloWorld') 32 | end 33 | 34 | it 'sets render_params in data-react-props hash' do 35 | html = helper.react_component('Components::HelloWorld', {'foo' => 'bar'}) 36 | props = react_props_for(html) 37 | 38 | expect(props['render_params']).to include({ 'foo' => 'bar' }) 39 | end 40 | 41 | it 'sets controller in data-react-props hash' do 42 | html = helper.react_component('Components::HelloWorld') 43 | props = react_props_for(html) 44 | 45 | expect(props['controller']).to eq('ActionView::TestCase::Test') 46 | end 47 | 48 | it 'passes additional options through as html attributes' do 49 | html = helper.react_component('Components::HelloWorld', {}, 50 | { 'foo-bar' => 'biz-baz' }) 51 | 52 | expect(attr_value(html, 'foo-bar')).to eq('biz-baz') 53 | end 54 | end 55 | 56 | def attr_value(html, attr) 57 | matches = html.match(/#{attr}=["']((?:.(?!["']\s+(?:\S+)=|[>"']))+.)["']?/) 58 | matches.captures.first 59 | end 60 | 61 | def react_props_for(html) 62 | JSON.parse(CGI.unescapeHTML("#{attr_value(html, 'data-react-props')}")) 63 | end 64 | end 65 | -------------------------------------------------------------------------------- /spec/reactive-ruby/server_rendering/contextual_renderer_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | RSpec.describe ReactiveRuby::ServerRendering::ContextualRenderer do 4 | let(:renderer) { described_class.new({}) } 5 | let(:init) { Proc.new {} } 6 | let(:options) { { context_initializer: init } } 7 | 8 | describe '#render' do 9 | it 'pre-renders HTML' do 10 | result = renderer.render('Components.Todo', 11 | { todo: 'finish reactive-ruby' }, 12 | options) 13 | expect(result).to match(/finish reactive-ruby<\/li>/) 14 | # react 16 does not generate checksum 15 | # expect(result).to match(/data-react-checksum/) 16 | expect(result).to match(/data-reactroot/) 17 | end 18 | 19 | it 'accepts props as a string' do 20 | result = renderer.render('Components.Todo', 21 | { todo: 'finish reactive-ruby' }.to_json, 22 | options) 23 | expect(result).to match(/finish reactive-ruby<\/li>/) 24 | # react 16 does not generate checksum 25 | # expect(result).to match(/data-react-checksum/) 26 | expect(result).to match(/data-reactroot/) 27 | end 28 | 29 | it 'pre-renders static content' do 30 | result = renderer.render('Components.Todo', 31 | { todo: 'finish reactive-ruby' }, 32 | :static) 33 | expect(result).to match(/finish reactive-ruby<\/li>/) 34 | # react 16 does not generate checksum 35 | # expect(result).to_not match(/data-react-checksum/) 36 | expect(result).to_not match(/data-reactroot/) 37 | end 38 | end 39 | end 40 | -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | ENV["RAILS_ENV"] ||= 'test' 2 | 3 | require 'opal' 4 | require 'opal-rspec' 5 | require 'opal-jquery' 6 | 7 | begin 8 | require File.expand_path('../test_app/config/environment', __FILE__) 9 | rescue LoadError 10 | puts 'Could not load test application. Please ensure you have run `bundle exec rake test_app`' 11 | end 12 | require 'rspec/rails' 13 | require 'hyper-spec' 14 | require 'pry' 15 | require 'opal-browser' 16 | require 'timecop' 17 | 18 | RSpec.configure do |config| 19 | config.color = true 20 | config.fail_fast = ENV['FAIL_FAST'] || false 21 | config.fixture_path = File.join(File.expand_path(File.dirname(__FILE__)), "fixtures") 22 | config.infer_spec_type_from_file_location! 23 | config.mock_with :rspec 24 | config.raise_errors_for_deprecations! 25 | 26 | # If you're not using ActiveRecord, or you'd prefer not to run each of your 27 | # examples within a transaction, comment the following line or assign false 28 | # instead of true. 29 | config.use_transactional_fixtures = true 30 | 31 | config.before :each do 32 | Rails.cache.clear 33 | end 34 | 35 | config.filter_run_including focus: true 36 | config.filter_run_excluding opal: true 37 | config.run_all_when_everything_filtered = true 38 | 39 | # Fail tests on JavaScript errors in Chrome Headless 40 | class JavaScriptError < StandardError; end 41 | 42 | config.after(:each, js: true) do |spec| 43 | logs = page.driver.browser.manage.logs.get(:browser) 44 | errors = logs.select { |e| e.level == "SEVERE" && e.message.present? } 45 | .map { |m| m.message.gsub(/\\n/, "\n") }.to_a 46 | if client_options[:deprecation_warnings] == :on 47 | warnings = logs.select { |e| e.level == "WARNING" && e.message.present? } 48 | .map { |m| m.message.gsub(/\\n/, "\n") }.to_a 49 | puts "\033[0;33;1m\nJavascript client console warnings:\n\n" + warnings.join("\n\n") + "\033[0;30;21m" if warnings.present? 50 | end 51 | unless client_options[:raise_on_js_errors] == :off 52 | raise JavaScriptError, errors.join("\n\n") if errors.present? 53 | end 54 | end 55 | end 56 | -------------------------------------------------------------------------------- /spec/test_app/README.md: -------------------------------------------------------------------------------- 1 | # README 2 | 3 | This README would normally document whatever steps are necessary to get the 4 | application up and running. 5 | 6 | Things you may want to cover: 7 | 8 | * Ruby version 9 | 10 | * System dependencies 11 | 12 | * Configuration 13 | 14 | * Database creation 15 | 16 | * Database initialization 17 | 18 | * How to run the test suite 19 | 20 | * Services (job queues, cache servers, search engines, etc.) 21 | 22 | * Deployment instructions 23 | 24 | * ... 25 | -------------------------------------------------------------------------------- /spec/test_app/Rakefile: -------------------------------------------------------------------------------- 1 | # Add your own tasks in files placed in lib/tasks ending in .rake, 2 | # for example lib/tasks/capistrano.rake, and they will automatically be available to Rake. 3 | 4 | require_relative 'config/application' 5 | 6 | Rails.application.load_tasks 7 | -------------------------------------------------------------------------------- /spec/test_app/app/assets/config/manifest.js: -------------------------------------------------------------------------------- 1 | //= link_tree ../images 2 | //= link_directory ../javascripts .js 3 | //= link_directory ../stylesheets .css 4 | -------------------------------------------------------------------------------- /spec/test_app/app/assets/images/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ruby-hyperloop/hyper-react/579326fd76ee09c38742b4c26160ae2106581671/spec/test_app/app/assets/images/.keep -------------------------------------------------------------------------------- /spec/test_app/app/assets/javascripts/application.rb: -------------------------------------------------------------------------------- 1 | require 'jquery' 2 | require 'react.js' 3 | require 'react-server.js' 4 | require 'react_ujs' 5 | require 'opal' 6 | require 'opal-jquery' 7 | require 'components' 8 | -------------------------------------------------------------------------------- /spec/test_app/app/assets/javascripts/cable.js: -------------------------------------------------------------------------------- 1 | // Action Cable provides the framework to deal with WebSockets in Rails. 2 | // You can generate new channels where WebSocket features live using the `rails generate channel` command. 3 | // 4 | //= require action_cable 5 | //= require_self 6 | //= require_tree ./channels 7 | 8 | (function() { 9 | this.App || (this.App = {}); 10 | 11 | App.cable = ActionCable.createConsumer(); 12 | 13 | }).call(this); 14 | -------------------------------------------------------------------------------- /spec/test_app/app/assets/javascripts/channels/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ruby-hyperloop/hyper-react/579326fd76ee09c38742b4c26160ae2106581671/spec/test_app/app/assets/javascripts/channels/.keep -------------------------------------------------------------------------------- /spec/test_app/app/assets/javascripts/server_rendering.js: -------------------------------------------------------------------------------- 1 | //= require 'react-server' 2 | //= require 'react_ujs' 3 | //= require 'opal' 4 | //= require 'components' 5 | Opal.load('components'); -------------------------------------------------------------------------------- /spec/test_app/app/assets/stylesheets/application.css: -------------------------------------------------------------------------------- 1 | /* 2 | * This is a manifest file that'll be compiled into application.css, which will include all the files 3 | * listed below. 4 | * 5 | * Any CSS and SCSS file within this directory, lib/assets/stylesheets, or any plugin's 6 | * vendor/assets/stylesheets directory can be referenced here using a relative path. 7 | * 8 | * You're free to add application-wide styles to this file and they'll appear at the bottom of the 9 | * compiled file so the styles you add here take precedence over styles defined in any other CSS/SCSS 10 | * files in this directory. Styles in this file should be added after the last require_* statement. 11 | * It is generally better to create a new file per style scope. 12 | * 13 | *= require_tree . 14 | *= require_self 15 | */ 16 | -------------------------------------------------------------------------------- /spec/test_app/app/channels/application_cable/channel.rb: -------------------------------------------------------------------------------- 1 | module ApplicationCable 2 | class Channel < ActionCable::Channel::Base 3 | end 4 | end 5 | -------------------------------------------------------------------------------- /spec/test_app/app/channels/application_cable/connection.rb: -------------------------------------------------------------------------------- 1 | module ApplicationCable 2 | class Connection < ActionCable::Connection::Base 3 | end 4 | end 5 | -------------------------------------------------------------------------------- /spec/test_app/app/controllers/application_controller.rb: -------------------------------------------------------------------------------- 1 | class ApplicationController < ActionController::Base 2 | protect_from_forgery with: :exception 3 | end 4 | -------------------------------------------------------------------------------- /spec/test_app/app/controllers/concerns/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ruby-hyperloop/hyper-react/579326fd76ee09c38742b4c26160ae2106581671/spec/test_app/app/controllers/concerns/.keep -------------------------------------------------------------------------------- /spec/test_app/app/helpers/application_helper.rb: -------------------------------------------------------------------------------- 1 | module ApplicationHelper 2 | end 3 | -------------------------------------------------------------------------------- /spec/test_app/app/jobs/application_job.rb: -------------------------------------------------------------------------------- 1 | class ApplicationJob < ActiveJob::Base 2 | end 3 | -------------------------------------------------------------------------------- /spec/test_app/app/mailers/application_mailer.rb: -------------------------------------------------------------------------------- 1 | class ApplicationMailer < ActionMailer::Base 2 | default from: 'from@example.com' 3 | layout 'mailer' 4 | end 5 | -------------------------------------------------------------------------------- /spec/test_app/app/models/application_record.rb: -------------------------------------------------------------------------------- 1 | class ApplicationRecord < ActiveRecord::Base 2 | self.abstract_class = true 3 | end 4 | -------------------------------------------------------------------------------- /spec/test_app/app/models/concerns/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ruby-hyperloop/hyper-react/579326fd76ee09c38742b4c26160ae2106581671/spec/test_app/app/models/concerns/.keep -------------------------------------------------------------------------------- /spec/test_app/app/views/components.rb: -------------------------------------------------------------------------------- 1 | require 'hyper-react' 2 | if React::IsomorphicHelpers.on_opal_client? 3 | require 'browser' 4 | require 'browser/delay' 5 | end 6 | require 'react/server' 7 | require 'react/test/utils' 8 | require 'reactrb/auto-import' 9 | require 'js' 10 | 11 | require_tree './components' 12 | -------------------------------------------------------------------------------- /spec/test_app/app/views/components/hello_world.rb: -------------------------------------------------------------------------------- 1 | module Components 2 | class HelloWorld 3 | include React::Component 4 | 5 | def render 6 | div do 7 | "Hello, World!".span 8 | end 9 | end 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /spec/test_app/app/views/components/todo.rb: -------------------------------------------------------------------------------- 1 | module Components 2 | class Todo 3 | include React::Component 4 | export_component 5 | 6 | params do 7 | requires :todo 8 | end 9 | 10 | def render 11 | li { "#{params[:todo]}" } 12 | end 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /spec/test_app/app/views/layouts/application.html.erb: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | TestApp 5 | <%= csrf_meta_tags %> 6 | 7 | <%= stylesheet_link_tag 'application', media: 'all', 'data-turbolinks-track': 'reload' %> 8 | <%= javascript_include_tag 'application', 'data-turbolinks-track': 'reload' %> 9 | 10 | 11 | 12 | <%= yield %> 13 | 14 | 15 | -------------------------------------------------------------------------------- /spec/test_app/app/views/layouts/explicit_layout.html.erb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ruby-hyperloop/hyper-react/579326fd76ee09c38742b4c26160ae2106581671/spec/test_app/app/views/layouts/explicit_layout.html.erb -------------------------------------------------------------------------------- /spec/test_app/app/views/layouts/mailer.html.erb: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 8 | 9 | 10 | 11 | <%= yield %> 12 | 13 | 14 | -------------------------------------------------------------------------------- /spec/test_app/app/views/layouts/mailer.text.erb: -------------------------------------------------------------------------------- 1 | <%= yield %> 2 | -------------------------------------------------------------------------------- /spec/test_app/app/views/layouts/test_layout.html.erb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ruby-hyperloop/hyper-react/579326fd76ee09c38742b4c26160ae2106581671/spec/test_app/app/views/layouts/test_layout.html.erb -------------------------------------------------------------------------------- /spec/test_app/bin/bundle: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../../Gemfile', __FILE__) 3 | load Gem.bin_path('bundler', 'bundle') 4 | -------------------------------------------------------------------------------- /spec/test_app/bin/rails: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | APP_PATH = File.expand_path('../config/application', __dir__) 3 | require_relative '../config/boot' 4 | require 'rails/commands' 5 | -------------------------------------------------------------------------------- /spec/test_app/bin/rake: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | require_relative '../config/boot' 3 | require 'rake' 4 | Rake.application.run 5 | -------------------------------------------------------------------------------- /spec/test_app/bin/setup: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | require 'pathname' 3 | require 'fileutils' 4 | include FileUtils 5 | 6 | # path to your application root. 7 | APP_ROOT = Pathname.new File.expand_path('../../', __FILE__) 8 | 9 | def system!(*args) 10 | system(*args) || abort("\n== Command #{args} failed ==") 11 | end 12 | 13 | chdir APP_ROOT do 14 | # This script is a starting point to setup your application. 15 | # Add necessary setup steps to this file. 16 | 17 | puts '== Installing dependencies ==' 18 | system! 'gem install bundler --conservative' 19 | system('bundle check') || system!('bundle install') 20 | 21 | # Install JavaScript dependencies if using Yarn 22 | # system('bin/yarn') 23 | 24 | 25 | # puts "\n== Copying sample files ==" 26 | # unless File.exist?('config/database.yml') 27 | # cp 'config/database.yml.sample', 'config/database.yml' 28 | # end 29 | 30 | puts "\n== Preparing database ==" 31 | system! 'bin/rails db:setup' 32 | 33 | puts "\n== Removing old logs and tempfiles ==" 34 | system! 'bin/rails log:clear tmp:clear' 35 | 36 | puts "\n== Restarting application server ==" 37 | system! 'bin/rails restart' 38 | end 39 | -------------------------------------------------------------------------------- /spec/test_app/bin/update: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | require 'pathname' 3 | require 'fileutils' 4 | include FileUtils 5 | 6 | # path to your application root. 7 | APP_ROOT = Pathname.new File.expand_path('../../', __FILE__) 8 | 9 | def system!(*args) 10 | system(*args) || abort("\n== Command #{args} failed ==") 11 | end 12 | 13 | chdir APP_ROOT do 14 | # This script is a way to update your development environment automatically. 15 | # Add necessary update steps to this file. 16 | 17 | puts '== Installing dependencies ==' 18 | system! 'gem install bundler --conservative' 19 | system('bundle check') || system!('bundle install') 20 | 21 | puts "\n== Updating database ==" 22 | system! 'bin/rails db:migrate' 23 | 24 | puts "\n== Removing old logs and tempfiles ==" 25 | system! 'bin/rails log:clear tmp:clear' 26 | 27 | puts "\n== Restarting application server ==" 28 | system! 'bin/rails restart' 29 | end 30 | -------------------------------------------------------------------------------- /spec/test_app/bin/yarn: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | VENDOR_PATH = File.expand_path('..', __dir__) 3 | Dir.chdir(VENDOR_PATH) do 4 | begin 5 | exec "yarnpkg #{ARGV.join(" ")}" 6 | rescue Errno::ENOENT 7 | $stderr.puts "Yarn executable was not detected in the system." 8 | $stderr.puts "Download Yarn at https://yarnpkg.com/en/docs/install" 9 | exit 1 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /spec/test_app/config.ru: -------------------------------------------------------------------------------- 1 | # This file is used by Rack-based servers to start the application. 2 | 3 | require_relative 'config/environment' 4 | 5 | run Rails.application 6 | -------------------------------------------------------------------------------- /spec/test_app/config/application.rb: -------------------------------------------------------------------------------- 1 | 2 | require 'rails/all' 3 | require File.expand_path('../boot', __FILE__) 4 | 5 | # Require the gems listed in Gemfile, including any gems 6 | # you've limited to :test, :development, or :production. 7 | Bundler.require(*Rails.groups(assets: %w(development test))) 8 | require 'jquery-rails' 9 | require 'opal' 10 | require 'opal-jquery' 11 | require 'opal-browser' 12 | require 'opal-rails' 13 | require 'react-rails' 14 | require 'hyper-store' 15 | require 'hyper-react' 16 | require 'hyper-spec' 17 | 18 | module TestApp 19 | class Application < Rails::Application 20 | config.opal.method_missing = true 21 | config.opal.optimized_operators = true 22 | config.opal.arity_check = false 23 | config.opal.const_missing = true 24 | config.opal.dynamic_require_severity = :ignore 25 | config.opal.enable_specs = true 26 | config.opal.spec_location = 'spec-opal' 27 | config.hyperloop.auto_config = false 28 | 29 | config.assets.cache_store = :null_store 30 | 31 | config.react.server_renderer_options = { 32 | files: ["server_rendering.js"] 33 | } 34 | config.react.server_renderer_directories = ["/app/assets/javascripts"] 35 | 36 | # Initialize configuration defaults for originally generated Rails version. 37 | config.load_defaults 5.1 38 | 39 | # Settings in config/environments/* take precedence over those specified here. 40 | # Application configuration should go into files in config/initializers 41 | # -- all .rb files in that directory are automatically loaded. 42 | end 43 | end 44 | 45 | 46 | -------------------------------------------------------------------------------- /spec/test_app/config/boot.rb: -------------------------------------------------------------------------------- 1 | require 'rubygems' 2 | gemfile = File.expand_path("../../../../Gemfile", __FILE__) 3 | 4 | ENV['BUNDLE_GEMFILE'] = gemfile 5 | require 'bundler' 6 | Bundler.setup 7 | -------------------------------------------------------------------------------- /spec/test_app/config/cable.yml: -------------------------------------------------------------------------------- 1 | development: 2 | adapter: async 3 | 4 | test: 5 | adapter: async 6 | 7 | production: 8 | adapter: redis 9 | url: redis://localhost:6379/1 10 | channel_prefix: test_app_production 11 | -------------------------------------------------------------------------------- /spec/test_app/config/database.yml: -------------------------------------------------------------------------------- 1 | # SQLite version 3.x 2 | # gem install sqlite3 3 | # 4 | # Ensure the SQLite 3 gem is defined in your Gemfile 5 | # gem 'sqlite3' 6 | # 7 | default: &default 8 | adapter: sqlite3 9 | pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %> 10 | timeout: 5000 11 | 12 | development: 13 | <<: *default 14 | database: db/development.sqlite3 15 | 16 | # Warning: The database defined as "test" will be erased and 17 | # re-generated from your development database when you run "rake". 18 | # Do not set this db to the same as development or production. 19 | test: 20 | <<: *default 21 | database: db/test.sqlite3 22 | 23 | production: 24 | <<: *default 25 | database: db/production.sqlite3 26 | -------------------------------------------------------------------------------- /spec/test_app/config/environment.rb: -------------------------------------------------------------------------------- 1 | # Load the Rails application. 2 | require_relative 'application' 3 | 4 | # Initialize the Rails application. 5 | Rails.application.initialize! 6 | -------------------------------------------------------------------------------- /spec/test_app/config/environments/development.rb: -------------------------------------------------------------------------------- 1 | Rails.application.configure do 2 | # Settings specified here will take precedence over those in config/application.rb. 3 | 4 | # In the development environment your application's code is reloaded on 5 | # every request. This slows down response time but is perfect for development 6 | # since you don't have to restart the web server when you make code changes. 7 | config.cache_classes = false 8 | 9 | # Do not eager load code on boot. 10 | config.eager_load = false 11 | 12 | # Show full error reports. 13 | config.consider_all_requests_local = true 14 | 15 | # Enable/disable caching. By default caching is disabled. 16 | if Rails.root.join('tmp/caching-dev.txt').exist? 17 | config.action_controller.perform_caching = true 18 | 19 | config.cache_store = :memory_store 20 | config.public_file_server.headers = { 21 | 'Cache-Control' => "public, max-age=#{2.days.seconds.to_i}" 22 | } 23 | else 24 | config.action_controller.perform_caching = false 25 | 26 | config.cache_store = :null_store 27 | end 28 | 29 | # Don't care if the mailer can't send. 30 | config.action_mailer.raise_delivery_errors = false 31 | 32 | config.action_mailer.perform_caching = false 33 | 34 | # Print deprecation notices to the Rails logger. 35 | config.active_support.deprecation = :log 36 | 37 | # Raise an error on page load if there are pending migrations. 38 | config.active_record.migration_error = :page_load 39 | 40 | # Debug mode disables concatenation and preprocessing of assets. 41 | # This option may cause significant delays in view rendering with a large 42 | # number of complex assets. 43 | config.assets.debug = true 44 | 45 | # Suppress logger output for asset requests. 46 | config.assets.quiet = true 47 | 48 | # Raises error for missing translations 49 | # config.action_view.raise_on_missing_translations = true 50 | 51 | # Use an evented file watcher to asynchronously detect changes in source code, 52 | # routes, locales, etc. This feature depends on the listen gem. 53 | config.file_watcher = ActiveSupport::EventedFileUpdateChecker 54 | end 55 | -------------------------------------------------------------------------------- /spec/test_app/config/environments/production.rb: -------------------------------------------------------------------------------- 1 | Rails.application.configure do 2 | # Settings specified here will take precedence over those in config/application.rb. 3 | 4 | # Code is not reloaded between requests. 5 | config.cache_classes = true 6 | 7 | # Eager load code on boot. This eager loads most of Rails and 8 | # your application in memory, allowing both threaded web servers 9 | # and those relying on copy on write to perform better. 10 | # Rake tasks automatically ignore this option for performance. 11 | config.eager_load = true 12 | 13 | # Full error reports are disabled and caching is turned on. 14 | config.consider_all_requests_local = false 15 | config.action_controller.perform_caching = true 16 | 17 | # Attempt to read encrypted secrets from `config/secrets.yml.enc`. 18 | # Requires an encryption key in `ENV["RAILS_MASTER_KEY"]` or 19 | # `config/secrets.yml.key`. 20 | config.read_encrypted_secrets = true 21 | 22 | # Disable serving static files from the `/public` folder by default since 23 | # Apache or NGINX already handles this. 24 | config.public_file_server.enabled = ENV['RAILS_SERVE_STATIC_FILES'].present? 25 | 26 | # Compress JavaScripts and CSS. 27 | config.assets.js_compressor = :uglifier 28 | # config.assets.css_compressor = :sass 29 | 30 | # Do not fallback to assets pipeline if a precompiled asset is missed. 31 | config.assets.compile = false 32 | 33 | # `config.assets.precompile` and `config.assets.version` have moved to config/initializers/assets.rb 34 | 35 | # Enable serving of images, stylesheets, and JavaScripts from an asset server. 36 | # config.action_controller.asset_host = 'http://assets.example.com' 37 | 38 | # Specifies the header that your server uses for sending files. 39 | # config.action_dispatch.x_sendfile_header = 'X-Sendfile' # for Apache 40 | # config.action_dispatch.x_sendfile_header = 'X-Accel-Redirect' # for NGINX 41 | 42 | # Mount Action Cable outside main process or domain 43 | # config.action_cable.mount_path = nil 44 | # config.action_cable.url = 'wss://example.com/cable' 45 | # config.action_cable.allowed_request_origins = [ 'http://example.com', /http:\/\/example.*/ ] 46 | 47 | # Force all access to the app over SSL, use Strict-Transport-Security, and use secure cookies. 48 | # config.force_ssl = true 49 | 50 | # Use the lowest log level to ensure availability of diagnostic information 51 | # when problems arise. 52 | config.log_level = :debug 53 | 54 | # Prepend all log lines with the following tags. 55 | config.log_tags = [ :request_id ] 56 | 57 | # Use a different cache store in production. 58 | # config.cache_store = :mem_cache_store 59 | 60 | # Use a real queuing backend for Active Job (and separate queues per environment) 61 | # config.active_job.queue_adapter = :resque 62 | # config.active_job.queue_name_prefix = "test_app_#{Rails.env}" 63 | config.action_mailer.perform_caching = false 64 | 65 | # Ignore bad email addresses and do not raise email delivery errors. 66 | # Set this to true and configure the email server for immediate delivery to raise delivery errors. 67 | # config.action_mailer.raise_delivery_errors = false 68 | 69 | # Enable locale fallbacks for I18n (makes lookups for any locale fall back to 70 | # the I18n.default_locale when a translation cannot be found). 71 | config.i18n.fallbacks = true 72 | 73 | # Send deprecation notices to registered listeners. 74 | config.active_support.deprecation = :notify 75 | 76 | # Use default logging formatter so that PID and timestamp are not suppressed. 77 | config.log_formatter = ::Logger::Formatter.new 78 | 79 | # Use a different logger for distributed setups. 80 | # require 'syslog/logger' 81 | # config.logger = ActiveSupport::TaggedLogging.new(Syslog::Logger.new 'app-name') 82 | 83 | if ENV["RAILS_LOG_TO_STDOUT"].present? 84 | logger = ActiveSupport::Logger.new(STDOUT) 85 | logger.formatter = config.log_formatter 86 | config.logger = ActiveSupport::TaggedLogging.new(logger) 87 | end 88 | 89 | # Do not dump schema after migrations. 90 | config.active_record.dump_schema_after_migration = false 91 | end 92 | -------------------------------------------------------------------------------- /spec/test_app/config/environments/test.rb: -------------------------------------------------------------------------------- 1 | Rails.application.configure do 2 | # Settings specified here will take precedence over those in config/application.rb. 3 | 4 | # The test environment is used exclusively to run your application's 5 | # test suite. You never need to work with it otherwise. Remember that 6 | # your test database is "scratch space" for the test suite and is wiped 7 | # and recreated between test runs. Don't rely on the data there! 8 | config.cache_classes = true 9 | 10 | # Do not eager load code on boot. This avoids loading your whole application 11 | # just for the purpose of running a single test. If you are using a tool that 12 | # preloads Rails for running tests, you may have to set it to true. 13 | config.eager_load = false 14 | 15 | # Configure public file server for tests with Cache-Control for performance. 16 | config.public_file_server.enabled = true 17 | config.public_file_server.headers = { 18 | 'Cache-Control' => "public, max-age=#{1.hour.seconds.to_i}" 19 | } 20 | 21 | # Show full error reports and disable caching. 22 | config.consider_all_requests_local = true 23 | config.action_controller.perform_caching = false 24 | 25 | # Raise exceptions instead of rendering exception templates. 26 | config.action_dispatch.show_exceptions = false 27 | 28 | # Disable request forgery protection in test environment. 29 | config.action_controller.allow_forgery_protection = false 30 | config.action_mailer.perform_caching = false 31 | 32 | # Tell Action Mailer not to deliver emails to the real world. 33 | # The :test delivery method accumulates sent emails in the 34 | # ActionMailer::Base.deliveries array. 35 | config.action_mailer.delivery_method = :test 36 | 37 | # Print deprecation notices to the stderr. 38 | config.active_support.deprecation = :stderr 39 | 40 | # Raises error for missing translations 41 | # config.action_view.raise_on_missing_translations = true 42 | end 43 | -------------------------------------------------------------------------------- /spec/test_app/config/initializers/application_controller_renderer.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # ActiveSupport::Reloader.to_prepare do 4 | # ApplicationController.renderer.defaults.merge!( 5 | # http_host: 'example.org', 6 | # https: false 7 | # ) 8 | # end 9 | -------------------------------------------------------------------------------- /spec/test_app/config/initializers/assets.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # Version of your assets, change this if you want to expire all your assets. 4 | Rails.application.config.assets.version = '1.0' 5 | 6 | # Add additional assets to the asset load path. 7 | # Rails.application.config.assets.paths << Emoji.images_path 8 | # Add Yarn node_modules folder to the asset load path. 9 | Rails.application.config.assets.paths << Rails.root.join('node_modules') 10 | 11 | # Precompile additional assets. 12 | # application.js, application.css, and all non-JS/CSS in the app/assets 13 | # folder are already added. 14 | Rails.application.config.assets.precompile += %w(time_cop.js) 15 | -------------------------------------------------------------------------------- /spec/test_app/config/initializers/backtrace_silencers.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # You can add backtrace silencers for libraries that you're using but don't wish to see in your backtraces. 4 | # Rails.backtrace_cleaner.add_silencer { |line| line =~ /my_noisy_library/ } 5 | 6 | # You can also remove all the silencers if you're trying to debug a problem that might stem from framework code. 7 | # Rails.backtrace_cleaner.remove_silencers! 8 | -------------------------------------------------------------------------------- /spec/test_app/config/initializers/cookies_serializer.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # Specify a serializer for the signed and encrypted cookie jars. 4 | # Valid options are :json, :marshal, and :hybrid. 5 | Rails.application.config.action_dispatch.cookies_serializer = :json 6 | -------------------------------------------------------------------------------- /spec/test_app/config/initializers/filter_parameter_logging.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # Configure sensitive parameters which will be filtered from the log file. 4 | Rails.application.config.filter_parameters += [:password] 5 | -------------------------------------------------------------------------------- /spec/test_app/config/initializers/inflections.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # Add new inflection rules using the following format. Inflections 4 | # are locale specific, and you may define rules for as many different 5 | # locales as you wish. All of these examples are active by default: 6 | # ActiveSupport::Inflector.inflections(:en) do |inflect| 7 | # inflect.plural /^(ox)$/i, '\1en' 8 | # inflect.singular /^(ox)en/i, '\1' 9 | # inflect.irregular 'person', 'people' 10 | # inflect.uncountable %w( fish sheep ) 11 | # end 12 | 13 | # These inflection rules are supported but not enabled by default: 14 | # ActiveSupport::Inflector.inflections(:en) do |inflect| 15 | # inflect.acronym 'RESTful' 16 | # end 17 | -------------------------------------------------------------------------------- /spec/test_app/config/initializers/mime_types.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # Add new mime types for use in respond_to blocks: 4 | # Mime::Type.register "text/richtext", :rtf 5 | -------------------------------------------------------------------------------- /spec/test_app/config/initializers/wrap_parameters.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # This file contains settings for ActionController::ParamsWrapper which 4 | # is enabled by default. 5 | 6 | # Enable parameter wrapping for JSON. You can disable this by setting :format to an empty array. 7 | ActiveSupport.on_load(:action_controller) do 8 | wrap_parameters format: [:json] 9 | end 10 | 11 | # To enable root element in JSON for ActiveRecord objects. 12 | # ActiveSupport.on_load(:active_record) do 13 | # self.include_root_in_json = true 14 | # end 15 | -------------------------------------------------------------------------------- /spec/test_app/config/locales/en.yml: -------------------------------------------------------------------------------- 1 | # Files in the config/locales directory are used for internationalization 2 | # and are automatically loaded by Rails. If you want to use locales other 3 | # than English, add the necessary files in this directory. 4 | # 5 | # To use the locales, use `I18n.t`: 6 | # 7 | # I18n.t 'hello' 8 | # 9 | # In views, this is aliased to just `t`: 10 | # 11 | # <%= t('hello') %> 12 | # 13 | # To use a different locale, set it with `I18n.locale`: 14 | # 15 | # I18n.locale = :es 16 | # 17 | # This would use the information in config/locales/es.yml. 18 | # 19 | # The following keys must be escaped otherwise they will not be retrieved by 20 | # the default I18n backend: 21 | # 22 | # true, false, on, off, yes, no 23 | # 24 | # Instead, surround them with single quotes. 25 | # 26 | # en: 27 | # 'true': 'foo' 28 | # 29 | # To learn more, please read the Rails Internationalization guide 30 | # available at http://guides.rubyonrails.org/i18n.html. 31 | 32 | en: 33 | hello: "Hello world" 34 | -------------------------------------------------------------------------------- /spec/test_app/config/puma.rb: -------------------------------------------------------------------------------- 1 | # Puma can serve each request in a thread from an internal thread pool. 2 | # The `threads` method setting takes two numbers: a minimum and maximum. 3 | # Any libraries that use thread pools should be configured to match 4 | # the maximum value specified for Puma. Default is set to 5 threads for minimum 5 | # and maximum; this matches the default thread size of Active Record. 6 | # 7 | threads_count = ENV.fetch("RAILS_MAX_THREADS") { 5 } 8 | threads threads_count, threads_count 9 | 10 | # Specifies the `port` that Puma will listen on to receive requests; default is 3000. 11 | # 12 | port ENV.fetch("PORT") { 3000 } 13 | 14 | # Specifies the `environment` that Puma will run in. 15 | # 16 | environment ENV.fetch("RAILS_ENV") { "development" } 17 | 18 | # Specifies the number of `workers` to boot in clustered mode. 19 | # Workers are forked webserver processes. If using threads and workers together 20 | # the concurrency of the application would be max `threads` * `workers`. 21 | # Workers do not work on JRuby or Windows (both of which do not support 22 | # processes). 23 | # 24 | # workers ENV.fetch("WEB_CONCURRENCY") { 2 } 25 | 26 | # Use the `preload_app!` method when specifying a `workers` number. 27 | # This directive tells Puma to first boot the application and load code 28 | # before forking the application. This takes advantage of Copy On Write 29 | # process behavior so workers use less memory. If you use this option 30 | # you need to make sure to reconnect any threads in the `on_worker_boot` 31 | # block. 32 | # 33 | # preload_app! 34 | 35 | # If you are preloading your application and using Active Record, it's 36 | # recommended that you close any connections to the database before workers 37 | # are forked to prevent connection leakage. 38 | # 39 | # before_fork do 40 | # ActiveRecord::Base.connection_pool.disconnect! if defined?(ActiveRecord) 41 | # end 42 | 43 | # The code in the `on_worker_boot` will be called if you are using 44 | # clustered mode by specifying a number of `workers`. After each worker 45 | # process is booted, this block will be run. If you are using the `preload_app!` 46 | # option, you will want to use this block to reconnect to any threads 47 | # or connections that may have been created at application boot, as Ruby 48 | # cannot share connections between processes. 49 | # 50 | # on_worker_boot do 51 | # ActiveRecord::Base.establish_connection if defined?(ActiveRecord) 52 | # end 53 | # 54 | 55 | # Allow puma to be restarted by `rails restart` command. 56 | plugin :tmp_restart 57 | -------------------------------------------------------------------------------- /spec/test_app/config/routes.rb: -------------------------------------------------------------------------------- 1 | Rails.application.routes.draw do 2 | # For details on the DSL available within this file, see http://guides.rubyonrails.org/routing.html 3 | end 4 | -------------------------------------------------------------------------------- /spec/test_app/config/secrets.yml: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # Your secret key is used for verifying the integrity of signed cookies. 4 | # If you change this key, all old signed cookies will become invalid! 5 | 6 | # Make sure the secret is at least 30 characters and all random, 7 | # no regular words or you'll be exposed to dictionary attacks. 8 | # You can use `rails secret` to generate a secure secret key. 9 | 10 | # Make sure the secrets in this file are kept private 11 | # if you're sharing your code publicly. 12 | 13 | # Shared secrets are available across all environments. 14 | 15 | # shared: 16 | # api_key: a1B2c3D4e5F6 17 | 18 | # Environmental secrets are only available for that specific environment. 19 | 20 | development: 21 | secret_key_base: 44614022137481567d758b39609607efaa37be4f81c7c562d874eccc298088c602cc3ce22006e75b63ffb2b766d3f180a7857bdf779b911aabffc7a6afeeac27 22 | 23 | test: 24 | secret_key_base: b6f22d4ce913d3053fbdab42e1cf8f9bf6d22af1ff131b6d57246d690abdd29b1d799809fbe2c0b0e6bdd7d7c7e6974083467f9ff31b97e41f0e44034a0e7f72 25 | 26 | # Do not keep production secrets in the unencrypted secrets file. 27 | # Instead, either read values from the environment. 28 | # Or, use `bin/rails secrets:setup` to configure encrypted secrets 29 | # and move the `production:` environment over there. 30 | 31 | production: 32 | secret_key_base: <%= ENV["SECRET_KEY_BASE"] %> 33 | -------------------------------------------------------------------------------- /spec/test_app/config/spring.rb: -------------------------------------------------------------------------------- 1 | %w( 2 | .ruby-version 3 | .rbenv-vars 4 | tmp/restart.txt 5 | tmp/caching-dev.txt 6 | ).each { |path| Spring.watch(path) } 7 | -------------------------------------------------------------------------------- /spec/test_app/db/schema.rb: -------------------------------------------------------------------------------- 1 | # This file is auto-generated from the current state of the database. Instead 2 | # of editing this file, please use the migrations feature of Active Record to 3 | # incrementally modify your database, and then regenerate this schema definition. 4 | # 5 | # Note that this schema.rb definition is the authoritative source for your 6 | # database schema. If you need to create the application database on another 7 | # system, you should be using db:schema:load, not running all the migrations 8 | # from scratch. The latter is a flawed and unsustainable approach (the more migrations 9 | # you'll amass, the slower it'll run and the greater likelihood for issues). 10 | # 11 | # It's strongly recommended that you check this file into your version control system. 12 | 13 | ActiveRecord::Schema.define(version: 0) do 14 | 15 | end 16 | -------------------------------------------------------------------------------- /spec/test_app/db/seeds.rb: -------------------------------------------------------------------------------- 1 | # This file should contain all the record creation needed to seed the database with its default values. 2 | # The data can then be loaded with the rails db:seed command (or created alongside the database with db:setup). 3 | # 4 | # Examples: 5 | # 6 | # movies = Movie.create([{ name: 'Star Wars' }, { name: 'Lord of the Rings' }]) 7 | # Character.create(name: 'Luke', movie: movies.first) 8 | -------------------------------------------------------------------------------- /spec/test_app/lib/assets/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ruby-hyperloop/hyper-react/579326fd76ee09c38742b4c26160ae2106581671/spec/test_app/lib/assets/.keep -------------------------------------------------------------------------------- /spec/test_app/log/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ruby-hyperloop/hyper-react/579326fd76ee09c38742b4c26160ae2106581671/spec/test_app/log/.keep -------------------------------------------------------------------------------- /spec/test_app/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "test_app", 3 | "private": true, 4 | "dependencies": {} 5 | } 6 | -------------------------------------------------------------------------------- /spec/test_app/public/404.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | The page you were looking for doesn't exist (404) 5 | 6 | 55 | 56 | 57 | 58 | 59 |
60 |
61 |

The page you were looking for doesn't exist.

62 |

You may have mistyped the address or the page may have moved.

63 |
64 |

If you are the application owner check the logs for more information.

65 |
66 | 67 | 68 | -------------------------------------------------------------------------------- /spec/test_app/public/422.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | The change you wanted was rejected (422) 5 | 6 | 55 | 56 | 57 | 58 | 59 |
60 |
61 |

The change you wanted was rejected.

62 |

Maybe you tried to change something you didn't have access to.

63 |
64 |

If you are the application owner check the logs for more information.

65 |
66 | 67 | 68 | -------------------------------------------------------------------------------- /spec/test_app/public/500.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | We're sorry, but something went wrong (500) 5 | 6 | 55 | 56 | 57 | 58 | 59 |
60 |
61 |

We're sorry, but something went wrong.

62 |
63 |

If you are the application owner check the logs for more information.

64 |
65 | 66 | 67 | -------------------------------------------------------------------------------- /spec/test_app/public/apple-touch-icon-precomposed.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ruby-hyperloop/hyper-react/579326fd76ee09c38742b4c26160ae2106581671/spec/test_app/public/apple-touch-icon-precomposed.png -------------------------------------------------------------------------------- /spec/test_app/public/apple-touch-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ruby-hyperloop/hyper-react/579326fd76ee09c38742b4c26160ae2106581671/spec/test_app/public/apple-touch-icon.png -------------------------------------------------------------------------------- /spec/test_app/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ruby-hyperloop/hyper-react/579326fd76ee09c38742b4c26160ae2106581671/spec/test_app/public/favicon.ico --------------------------------------------------------------------------------