├── test ├── dummy │ ├── log │ │ └── .keep │ ├── app │ │ ├── mailers │ │ │ └── .keep │ │ ├── models │ │ │ ├── .keep │ │ │ └── concerns │ │ │ │ └── .keep │ │ ├── assets │ │ │ ├── images │ │ │ │ └── .keep │ │ │ ├── javascripts │ │ │ │ ├── example.js.jsx │ │ │ │ ├── example3.js.jsx │ │ │ │ ├── components.js │ │ │ │ ├── flow_types_example.js.jsx │ │ │ │ ├── example2.js.jsx.coffee │ │ │ │ ├── components │ │ │ │ │ ├── Todo.js.jsx.coffee │ │ │ │ │ ├── TodoList.js.jsx │ │ │ │ │ └── TodoListWithConsoleLog.js.jsx │ │ │ │ ├── pages.js │ │ │ │ ├── harmony_example.js.jsx │ │ │ │ └── application.js │ │ │ └── stylesheets │ │ │ │ └── application.css │ │ ├── controllers │ │ │ ├── concerns │ │ │ │ └── .keep │ │ │ ├── pages_controller.rb │ │ │ ├── application_controller.rb │ │ │ └── server_controller.rb │ │ ├── helpers │ │ │ └── application_helper.rb │ │ └── views │ │ │ ├── server │ │ │ ├── show.html.erb │ │ │ ├── console_example.html.erb │ │ │ └── console_example_suppressed.html.erb │ │ │ ├── layouts │ │ │ └── application.html.erb │ │ │ └── pages │ │ │ └── show.html.erb │ ├── lib │ │ └── assets │ │ │ └── .keep │ ├── public │ │ ├── favicon.ico │ │ ├── 500.html │ │ ├── 422.html │ │ └── 404.html │ ├── vendor │ │ └── assets │ │ │ ├── javascripts │ │ │ └── .gitkeep │ │ │ └── react │ │ │ ├── test │ │ │ └── react__.js │ │ │ └── JSXTransformer__.js │ ├── bin │ │ ├── rake │ │ ├── bundle │ │ └── rails │ ├── config.ru │ ├── config │ │ ├── initializers │ │ │ ├── session_store.rb │ │ │ ├── filter_parameter_logging.rb │ │ │ ├── mime_types.rb │ │ │ ├── backtrace_silencers.rb │ │ │ ├── wrap_parameters.rb │ │ │ ├── inflections.rb │ │ │ └── secret_token.rb │ │ ├── environment.rb │ │ ├── boot.rb │ │ ├── routes.rb │ │ ├── locales │ │ │ └── en.yml │ │ ├── environments │ │ │ ├── development.rb │ │ │ ├── test.rb │ │ │ └── production.rb │ │ └── application.rb │ ├── Rakefile │ └── README.rdoc ├── helper_files │ └── TodoListWithUpdates.js.jsx ├── react │ ├── rails │ │ ├── asset_variant_test.rb │ │ └── view_helper_test.rb │ ├── jsx_test.rb │ ├── jsx │ │ └── jsx_transformer_test.rb │ ├── server_rendering_test.rb │ └── server_rendering │ │ ├── exec_js_renderer_test.rb │ │ └── sprockets_renderer_test.rb ├── generators │ ├── es6_component_generator_test.rb │ ├── install_generator_test.rb │ └── component_generator_test.rb ├── react_test.rb ├── test_helper.rb └── server_rendered_html_test.rb ├── lib ├── generators │ ├── templates │ │ ├── .gitkeep │ │ ├── component.js.jsx │ │ └── component.es6.jsx │ └── react │ │ ├── install_generator.rb │ │ └── component_generator.rb ├── react-rails.rb ├── react │ ├── rails │ │ ├── version.rb │ │ ├── engine.rb │ │ ├── controller_renderer.rb │ │ ├── asset_variant.rb │ │ ├── view_helper.rb │ │ └── railtie.rb │ ├── rails.rb │ ├── jsx │ │ ├── template.rb │ │ ├── babel_transformer.rb │ │ └── jsx_transformer.rb │ ├── jsx.rb │ ├── server_rendering.rb │ └── server_rendering │ │ ├── exec_js_renderer.rb │ │ └── sprockets_renderer.rb └── assets │ └── javascripts │ └── react_ujs.js.erb ├── .bowerrc ├── Gemfile ├── .gitignore ├── gemfiles ├── rails_3.2.gemfile ├── rails_4.0.gemfile ├── rails_4.1.gemfile ├── rails_4.2.gemfile └── rails_4.0_with_therubyracer.gemfile ├── .travis.yml ├── Guardfile ├── Appraisals ├── VERSIONS.md ├── Rakefile ├── react-rails.gemspec ├── CHANGELOG.md ├── LICENSE └── README.md /test/dummy/log/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /test/dummy/app/mailers/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /test/dummy/app/models/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /test/dummy/lib/assets/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /lib/generators/templates/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /test/dummy/public/favicon.ico: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /test/dummy/app/assets/images/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /test/dummy/app/models/concerns/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /test/dummy/app/controllers/concerns/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /test/dummy/vendor/assets/javascripts/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.bowerrc: -------------------------------------------------------------------------------- 1 | { 2 | "directory" : "vendor/" 3 | } 4 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source 'http://rubygems.org' 2 | 3 | gemspec 4 | -------------------------------------------------------------------------------- /test/dummy/app/assets/javascripts/example.js.jsx: -------------------------------------------------------------------------------- 1 |
; 2 | -------------------------------------------------------------------------------- /test/dummy/app/assets/javascripts/example3.js.jsx: -------------------------------------------------------------------------------- 1 |
; 2 | -------------------------------------------------------------------------------- /test/dummy/app/helpers/application_helper.rb: -------------------------------------------------------------------------------- 1 | module ApplicationHelper 2 | end 3 | -------------------------------------------------------------------------------- /test/dummy/vendor/assets/react/test/react__.js: -------------------------------------------------------------------------------- 1 | 'test_confirmation_token_react_content'; -------------------------------------------------------------------------------- /test/dummy/app/assets/javascripts/components.js: -------------------------------------------------------------------------------- 1 | //= require_self 2 | //= require_tree ./components 3 | -------------------------------------------------------------------------------- /lib/react-rails.rb: -------------------------------------------------------------------------------- 1 | require 'react/jsx' 2 | require 'react/rails' 3 | require 'react/server_rendering' 4 | 5 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.gem 2 | Gemfile.lock 3 | *.log 4 | test/dummy/tmp 5 | gemfiles/*.lock 6 | *.swp 7 | /vendor/react 8 | -------------------------------------------------------------------------------- /test/dummy/app/views/server/show.html.erb: -------------------------------------------------------------------------------- 1 | <%= react_component "TodoList", {todos: @todos}, {prerender: true} %> 2 | -------------------------------------------------------------------------------- /test/dummy/bin/rake: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | require_relative '../config/boot' 3 | require 'rake' 4 | Rake.application.run 5 | -------------------------------------------------------------------------------- /test/dummy/app/assets/javascripts/flow_types_example.js.jsx: -------------------------------------------------------------------------------- 1 | function flowTypesExample(i: number, name: string): string { 2 | return "OK" 3 | } -------------------------------------------------------------------------------- /test/dummy/app/views/server/console_example.html.erb: -------------------------------------------------------------------------------- 1 | <%= react_component "TodoListWithConsoleLog", {todos: @todos}, {prerender: true} %> 2 | -------------------------------------------------------------------------------- /test/dummy/app/views/server/console_example_suppressed.html.erb: -------------------------------------------------------------------------------- 1 | <%= react_component "TodoListWithConsoleLog", {todos: @todos}, {prerender: true} %> 2 | -------------------------------------------------------------------------------- /test/dummy/bin/bundle: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../../Gemfile', __FILE__) 3 | load Gem.bin_path('bundler', 'bundle') 4 | -------------------------------------------------------------------------------- /lib/react/rails/version.rb: -------------------------------------------------------------------------------- 1 | module React 2 | module Rails 3 | # If you change this, make sure to update VERSIONS.md 4 | VERSION = '1.2.0' 5 | end 6 | end 7 | -------------------------------------------------------------------------------- /gemfiles/rails_3.2.gemfile: -------------------------------------------------------------------------------- 1 | # This file was generated by Appraisal 2 | 3 | source "http://rubygems.org" 4 | 5 | gem "rails", "~> 3.2.21" 6 | 7 | gemspec :path => "../" 8 | -------------------------------------------------------------------------------- /gemfiles/rails_4.0.gemfile: -------------------------------------------------------------------------------- 1 | # This file was generated by Appraisal 2 | 3 | source "http://rubygems.org" 4 | 5 | gem "rails", "~> 4.0.13" 6 | 7 | gemspec :path => "../" 8 | -------------------------------------------------------------------------------- /gemfiles/rails_4.1.gemfile: -------------------------------------------------------------------------------- 1 | # This file was generated by Appraisal 2 | 3 | source "http://rubygems.org" 4 | 5 | gem "rails", "~> 4.1.10" 6 | 7 | gemspec :path => "../" 8 | -------------------------------------------------------------------------------- /gemfiles/rails_4.2.gemfile: -------------------------------------------------------------------------------- 1 | # This file was generated by Appraisal 2 | 3 | source "http://rubygems.org" 4 | 5 | gem "rails", "~> 4.2.1" 6 | 7 | gemspec :path => "../" 8 | -------------------------------------------------------------------------------- /test/dummy/app/controllers/pages_controller.rb: -------------------------------------------------------------------------------- 1 | class PagesController < ApplicationController 2 | def show 3 | @name = %w(Alice Bob)[params[:id].to_i % 2] 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /test/dummy/bin/rails: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | APP_PATH = File.expand_path('../../config/application', __FILE__) 3 | require_relative '../config/boot' 4 | require 'rails/commands' 5 | -------------------------------------------------------------------------------- /test/dummy/config.ru: -------------------------------------------------------------------------------- 1 | # This file is used by Rack-based servers to start the application. 2 | 3 | require ::File.expand_path('../config/environment', __FILE__) 4 | run Rails.application 5 | -------------------------------------------------------------------------------- /test/dummy/config/initializers/session_store.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | Dummy::Application.config.session_store :cookie_store, key: '_dummy_session' 4 | -------------------------------------------------------------------------------- /test/dummy/app/assets/javascripts/example2.js.jsx.coffee: -------------------------------------------------------------------------------- 1 | Component = React.createClass 2 | render: -> 3 | `` 4 | 5 | window.Component = Component 6 | -------------------------------------------------------------------------------- /test/dummy/config/environment.rb: -------------------------------------------------------------------------------- 1 | # Load the Rails application. 2 | require File.expand_path('../application', __FILE__) 3 | 4 | # Initialize the Rails application. 5 | Dummy::Application.initialize! 6 | -------------------------------------------------------------------------------- /test/dummy/vendor/assets/react/JSXTransformer__.js: -------------------------------------------------------------------------------- 1 | var JSXTransformer = { 2 | transform: function () { 3 | return { 4 | code: 'test_confirmation_token_jsx_transformed;' 5 | }; 6 | } 7 | }; 8 | -------------------------------------------------------------------------------- /gemfiles/rails_4.0_with_therubyracer.gemfile: -------------------------------------------------------------------------------- 1 | # This file was generated by Appraisal 2 | 3 | source "http://rubygems.org" 4 | 5 | gem "rails", "~> 4.0.13" 6 | gem "therubyracer", "0.12.0", :platform => :mri 7 | 8 | gemspec :path => "../" 9 | -------------------------------------------------------------------------------- /lib/react/rails.rb: -------------------------------------------------------------------------------- 1 | require 'react/rails/asset_variant' 2 | require 'react/rails/engine' 3 | require 'react/rails/railtie' 4 | require 'react/rails/version' 5 | require 'react/rails/view_helper' 6 | require 'react/rails/controller_renderer' 7 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | --- 2 | language: ruby 3 | sudo: false 4 | rvm: 5 | - 2.2 6 | - 2.1 7 | - 2.0.0 8 | - 1.9.3 9 | - jruby-19mode 10 | before_script: 'bundle exec appraisal install' 11 | script: 'bundle exec rake appraisal test' 12 | -------------------------------------------------------------------------------- /Guardfile: -------------------------------------------------------------------------------- 1 | guard :minitest do 2 | # with Minitest::Unit 3 | watch(%r{^test/(.*)\/?(.*)_test\.rb$}) 4 | watch(%r{^lib/(.*/)?([^/]+)\.rb$}) { |m| "test/#{m[1]}#{m[2]}_test.rb" } 5 | watch(%r{^test/test_helper\.rb$}) { 'test' } 6 | end 7 | -------------------------------------------------------------------------------- /test/dummy/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 | -------------------------------------------------------------------------------- /test/dummy/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 | # Mime::Type.register_alias "text/html", :iphone 6 | -------------------------------------------------------------------------------- /test/dummy/app/assets/javascripts/components/Todo.js.jsx.coffee: -------------------------------------------------------------------------------- 1 | Todo = React.createClass 2 | render: -> 3 | `
  • {this.props.todo}
  • ` 4 | 5 | # Because Coffee files are in an anonymous function, 6 | # expose it for server rendering tests 7 | window.Todo = Todo 8 | -------------------------------------------------------------------------------- /test/dummy/app/controllers/application_controller.rb: -------------------------------------------------------------------------------- 1 | class ApplicationController < ActionController::Base 2 | # Prevent CSRF attacks by raising an exception. 3 | # For APIs, you may want to use :null_session instead. 4 | protect_from_forgery with: :exception 5 | end 6 | -------------------------------------------------------------------------------- /test/dummy/config/boot.rb: -------------------------------------------------------------------------------- 1 | # Set up gems listed in the Gemfile. 2 | ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../../../../Gemfile', __FILE__) 3 | 4 | require 'bundler/setup' if File.exist?(ENV['BUNDLE_GEMFILE']) 5 | $LOAD_PATH.unshift File.expand_path('../../../../lib', __FILE__) 6 | -------------------------------------------------------------------------------- /test/dummy/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 File.expand_path('../config/application', __FILE__) 5 | 6 | Dummy::Application.load_tasks 7 | -------------------------------------------------------------------------------- /test/dummy/config/routes.rb: -------------------------------------------------------------------------------- 1 | Dummy::Application.routes.draw do 2 | resources :pages, only: [:show] 3 | resources :server, only: [:show] do 4 | collection do 5 | get :console_example 6 | get :console_example_suppressed 7 | get :inline_component 8 | end 9 | end 10 | end 11 | -------------------------------------------------------------------------------- /lib/react/jsx/template.rb: -------------------------------------------------------------------------------- 1 | require 'tilt' 2 | 3 | module React 4 | module JSX 5 | 6 | class Template < Tilt::Template 7 | self.default_mime_type = 'application/javascript' 8 | 9 | def prepare 10 | end 11 | 12 | def evaluate(scope, locals, &block) 13 | @output ||= JSX::transform(data) 14 | end 15 | end 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /lib/react/rails/engine.rb: -------------------------------------------------------------------------------- 1 | module React 2 | module Rails 3 | class Engine < ::Rails::Engine 4 | initializer "react_rails.setup_engine", :group => :all do |app| 5 | sprockets_env = app.assets || Sprockets # Sprockets 3.x expects this in a different place 6 | sprockets_env.register_engine(".jsx", React::JSX::Template) 7 | end 8 | end 9 | end 10 | end 11 | -------------------------------------------------------------------------------- /test/dummy/app/views/layouts/application.html.erb: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Dummy 5 | <%= stylesheet_link_tag "application", media: "all", "data-turbolinks-track" => true %> 6 | <%= javascript_include_tag "application", "data-turbolinks-track" => true %> 7 | <%= csrf_meta_tags %> 8 | 9 | 10 | 11 | <%= yield %> 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /Appraisals: -------------------------------------------------------------------------------- 1 | appraise "rails-3.2" do 2 | gem 'rails', '~> 3.2.21' 3 | end 4 | 5 | appraise "rails-4.0" do 6 | gem 'rails', '~> 4.0.13' 7 | end 8 | 9 | appraise "rails-4.0-with-therubyracer" do 10 | gem 'rails', '~> 4.0.13' 11 | gem 'therubyracer', '0.12.0', :platform => :mri 12 | end 13 | 14 | appraise "rails-4.1" do 15 | gem 'rails', '~> 4.1.10' 16 | end 17 | 18 | appraise "rails-4.2" do 19 | gem 'rails', '~> 4.2.1' 20 | end 21 | -------------------------------------------------------------------------------- /test/dummy/app/assets/javascripts/pages.js: -------------------------------------------------------------------------------- 1 | var HelloMessage = React.createClass({ 2 | getInitialState: function() { 3 | return {greeting: 'Hello'}; 4 | }, 5 | goodbye: function() { 6 | this.setState({greeting: 'Goodbye'}); 7 | }, 8 | render: function() { 9 | return React.DOM.div({}, 10 | React.DOM.div({}, this.state.greeting, ' ', this.props.name), 11 | React.DOM.button({onClick: this.goodbye}, 'Goodbye') 12 | ); 13 | } 14 | }); 15 | -------------------------------------------------------------------------------- /test/dummy/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 | -------------------------------------------------------------------------------- /test/dummy/app/views/pages/show.html.erb: -------------------------------------------------------------------------------- 1 |
      2 |
    • <%= link_to 'Alice', page_path(:id => 0) %>
    • 3 |
    • <%= link_to 'Bob', page_path(:id => 1) %>
    • 4 |
    5 |
    6 | <%= react_component 'HelloMessage', :name => @name %> 7 |
    8 | Unmount at #test-component 9 | Mount at #test-component 10 | -------------------------------------------------------------------------------- /test/dummy/app/assets/javascripts/components/TodoList.js.jsx: -------------------------------------------------------------------------------- 1 | TodoList = React.createClass({ 2 | getInitialState: function() { 3 | return({mounted: "nope"}); 4 | }, 5 | componentWillMount: function() { 6 | this.setState({mounted: 'yep'}); 7 | }, 8 | render: function() { 9 | return ( 10 |
      11 |
    • {this.state.mounted}
    • 12 | {this.props.todos.map(function(todo, i) { 13 | return () 14 | })} 15 |
    16 | ) 17 | } 18 | }) 19 | -------------------------------------------------------------------------------- /test/helper_files/TodoListWithUpdates.js.jsx: -------------------------------------------------------------------------------- 1 | TodoList = React.createClass({ 2 | getInitialState: function() { 3 | return({mounted: "nope"}); 4 | }, 5 | componentWillMount: function() { 6 | this.setState({mounted: 'yep'}); 7 | }, 8 | render: function() { 9 | return ( 10 |
      11 |
    • Updated
    • 12 |
    • {this.state.mounted}
    • 13 | {this.props.todos.map(function(todo, i) { 14 | return () 15 | })} 16 |
    17 | ) 18 | } 19 | }) 20 | -------------------------------------------------------------------------------- /lib/react/rails/controller_renderer.rb: -------------------------------------------------------------------------------- 1 | class React::Rails::ControllerRenderer 2 | include React::Rails::ViewHelper 3 | include ActionView::Helpers::TagHelper 4 | include ActionView::Helpers::TextHelper 5 | 6 | attr_accessor :output_buffer 7 | 8 | def self.call(*args, &block) 9 | new.call(*args, &block) 10 | end 11 | 12 | def call(name, options, &block) 13 | props = options.fetch(:props, {}) 14 | options = options.slice(:data, :tag).merge(prerender: true) 15 | react_component(name, props, options, &block) 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /test/dummy/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] if respond_to?(:wrap_parameters) 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 | -------------------------------------------------------------------------------- /test/dummy/README.rdoc: -------------------------------------------------------------------------------- 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 | 26 | 27 | Please feel free to use a different markup language if you do not plan to run 28 | rake doc:app. 29 | -------------------------------------------------------------------------------- /test/dummy/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, vendor/assets/stylesheets, 6 | * or vendor/assets/stylesheets of plugins, if any, 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 top of the 9 | * compiled file, but it's generally better to create a new file per style scope. 10 | * 11 | *= require_self 12 | *= require_tree . 13 | */ 14 | -------------------------------------------------------------------------------- /test/dummy/app/controllers/server_controller.rb: -------------------------------------------------------------------------------- 1 | class ServerController < ApplicationController 2 | def show 3 | @todos = %w{todo1 todo2 todo3} 4 | end 5 | 6 | def console_example 7 | React::ServerRendering.renderer_options = {replay_console: true} 8 | React::ServerRendering.reset_pool 9 | @todos = %w{todo1 todo2 todo3} 10 | end 11 | 12 | def console_example_suppressed 13 | React::ServerRendering.renderer_options = {replay_console: false} 14 | React::ServerRendering.reset_pool 15 | @todos = %w{todo1 todo2 todo3} 16 | end 17 | 18 | def inline_component 19 | render component: 'TodoList', props: { todos: [{todo: 'Render this inline'}] }, tag: 'span' 20 | end 21 | end 22 | -------------------------------------------------------------------------------- /lib/generators/templates/component.js.jsx: -------------------------------------------------------------------------------- 1 | var <%= file_name.camelize %> = React.createClass({ 2 | <% if attributes.size > 0 -%> 3 | propTypes: { 4 | <% attributes.each_with_index do |attribute, idx| -%> 5 | <%= attribute[:name].camelize(:lower) %>: <%= attribute[:type] %><% if (idx < attributes.length-1) %>,<% end %> 6 | <% end -%> 7 | }, 8 | <% end -%> 9 | 10 | render: function() { 11 | <% if attributes.size > 0 -%> 12 | return ( 13 |
    14 | <% attributes.each do |attribute| -%> 15 |
    <%= attribute[:name].titleize %>: {this.props.<%= attribute[:name].camelize(:lower) %>}
    16 | <% end -%> 17 |
    18 | ); 19 | <% else -%> 20 | return
    ; 21 | <% end -%> 22 | } 23 | }); 24 | -------------------------------------------------------------------------------- /test/dummy/app/assets/javascripts/harmony_example.js.jsx: -------------------------------------------------------------------------------- 1 | var HarmonyComponent = React.createClass({ 2 | statics: { 3 | generateGreeting() { 4 | return "Hello Harmony!" 5 | }, 6 | generateGreetingWithWrapper() { 7 | var insertedGreeting = this.generateGreeting(); 8 | return `Your greeting is: '${insertedGreeting}'.` 9 | }, 10 | }, 11 | render: function(){ 12 | var greeting = HarmonyComponent.generateGreeting(); 13 | var { active, ...other } = { active: true, x: 1, y:2 } 14 | return ( 15 |
    16 |

    {greeting}

    17 |
    18 | 19 |
    20 |
    21 | ) 22 | }, 23 | }); 24 | -------------------------------------------------------------------------------- /test/dummy/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 | -------------------------------------------------------------------------------- /lib/generators/templates/component.es6.jsx: -------------------------------------------------------------------------------- 1 | class <%= file_name.camelize %> extends React.Component { 2 | render () { 3 | <% if attributes.size > 0 -%> 4 | return ( 5 |
    6 | <% attributes.each do |attribute| -%> 7 |
    <%= attribute[:name].titleize %>: {this.props.<%= attribute[:name].camelize(:lower) %>}
    8 | <% end -%> 9 |
    10 | ); 11 | <% else -%> 12 | return
    ; 13 | <% end -%> 14 | } 15 | } 16 | 17 | <% if attributes.size > 0 -%> 18 | <%= file_name.camelize %>.propTypes = { 19 | <% attributes.each_with_index do |attribute, idx| -%> 20 | <%= attribute[:name].camelize(:lower) %>: <%= attribute[:type] %><% if (idx < attributes.length-1) %>,<% end %> 21 | <% end -%> 22 | }; 23 | <% end -%> 24 | -------------------------------------------------------------------------------- /test/dummy/app/assets/javascripts/components/TodoListWithConsoleLog.js.jsx: -------------------------------------------------------------------------------- 1 | TodoListWithConsoleLog = React.createClass({ 2 | getInitialState: function() { 3 | console.log('got initial state'); 4 | return({mounted: "nope"}); 5 | }, 6 | componentWillMount: function() { 7 | console.warn('mounted component'); 8 | this.setState({mounted: 'yep'}); 9 | }, 10 | render: function() { 11 | var x = 'foo'; 12 | console.error('rendered!', x); 13 | return ( 14 |
      15 |
    • Console Logged
    • 16 |
    • {this.state.mounted}
    • 17 | {this.props.todos.map(function(todo, i) { 18 | return () 19 | })} 20 |
    21 | ) 22 | } 23 | }) 24 | -------------------------------------------------------------------------------- /test/dummy/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 | # To learn more, please read the Rails Internationalization guide 20 | # available at http://guides.rubyonrails.org/i18n.html. 21 | 22 | en: 23 | hello: "Hello world" 24 | -------------------------------------------------------------------------------- /lib/react/jsx.rb: -------------------------------------------------------------------------------- 1 | require 'execjs' 2 | require 'react/jsx/template' 3 | require 'react/jsx/jsx_transformer' 4 | require 'react/jsx/babel_transformer' 5 | require 'rails' 6 | 7 | module React 8 | module JSX 9 | DEFAULT_TRANSFORMER = BabelTransformer 10 | mattr_accessor :transform_options, :transformer_class, :transformer 11 | 12 | # You can assign `React::JSX.transformer_class = ` 13 | # to provide your own transformer. It must implement: 14 | # - #initialize(options) 15 | # - #transform(code) => new code 16 | self.transformer_class = DEFAULT_TRANSFORMER 17 | 18 | def self.transform(code) 19 | self.transformer ||= transformer_class.new(transform_options) 20 | self.transformer.transform(code) 21 | end 22 | end 23 | end 24 | -------------------------------------------------------------------------------- /VERSIONS.md: -------------------------------------------------------------------------------- 1 | # Versions 2 | 3 | You can control what version of React.js (and JSXTransformer) is used by `react-rails`: 4 | 5 | - Use the [bundled version](#bundled-versions) that comes with the gem 6 | - [Drop in a copy](#drop-in-version) of React.js 7 | 8 | ## Bundled Versions 9 | 10 | | Gem | React.js | 11 | |----------|----------| 12 | | master | 0.13.3 | 13 | | 1.2.0 | 0.13.3 | 14 | | 1.1.0 | 0.13.3 | 15 | | 1.0.0 | ~> 0.13 | 16 | | 0.13.0.0 | 0.13.0 | 17 | | 0.12.2.0 | 0.12.2 | 18 | | 0.12.1.0 | 0.12.1 | 19 | | 0.12.0.0 | 0.12.0 | 20 | 21 | ## Drop-in Version 22 | 23 | You can also provide your own copies of React.js and JSXTransformer. Just add `react.js` or `JSXTransformer.js` (case-sensitive) files to the asset pipeline (eg, `app/assets/vendor/`). 24 | -------------------------------------------------------------------------------- /test/dummy/config/initializers/secret_token.rb: -------------------------------------------------------------------------------- 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 `rake secret` to generate a secure secret key. 9 | 10 | # Make sure your secret_key_base is kept private 11 | # if you're sharing your code publicly. 12 | Dummy::Application.config.secret_key_base = '43fa5672451bbd0a171668e625edc433eb00eeeb14c2606546e262e499ab853cfb532998d4809abe5019bf13888863e3a2c7d5cf7757de7a2b1fb50826d9874e' 13 | 14 | # For Rails 3.2. 15 | Dummy::Application.config.secret_token = Dummy::Application.config.secret_key_base 16 | -------------------------------------------------------------------------------- /lib/react/rails/asset_variant.rb: -------------------------------------------------------------------------------- 1 | module React 2 | module Rails 3 | class AssetVariant 4 | GEM_ROOT = Pathname.new('../../../../').expand_path(__FILE__) 5 | attr_reader :react_build, :react_directory, :jsx_directory 6 | 7 | def initialize(options={}) 8 | # We want to include different files in dev/prod. The development builds 9 | # contain console logging for invariants and logging to help catch 10 | # common mistakes. These are all stripped out in the production build. 11 | @react_build = options[:variant] == :production ? 'production' : 'development' 12 | options[:addons] && @react_build += '-with-addons' 13 | 14 | @react_directory = GEM_ROOT.join('lib/assets/react-source/').join(@react_build).to_s 15 | @jsx_directory = GEM_ROOT.join('lib/assets/javascripts/').to_s 16 | end 17 | end 18 | end 19 | end 20 | -------------------------------------------------------------------------------- /test/dummy/app/assets/javascripts/application.js: -------------------------------------------------------------------------------- 1 | // This is a manifest file that'll be compiled into application.js, which will include all the files 2 | // listed below. 3 | // 4 | // Any JavaScript/Coffee file within this directory, lib/assets/javascripts, vendor/assets/javascripts, 5 | // or vendor/assets/javascripts of plugins, if any, can be referenced here using a relative path. 6 | // 7 | // It's not advisable to add code directly here, but if you do, it'll appear at the bottom of the 8 | // compiled file. 9 | // 10 | // Read Sprockets README (https://github.com/sstephenson/sprockets#sprockets-directives) for details 11 | // about supported directives. 12 | // 13 | // 14 | // es5-shim is necessary for PhantomJS to pass tests. See https://github.com/facebook/react/issues/303 15 | // 16 | //= require turbolinks 17 | //= require es5-shim/es5-shim 18 | //= require react 19 | //= require react_ujs 20 | //= require_tree ./components 21 | //= require ./pages 22 | -------------------------------------------------------------------------------- /test/react/rails/asset_variant_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | class AssetVariantTest < ActiveSupport::TestCase 4 | def build_variant(options) 5 | React::Rails::AssetVariant.new(options) 6 | end 7 | 8 | test 'it points to different directories for react' do 9 | production_variant = build_variant(variant: :production) 10 | assert_match(%r{/lib/assets/react-source/production}, production_variant.react_directory) 11 | 12 | development_variant = build_variant(variant: nil) 13 | assert_match(%r{/lib/assets/react-source/development}, development_variant.react_directory) 14 | end 15 | 16 | test 'points to jsx transformer' do 17 | variant = build_variant({}) 18 | assert_match(%r{/lib/assets/javascripts/}, variant.jsx_directory) 19 | end 20 | 21 | test 'it includes addons if requested' do 22 | asset_variant = build_variant(addons: true) 23 | assert_equal "development-with-addons", asset_variant.react_build 24 | end 25 | end 26 | -------------------------------------------------------------------------------- /lib/react/jsx/babel_transformer.rb: -------------------------------------------------------------------------------- 1 | require 'babel/transpiler' 2 | module React 3 | module JSX 4 | class BabelTransformer 5 | DEPRECATED_OPTIONS = [:harmony, :strip_types, :asset_path] 6 | DEFAULT_TRANSFORM_OPTIONS = { blacklist: ['spec.functionName', 'validation.react', 'strict'] } 7 | def initialize(options) 8 | if (options.keys & DEPRECATED_OPTIONS).any? 9 | ActiveSupport::Deprecation.warn("Setting config.react.jsx_transform_options for :harmony, :strip_types, and :asset_path keys is now deprecated and has no effect with the default Babel Transformer."+ 10 | "Please use new Babel Transformer options :whitelist, :plugin instead.") 11 | end 12 | 13 | @transform_options = DEFAULT_TRANSFORM_OPTIONS.merge(options) 14 | end 15 | 16 | def transform(code) 17 | Babel::Transpiler.transform(code, @transform_options)['code'] 18 | end 19 | end 20 | end 21 | end 22 | -------------------------------------------------------------------------------- /lib/react/server_rendering.rb: -------------------------------------------------------------------------------- 1 | require 'connection_pool' 2 | require 'react/server_rendering/exec_js_renderer' 3 | require 'react/server_rendering/sprockets_renderer' 4 | 5 | module React 6 | module ServerRendering 7 | mattr_accessor :renderer, :renderer_options, 8 | :pool_size, :pool_timeout 9 | 10 | def self.reset_pool 11 | options = {size: pool_size, timeout: pool_timeout} 12 | @@pool = ConnectionPool.new(options) { create_renderer } 13 | end 14 | 15 | def self.render(component_name, props, prerender_options) 16 | @@pool.with do |renderer| 17 | renderer.render(component_name, props, prerender_options) 18 | end 19 | end 20 | 21 | def self.create_renderer 22 | renderer.new(renderer_options) 23 | end 24 | 25 | class PrerenderError < RuntimeError 26 | def initialize(component_name, props, js_message) 27 | message = ["Encountered error \"#{js_message}\" when prerendering #{component_name} with #{props}", 28 | js_message.backtrace.join("\n")].join("\n") 29 | super(message) 30 | end 31 | end 32 | end 33 | end -------------------------------------------------------------------------------- /test/dummy/config/environments/development.rb: -------------------------------------------------------------------------------- 1 | Dummy::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 and disable caching. 13 | config.consider_all_requests_local = true 14 | config.action_controller.perform_caching = false 15 | 16 | # Don't care if the mailer can't send. 17 | config.action_mailer.raise_delivery_errors = false 18 | 19 | # Print deprecation notices to the Rails logger. 20 | config.active_support.deprecation = :log 21 | 22 | # Debug mode disables concatenation and preprocessing of assets. 23 | # This option may cause significant delays in view rendering with a large 24 | # number of complex assets. 25 | config.assets.debug = true 26 | end 27 | -------------------------------------------------------------------------------- /lib/react/jsx/jsx_transformer.rb: -------------------------------------------------------------------------------- 1 | module React 2 | module JSX 3 | class JSXTransformer 4 | DEFAULT_ASSET_PATH = 'JSXTransformer.js' 5 | 6 | def initialize(options) 7 | @transform_options = { 8 | stripTypes: options.fetch(:strip_types, false), 9 | harmony: options.fetch(:harmony, false), 10 | } 11 | 12 | @asset_path = options.fetch(:asset_path, DEFAULT_ASSET_PATH) 13 | 14 | # If execjs uses therubyracer, there is no 'global'. Make sure 15 | # we have it so JSX script can work properly. 16 | js_code = 'var global = global || this;' + jsx_transform_code 17 | @context = ExecJS.compile(js_code) 18 | end 19 | 20 | 21 | def transform(code) 22 | result = @context.call('JSXTransformer.transform', code, @transform_options) 23 | result["code"] 24 | end 25 | 26 | def jsx_transform_code 27 | # search for transformer file using sprockets - allows user to override 28 | # this file in his own application 29 | ::Rails.application.assets[@asset_path].to_s 30 | end 31 | end 32 | end 33 | end 34 | -------------------------------------------------------------------------------- /lib/react/rails/view_helper.rb: -------------------------------------------------------------------------------- 1 | module React 2 | module Rails 3 | module ViewHelper 4 | # Render a UJS-type HTML tag annotated with data attributes, which 5 | # are used by react_ujs to actually instantiate the React component 6 | # on the client. 7 | def react_component(name, props = {}, options = {}, &block) 8 | options = {:tag => options} if options.is_a?(Symbol) 9 | 10 | prerender_options = options[:prerender] 11 | if prerender_options 12 | block = Proc.new{ concat React::ServerRendering.render(name, props, prerender_options) } 13 | end 14 | 15 | html_options = options.reverse_merge(:data => {}) 16 | html_options[:data].tap do |data| 17 | data[:react_class] = name 18 | data[:react_props] = (props.is_a?(String) ? props : props.to_json) 19 | end 20 | html_tag = html_options[:tag] || :div 21 | 22 | # remove internally used properties so they aren't rendered to DOM 23 | html_options.except!(:tag, :prerender) 24 | 25 | content_tag(html_tag, '', html_options, &block) 26 | end 27 | end 28 | end 29 | end 30 | -------------------------------------------------------------------------------- /test/generators/es6_component_generator_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | require 'generators/react/component_generator' 3 | 4 | class Es6ComponentGeneratorTest < Rails::Generators::TestCase 5 | destination File.join(Rails.root, 'tmp', 'component_generator_test_output') 6 | setup :prepare_destination 7 | tests React::Generators::ComponentGenerator 8 | 9 | def filename 10 | 'app/assets/javascripts/components/generated_component.es6.jsx' 11 | end 12 | 13 | def class_name 14 | 'GeneratedComponent' 15 | end 16 | 17 | test "uses es6 syntax" do 18 | run_generator %w(GeneratedComponent name --es6) 19 | 20 | assert_file filename, /^class\s#{class_name}\sextends\sReact\.Component/ 21 | end 22 | 23 | test "assigns defaultProps after class definintion" do 24 | run_generator %w(GeneratedComponent name --es6) 25 | 26 | assert_file filename, /\s^#{class_name}\.propTypes/ 27 | end 28 | 29 | test "generates working jsx" do 30 | expected_name_div = /React\.createElement\(\s*"div",\s*null,\s*\"Name:\s*\",\s*this\.props\.name\s*\)/x 31 | expected_shape_div = /React\.createElement\(\s*"div",\s*null,\s*\"Address:\s*\",\s*this\.props\.address\s*\)/x 32 | 33 | run_generator %w(GeneratedComponent name:string address:shape --es6) 34 | jsx = React::JSX.transform(File.read(File.join(destination_root, filename))) 35 | 36 | assert_match(Regexp.new(expected_name_div), jsx) 37 | assert_match(Regexp.new(expected_shape_div), jsx) 38 | end 39 | end 40 | -------------------------------------------------------------------------------- /test/dummy/public/500.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | We're sorry, but something went wrong (500) 5 | 48 | 49 | 50 | 51 | 52 |
    53 |

    We're sorry, but something went wrong.

    54 |
    55 |

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

    56 | 57 | 58 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | begin 2 | require 'bundler/setup' 3 | rescue LoadError 4 | puts 'You must `gem install bundler` and `bundle install` to run rake tasks' 5 | end 6 | 7 | Bundler::GemHelper.install_tasks 8 | 9 | require 'pathname' 10 | namespace :react do 11 | task :update do 12 | FileUtils.rm_f('vendor/react/.bower.json') 13 | `bower install react` 14 | assets_path = Pathname.new(File.dirname(__FILE__)).join('lib/assets/') 15 | copy_react_asset('JSXTransformer.js', assets_path.join('javascripts/JSXTransformer.js')) 16 | copy_react_asset('react.js', assets_path.join('react-source/development/react.js')) 17 | copy_react_asset('react.min.js', assets_path.join('react-source/production/react.js')) 18 | copy_react_asset('react-with-addons.js', assets_path.join('react-source/development-with-addons/react.js')) 19 | copy_react_asset('react-with-addons.min.js', assets_path.join('react-source/production-with-addons/react.js')) 20 | end 21 | 22 | def copy_react_asset(source, destination) 23 | vendor_path = Pathname.new(File.dirname(__FILE__)).join('vendor/react') 24 | FileUtils.mkdir_p(destination.dirname.to_s) 25 | FileUtils.cp(vendor_path.join(source), destination.to_s) 26 | end 27 | end 28 | 29 | require 'appraisal' 30 | require 'rake/testtask' 31 | 32 | Rake::TestTask.new(:test) do |t| 33 | t.libs << 'lib' 34 | t.libs << 'test' 35 | t.pattern = ENV['TEST_PATTERN'] || 'test/**/*_test.rb' 36 | t.verbose = ENV['TEST_VERBOSE'] == '1' 37 | t.warning = true 38 | end 39 | 40 | task default: :test 41 | -------------------------------------------------------------------------------- /react-rails.gemspec: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | 3 | $:.push File.expand_path('../lib', __FILE__) 4 | require 'react/rails/version' 5 | 6 | Gem::Specification.new do |s| 7 | s.name = 'react-rails' 8 | s.version = React::Rails::VERSION 9 | s.summary = 'React/JSX adapter for the Ruby on Rails asset pipeline.' 10 | s.description = 'Compile your JSX on demand or precompile for production.' 11 | s.homepage = 'https://github.com/reactjs/react-rails' 12 | s.license = 'Apache-2.0' 13 | 14 | s.author = ['Paul O’Shannessy'] 15 | s.email = ['paul@oshannessy.com'] 16 | 17 | s.add_development_dependency 'appraisal' 18 | s.add_development_dependency 'bundler', '>= 1.2.2' 19 | s.add_development_dependency 'codeclimate-test-reporter' 20 | s.add_development_dependency 'coffee-rails' 21 | s.add_development_dependency 'es5-shim-rails', '>= 2.0.5' 22 | s.add_development_dependency 'guard' 23 | s.add_development_dependency 'guard-minitest' 24 | s.add_development_dependency 'jbuilder' 25 | s.add_development_dependency 'poltergeist', '>= 0.3.3' 26 | s.add_development_dependency 'test-unit', '~> 2.5' 27 | s.add_development_dependency 'turbolinks', '>= 2.0.0' 28 | 29 | s.add_dependency 'coffee-script-source', '~>1.8' 30 | s.add_dependency 'connection_pool' 31 | s.add_dependency 'execjs' 32 | s.add_dependency 'rails', '>= 3.2' 33 | s.add_dependency 'tilt' 34 | s.add_dependency 'babel-transpiler', '>=0.7.0' 35 | 36 | s.files = Dir[ 37 | 'lib/**/*', 38 | 'README.md', 39 | 'LICENSE' 40 | ] 41 | 42 | s.require_paths = ['lib'] 43 | end 44 | -------------------------------------------------------------------------------- /test/dummy/config/application.rb: -------------------------------------------------------------------------------- 1 | require File.expand_path('../boot', __FILE__) 2 | 3 | # Pick the frameworks you want: 4 | # require "active_record/railtie" 5 | require "action_controller/railtie" 6 | require "action_mailer/railtie" 7 | require "sprockets/railtie" 8 | require "rails/test_unit/railtie" 9 | 10 | # Make sure gems in development group are required, for example, react-rails and turbolinks. 11 | # These gems are specified in .gemspec file by add_development_dependency. They are not runtime 12 | # dependencies for react-rails project but probably runtime dependencies for this dummy rails app. 13 | Bundler.require(*(Rails.groups | ['development'])) 14 | 15 | module Dummy 16 | class Application < Rails::Application 17 | # Settings in config/environments/* take precedence over those specified here. 18 | # Application configuration should go into files in config/initializers 19 | # -- all .rb files in that directory are automatically loaded. 20 | 21 | # Set Time.zone default to the specified zone and make Active Record auto-convert to this zone. 22 | # Run "rake -D time" for a list of tasks for finding time zone names. Default is UTC. 23 | # config.time_zone = 'Central Time (US & Canada)' 24 | 25 | # The default locale is :en and all translations from config/locales/*.rb,yml are auto loaded. 26 | # config.i18n.load_path += Dir[Rails.root.join('my', 'locales', '*.{rb,yml}').to_s] 27 | # config.i18n.default_locale = :de 28 | config.react.variant = :production 29 | 30 | config.assets.enabled = true 31 | end 32 | end 33 | -------------------------------------------------------------------------------- /test/react_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | class ReactTest < ActionDispatch::IntegrationTest 3 | setup do 4 | clear_sprockets_cache 5 | end 6 | 7 | teardown do 8 | clear_sprockets_cache 9 | end 10 | 11 | test 'asset pipeline should deliver drop-in react file replacement' do 12 | app_react_file_path = File.expand_path("../dummy/vendor/assets/javascripts/react.js", __FILE__) 13 | react_file_token = "'test_confirmation_token_react_content_non_production';\n" 14 | File.write(app_react_file_path, react_file_token) 15 | manually_expire_asset("react.js") 16 | react_asset = Rails.application.assets['react.js'] 17 | 18 | get '/assets/react.js' 19 | 20 | File.unlink(app_react_file_path) 21 | 22 | assert_response :success 23 | assert_equal react_file_token.length, react_asset.to_s.length, "The asset pipeline serves the drop-in file" 24 | assert_equal react_file_token.length, @response.body.length, "The asset route serves the drop-in file" 25 | end 26 | 27 | test 'precompiling assets works' do 28 | begin 29 | ENV['RAILS_GROUPS'] = 'assets' # required for Rails 3.2 30 | Dummy::Application.load_tasks 31 | Rake::Task['assets:precompile'].invoke 32 | FileUtils.rm_r(File.expand_path("../dummy/public/assets", __FILE__)) 33 | ensure 34 | ENV.delete('RAILS_GROUPS') 35 | end 36 | end 37 | 38 | test "the development version is loaded" do 39 | asset = Rails.application.assets.find_asset('react') 40 | assert asset.pathname.to_s.end_with?('development/react.js') 41 | end 42 | end 43 | -------------------------------------------------------------------------------- /test/dummy/public/422.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | The change you wanted was rejected (422) 5 | 48 | 49 | 50 | 51 | 52 |
    53 |

    The change you wanted was rejected.

    54 |

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

    55 |
    56 |

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

    57 | 58 | 59 | -------------------------------------------------------------------------------- /test/dummy/public/404.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | The page you were looking for doesn't exist (404) 5 | 48 | 49 | 50 | 51 | 52 |
    53 |

    The page you were looking for doesn't exist.

    54 |

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

    55 |
    56 |

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

    57 | 58 | 59 | -------------------------------------------------------------------------------- /lib/react/server_rendering/exec_js_renderer.rb: -------------------------------------------------------------------------------- 1 | # A bare-bones renderer for React.js + Exec.js 2 | # - No Rails dependency 3 | # - No browser concerns 4 | module React 5 | module ServerRendering 6 | class ExecJSRenderer 7 | def initialize(options={}) 8 | js_code = options[:code] || raise("Pass `code:` option to instantiate a JS context!") 9 | @context = ExecJS.compile(GLOBAL_WRAPPER + js_code) 10 | end 11 | 12 | def render(component_name, props, prerender_options) 13 | render_function = prerender_options.fetch(:render_function, "renderToString") 14 | js_code = <<-JS 15 | (function () { 16 | #{before_render(component_name, props, prerender_options)} 17 | var result = React.#{render_function}(React.createElement(#{component_name}, #{props})); 18 | #{after_render(component_name, props, prerender_options)} 19 | return result; 20 | })() 21 | JS 22 | @context.eval(js_code).html_safe 23 | rescue ExecJS::ProgramError => err 24 | raise React::ServerRendering::PrerenderError.new(component_name, props, err) 25 | end 26 | 27 | # Hooks for inserting JS before/after rendering 28 | def before_render(component_name, props, prerender_options); ""; end 29 | def after_render(component_name, props, prerender_options); ""; end 30 | 31 | # Handle Node.js & other ExecJS contexts 32 | GLOBAL_WRAPPER = <<-JS 33 | var global = global || this; 34 | var self = self || this; 35 | var window = window || this; 36 | JS 37 | 38 | end 39 | end 40 | end 41 | -------------------------------------------------------------------------------- /test/generators/install_generator_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | require 'generators/react/install_generator' 3 | 4 | class InstallGeneratorTest < Rails::Generators::TestCase 5 | destination File.join(Rails.root, 'tmp', 'generator_test_output') 6 | tests React::Generators::InstallGenerator 7 | 8 | def copy_directory(dir) 9 | source = Rails.root.join(dir) 10 | dest = Rails.root.join(destination_root, File.dirname(dir)) 11 | 12 | FileUtils.mkdir_p dest 13 | FileUtils.cp_r source, dest 14 | end 15 | 16 | test "adds requires to `application.js`" do 17 | run_generator 18 | 19 | assert_application_file_modified 20 | end 21 | 22 | test "it modifes an existing 'application.js'" do 23 | copy_directory('app/assets/javascripts/application.js') 24 | run_generator 25 | assert_application_file_modified 26 | end 27 | 28 | test "creates `application.js` if it doesn't exist" do 29 | copy_directory('app/assets/javascripts/application.js') 30 | File.delete destination_root + '/app/assets/javascripts/application.js' 31 | 32 | run_generator 33 | 34 | assert_application_file_modified 35 | end 36 | 37 | test "modifies `application.js` it's empty" do 38 | File.write(destination_root + '/app/assets/javascripts/application.js', '') 39 | 40 | run_generator 41 | 42 | assert_application_file_modified 43 | end 44 | 45 | def assert_application_file_modified 46 | assert_file 'app/assets/javascripts/application.js', %r{//= require react} 47 | assert_file 'app/assets/javascripts/application.js', %r{//= require react_ujs} 48 | assert_file 'app/assets/javascripts/application.js', %r{//= require components} 49 | end 50 | end 51 | -------------------------------------------------------------------------------- /test/test_helper.rb: -------------------------------------------------------------------------------- 1 | if RUBY_PLATFORM != "java" 2 | require "codeclimate-test-reporter" 3 | CodeClimate::TestReporter.start 4 | end 5 | 6 | # Configure Rails Environment 7 | ENV["RAILS_ENV"] = "test" 8 | 9 | require File.expand_path("../dummy/config/environment.rb", __FILE__) 10 | require "rails/test_help" 11 | require "rails/generators" 12 | require "pathname" 13 | require 'minitest/mock' 14 | 15 | CACHE_PATH = Pathname.new File.expand_path("../dummy/tmp/cache", __FILE__) 16 | 17 | Rails.backtrace_cleaner.remove_silencers! 18 | 19 | def clear_sprockets_cache 20 | # Remove cached files 21 | Rails.root.join('tmp/cache').tap do |tmp| 22 | tmp.rmtree if tmp.exist? 23 | tmp.mkpath 24 | end 25 | end 26 | 27 | def reset_transformer 28 | clear_sprockets_cache 29 | React::JSX.transformer_class = React::JSX::DEFAULT_TRANSFORMER 30 | React::JSX.transform_options = {} 31 | React::JSX.transformer = nil 32 | end 33 | 34 | # Sprockets 2 doesn't expire this assets well in 35 | # this kind of setting, 36 | # so override `fresh?` to mark it as expired. 37 | def manually_expire_asset(asset_name) 38 | asset = Rails.application.assets[asset_name] 39 | def asset.fresh?(env); false; end 40 | end 41 | 42 | # Load support files 43 | Dir["#{File.dirname(__FILE__)}/support/**/*.rb"].each { |f| require f } 44 | 45 | # Load fixtures from the engine 46 | if ActiveSupport::TestCase.method_defined?(:fixture_path=) 47 | ActiveSupport::TestCase.fixture_path = File.expand_path("../fixtures", __FILE__) 48 | end 49 | 50 | if ActiveSupport::TestCase.respond_to?(:test_order=) 51 | ActiveSupport::TestCase.test_order = :random 52 | end 53 | 54 | def wait_for_turbolinks_to_be_available 55 | sleep(1) 56 | end 57 | -------------------------------------------------------------------------------- /test/dummy/config/environments/test.rb: -------------------------------------------------------------------------------- 1 | Dummy::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 | 9 | # we need this to reload the jsx transformer when different version is dropped in 10 | config.cache_classes = false 11 | config.reload_plugins = true 12 | config.assets.cache_store = :null_store 13 | 14 | # Do not eager load code on boot. This avoids loading your whole application 15 | # just for the purpose of running a single test. If you are using a tool that 16 | # preloads Rails for running tests, you may have to set it to true. 17 | config.eager_load = false 18 | 19 | # Configure static asset server for tests with Cache-Control for performance. 20 | # Disabled since we dont use it and this option is deprecated from Rails 4.2 onwards 21 | # config.serve_static_assets = true 22 | config.static_cache_control = "public, max-age=3600" 23 | 24 | # Show full error reports and disable caching. 25 | config.consider_all_requests_local = true 26 | config.action_controller.perform_caching = false 27 | 28 | # Raise exceptions instead of rendering exception templates. 29 | config.action_dispatch.show_exceptions = false 30 | 31 | # Disable request forgery protection in test environment. 32 | config.action_controller.allow_forgery_protection = false 33 | 34 | # Tell Action Mailer not to deliver emails to the real world. 35 | # The :test delivery method accumulates sent emails in the 36 | # ActionMailer::Base.deliveries array. 37 | config.action_mailer.delivery_method = :test 38 | 39 | # Print deprecation notices to the stderr. 40 | config.active_support.deprecation = :stderr 41 | 42 | config.react.variant = :test 43 | end 44 | -------------------------------------------------------------------------------- /lib/generators/react/install_generator.rb: -------------------------------------------------------------------------------- 1 | module React 2 | module Generators 3 | class InstallGenerator < ::Rails::Generators::Base 4 | source_root File.expand_path '../../templates', __FILE__ 5 | 6 | desc 'Create default react.js folder layout and prep application.js' 7 | 8 | class_option :skip_git, 9 | type: :boolean, 10 | aliases: '-g', 11 | default: false, 12 | desc: 'Skip Git keeps' 13 | 14 | def create_directory 15 | empty_directory 'app/assets/javascripts/components' 16 | create_file 'app/assets/javascripts/components/.gitkeep' unless options[:skip_git] 17 | end 18 | 19 | def inject_react 20 | require_react = "//= require react\n" 21 | 22 | if manifest.exist? 23 | manifest_contents = File.read(manifest) 24 | 25 | if manifest_contents.include? 'require turbolinks' 26 | inject_into_file manifest, require_react, {after: "//= require turbolinks\n"} 27 | elsif manifest_contents.include? 'require_tree' 28 | inject_into_file manifest, require_react, {before: '//= require_tree'} 29 | else 30 | append_file manifest, require_react 31 | end 32 | else 33 | create_file manifest, require_react 34 | end 35 | end 36 | 37 | def inject_components 38 | inject_into_file manifest, "//= require components\n", {after: "//= require react\n"} 39 | end 40 | 41 | def inject_react_ujs 42 | inject_into_file manifest, "//= require react_ujs\n", {after: "//= require react\n"} 43 | end 44 | 45 | def create_components 46 | components_js = "//= require_tree ./components\n" 47 | components_file = File.join(*%w(app assets javascripts components.js)) 48 | create_file components_file, components_js 49 | end 50 | 51 | private 52 | 53 | def manifest 54 | Pathname.new(destination_root).join('app/assets/javascripts', 'application.js') 55 | end 56 | end 57 | end 58 | end 59 | -------------------------------------------------------------------------------- /test/react/jsx_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | require 'fileutils' 3 | 4 | # Sprockets is inserting a newline after the docblock for some reason... 5 | EXPECTED_JS = < 0) { 53 | result += '\\n'; 54 | history.forEach(function (msg) { 55 | result += '\\nconsole.' + msg.level + '.apply(console, ' + JSON.stringify(msg.arguments) + ');'; 56 | }); 57 | result += '\\n'; 58 | } 59 | })(console.history); 60 | JS 61 | end 62 | end 63 | end 64 | -------------------------------------------------------------------------------- /test/react/jsx/jsx_transformer_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | class JSXTransformerTest < ActionDispatch::IntegrationTest 4 | setup do 5 | reset_transformer 6 | React::JSX.transformer_class = React::JSX::JSXTransformer 7 | end 8 | 9 | teardown do 10 | reset_transformer 11 | end 12 | 13 | test 'can use dropped-in version of JSX transformer' do 14 | hidden_path = Rails.root.join("vendor/assets/react/JSXTransformer__.js") 15 | replacing_path = Rails.root.join("vendor/assets/react/JSXTransformer.js") 16 | 17 | FileUtils.cp hidden_path, replacing_path 18 | get '/assets/example3.js' 19 | FileUtils.rm replacing_path 20 | 21 | assert_response :success 22 | assert_equal 'test_confirmation_token_jsx_transformed;', @response.body 23 | end 24 | 25 | test 'accepts harmony: true option' do 26 | React::JSX.transform_options = {harmony: true} 27 | get '/assets/harmony_example.js' 28 | assert_response :success 29 | assert_match(/generateGreeting:\s*function\(\)/, @response.body, "object literal methods") 30 | assert_match(/React.__spread/, @response.body, "spreading props") 31 | assert_match(/Your greeting is: '" \+ insertedGreeting \+ "'/, @response.body, "string interpolation") 32 | assert_match(/active=\$__0\.active/, @response.body, "destructuring assignment") 33 | end 34 | 35 | test 'accepts strip_types: true option' do 36 | React::JSX.transform_options = {strip_types: true, harmony: true} 37 | get '/assets/flow_types_example.js' 38 | assert_response :success 39 | assert_match(/\(i\s*,\s*name\s*\)\s*\{/, @response.body, "type annotations are removed") 40 | end 41 | 42 | test 'accepts asset_path: option' do 43 | hidden_path = Rails.root.join("vendor/assets/react/JSXTransformer__.js") 44 | custom_path = Rails.root.join("vendor/assets/react/custom") 45 | replacing_path = custom_path.join("CustomTransformer.js") 46 | 47 | React::JSX.transform_options = {asset_path: "custom/CustomTransformer.js"} 48 | 49 | FileUtils.mkdir_p(custom_path) 50 | FileUtils.cp(hidden_path, replacing_path) 51 | get '/assets/example3.js' 52 | 53 | FileUtils.rm_rf custom_path 54 | assert_response :success 55 | assert_equal 'test_confirmation_token_jsx_transformed;', @response.body 56 | end 57 | 58 | end 59 | -------------------------------------------------------------------------------- /test/react/server_rendering_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | class NullRenderer 4 | def initialize(options) 5 | # in this case, options is actually a string (just for testing) 6 | @name = options 7 | end 8 | 9 | def render(component_name, props, prerender_options) 10 | "#{@name} rendered #{component_name} with #{props} and #{prerender_options}" 11 | end 12 | end 13 | 14 | class ReactServerRenderingTest < ActiveSupport::TestCase 15 | setup do 16 | @previous_renderer = React::ServerRendering.renderer 17 | @previous_options = React::ServerRendering.renderer_options 18 | React::ServerRendering.renderer_options = "TEST" 19 | React::ServerRendering.renderer = NullRenderer 20 | React::ServerRendering.reset_pool 21 | end 22 | 23 | teardown do 24 | React::ServerRendering.renderer = @previous_renderer 25 | React::ServerRendering.renderer_options = @previous_options 26 | React::ServerRendering.reset_pool 27 | end 28 | 29 | test '.create_renderer makes a renderer with initialization options' do 30 | mock_renderer = Minitest::Mock.new 31 | mock_renderer.expect(:new, :fake_renderer, [{mock: true}]) 32 | React::ServerRendering.renderer = mock_renderer 33 | React::ServerRendering.renderer_options = {mock: true} 34 | renderer = React::ServerRendering.create_renderer 35 | assert_equal(:fake_renderer, renderer) 36 | end 37 | 38 | test '.render returns a rendered string' do 39 | props = {"props" => true} 40 | result = React::ServerRendering.render("MyComponent", props, "prerender-opts") 41 | assert_equal("TEST rendered MyComponent with #{props} and prerender-opts", result) 42 | end 43 | 44 | test '.reset_pool forgets old renderers' do 45 | # At first, they use the first options: 46 | assert_match(/^TEST/, React::ServerRendering.render(nil, nil, nil)) 47 | assert_match(/^TEST/, React::ServerRendering.render(nil, nil, nil)) 48 | 49 | # Then change the init options and clear the pool: 50 | React::ServerRendering.renderer_options = "DIFFERENT" 51 | React::ServerRendering.reset_pool 52 | # New renderers are created with the new init options: 53 | assert_match(/^DIFFERENT/, React::ServerRendering.render(nil, nil, nil)) 54 | assert_match(/^DIFFERENT/, React::ServerRendering.render(nil, nil, nil)) 55 | end 56 | end -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # react-rails 2 | 3 | #### Breaking Changes 4 | 5 | #### New Features 6 | 7 | - Render components directly from the controller with `render component: ...` #329 8 | 9 | #### Deprecation 10 | 11 | #### Bug Fixes 12 | 13 | ## 1.2.0 (August 19, 2015) 14 | 15 | #### Breaking Changes 16 | 17 | #### New Features 18 | 19 | - Support `--es6` option in component generator #332 20 | - Support Sprockets 3 #322 21 | 22 | #### Deprecation 23 | 24 | #### Bug Fixes 25 | 26 | - Don't bother unmounting components `onBeforeUnload` #318 27 | - Include `React::Rails::VERSION` in the gem #335 28 | 29 | ## 1.1.0 (July 9, 2015) 30 | 31 | #### Breaking Changes 32 | 33 | - Changed server rendering configuration names #253 34 | 35 | | Old | New | 36 | | ---- | ---- | 37 | | `config.react.timeout` | `config.react.server_renderer_timeout` | 38 | | `config.react.max_renderers` | `config.react.server_renderer_pool_size` | 39 | | `config.react.react_js` | `config.react.server_renderer_options[:files]` | 40 | | `config.react.component_filenames` | `config.react.server_renderer_options[:files]` | 41 | | `config.react.replay_console` | `config.react.server_renderer_options[:replay_console]` | 42 | | (none) | `config.react.server_renderer` | 43 | 44 | - JSX is transformed by Babel, not JSTransform #295 45 | 46 | #### New Features 47 | 48 | - Allow custom renderers for server rendering #253 49 | - Server render with `renderToStaticMarkup` via `prerender: :static` #253 50 | - Accept `config.react.jsx_transform_options = {asset_path: "path/to/JSXTransformer.js"}` #273 51 | - Added `BabelTransformer` for transforming JSX #295 52 | - Added `ExecJSRenderer` to server rendering tools 53 | - Accept `config.react.jsx_transformer_class` #302 54 | 55 | #### Deprecations 56 | 57 | - `JSXTransformer` won't be updated 58 | 59 | #### Bug Fixes 60 | 61 | - Fix gem versions in tests #270 62 | - Expire the Sprockets cache if you change React.js builds #257 63 | - Instead of copying builds into local directires, add different React.js builds to the asset pipeline #254 64 | - Camelize attribute names in the component generator #262 65 | - Add `tilt` dependency #248 66 | - Default prerender pool size to 1 #302 67 | 68 | 69 | ## 1.0.0 (April 7, 2015) 70 | 71 | #### New Features 72 | 73 | - Vendor versions of React.js with `config.variant`, `config.addons` and `//= require react` 74 | - Component generator 75 | - View helper and UJS for mounting components 76 | - Server rendering with `prerender: true` 77 | - Transform `.jsx` in the asset pipeline 78 | -------------------------------------------------------------------------------- /test/generators/component_generator_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | require 'generators/react/component_generator' 3 | 4 | class ComponentGeneratorTest < Rails::Generators::TestCase 5 | destination File.join(Rails.root, 'tmp', 'component_generator_test_output') 6 | setup :prepare_destination 7 | tests React::Generators::ComponentGenerator 8 | 9 | def filename 10 | 'app/assets/javascripts/components/generated_component.js.jsx' 11 | end 12 | 13 | test "creates the component file" do 14 | run_generator %w(GeneratedComponent) 15 | 16 | assert_file filename 17 | end 18 | 19 | test "creates the component file with a node argument" do 20 | run_generator %w(GeneratedComponent name) 21 | assert_file filename, %r{name: React.PropTypes.node} 22 | end 23 | 24 | test "creates the component file with various standard proptypes" do 25 | proptypes = %w(string bool number array func number object any) 26 | run_generator %w(GeneratedComponent) + proptypes.map { |type| "my_#{type}:#{type}" } 27 | proptypes.each do |type| 28 | assert_file filename, %r(my#{type.capitalize}: React.PropTypes.#{type}) 29 | end 30 | end 31 | 32 | test "creates a component file with an instanceOf property" do 33 | run_generator %w(GeneratedComponent favorite_food:instanceOf{food}) 34 | assert_file filename, /favoriteFood: React.PropTypes.instanceOf\(Food\)/ 35 | end 36 | 37 | test "creates a component file with a oneOf property" do 38 | run_generator %w(GeneratedComponent favorite_food:oneOf{pizza,hamburgers}) 39 | assert_file filename, /favoriteFood: React.PropTypes.oneOf\(\['pizza','hamburgers'\]\)/ 40 | end 41 | 42 | test "creates a component file with a oneOfType property" do 43 | run_generator %w(GeneratedComponent favorite_food:oneOfType{string,Food}) 44 | expected_property = "favoriteFood: React.PropTypes.oneOfType([React.PropTypes.string,React.PropTypes.instanceOf(Food)])" 45 | 46 | assert_file filename, Regexp.new(Regexp.quote(expected_property)) 47 | end 48 | 49 | test "generates working jsx" do 50 | expected_name_div = /React\.createElement\(\s*"div",\s*null,\s*\"Name:\s*\",\s*this\.props\.name\s*\)/x 51 | expected_shape_div = /React\.createElement\(\s*"div",\s*null,\s*\"Address:\s*\",\s*this\.props\.address\s*\)/x 52 | 53 | run_generator %w(GeneratedComponent name:string address:shape) 54 | jsx = React::JSX.transform(File.read(File.join(destination_root, filename))) 55 | 56 | assert_match(Regexp.new(expected_name_div), jsx) 57 | assert_match(Regexp.new(expected_shape_div), jsx) 58 | end 59 | end 60 | -------------------------------------------------------------------------------- /test/react/server_rendering/exec_js_renderer_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | DUMMY_IMPLEMENTATION = " 4 | var Todo = null 5 | var React = { 6 | renderToString: function() { 7 | return 'renderToString was called' 8 | }, 9 | createElement: function() {} 10 | } 11 | " 12 | 13 | class ExecJSRendererTest < ActiveSupport::TestCase 14 | setup do 15 | react_source = Rails.application.assets["react.js"].to_s 16 | todo_component_source = Rails.application.assets["components/Todo.js"].to_s 17 | @renderer = React::ServerRendering::ExecJSRenderer.new(code: react_source + todo_component_source) 18 | end 19 | 20 | test '#render returns HTML' do 21 | result = @renderer.render("Todo", {todo: "write tests"}.to_json, {}) 22 | assert_match(//, result) 23 | assert_match(/data-react-checksum/, result) 24 | end 25 | 26 | test '#render accepts render_function:' do 27 | result = @renderer.render("Todo", {todo: "write more tests"}.to_json, render_function: "renderToStaticMarkup") 28 | assert_match(/
  • write more tests<\/li>/, result) 29 | assert_no_match(/data-react-checksum/, result) 30 | end 31 | 32 | test '#before_render is called before #after_render' do 33 | def @renderer.before_render(name, props, opts) 34 | "throw 'before_render ' + afterRenderVar" 35 | end 36 | 37 | def @renderer.after_render(name, props, opts) 38 | "var afterRenderVar = 'assigned_after_render'" 39 | end 40 | 41 | error = assert_raises(React::ServerRendering::PrerenderError) do 42 | @renderer.render("Todo", {todo: "write tests"}.to_json, {}) 43 | end 44 | 45 | assert_match(/before_render/, error.message) 46 | assert_no_match(/assigned_after_render/, error.message) 47 | end 48 | 49 | 50 | test '#after_render is called after #before_render' do 51 | def @renderer.before_render(name, props, opts) 52 | "var beforeRenderVar = 'assigned_before_render'" 53 | end 54 | 55 | def @renderer.after_render(name, props, opts) 56 | "throw 'after_render ' + beforeRenderVar" 57 | end 58 | 59 | error = assert_raises(React::ServerRendering::PrerenderError) do 60 | @renderer.render("Todo", {todo: "write tests"}.to_json, {}) 61 | end 62 | 63 | assert_match(/after_render/, error.message) 64 | assert_match(/assigned_before_render/, error.message) 65 | end 66 | 67 | test '.new accepts code:' do 68 | dummy_renderer = React::ServerRendering::ExecJSRenderer.new(code: DUMMY_IMPLEMENTATION) 69 | result = dummy_renderer.render("Todo", {todo: "get a real job"}.to_json, {}) 70 | assert_equal("renderToString was called", result) 71 | end 72 | end -------------------------------------------------------------------------------- /test/react/server_rendering/sprockets_renderer_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | class SprocketsRendererTest < ActiveSupport::TestCase 4 | setup do 5 | @renderer = React::ServerRendering::SprocketsRenderer.new({}) 6 | end 7 | 8 | test '#render returns HTML' do 9 | result = @renderer.render("Todo", {todo: "write tests"}, nil) 10 | assert_match(//, result) 11 | assert_match(/data-react-checksum/, result) 12 | end 13 | 14 | test '#render accepts strings' do 15 | result = @renderer.render("Todo", {todo: "write more tests"}.to_json, nil) 16 | assert_match(//, result) 17 | end 18 | 19 | test '#render accepts :static pre-render option' do 20 | result = @renderer.render("Todo", {todo: "write more tests"}, :static) 21 | assert_match(/
  • write more tests<\/li>/, result) 22 | assert_no_match(/data-react-checksum/, result) 23 | end 24 | 25 | test '#render replays console messages' do 26 | result = @renderer.render("TodoListWithConsoleLog", {todos: ["log some messages"]}, nil) 27 | assert_match(/console.log.apply\(console, \["got initial state"\]\)/, result) 28 | assert_match(/console.warn.apply\(console, \["mounted component"\]\)/, result) 29 | assert_match(/console.error.apply\(console, \["rendered!","foo"\]\)/, result) 30 | end 31 | 32 | test '#render console messages can be disabled' do 33 | no_log_renderer = React::ServerRendering::SprocketsRenderer.new({replay_console: false}) 34 | result = no_log_renderer.render("TodoListWithConsoleLog", {todos: ["log some messages"]}, nil) 35 | assert_no_match(/console.log.apply\(console, \["got initial state"\]\)/, result) 36 | assert_no_match(/console.warn.apply\(console, \["mounted component"\]\)/, result) 37 | assert_no_match(/console.error.apply\(console, \["rendered!","foo"\]\)/, result) 38 | end 39 | 40 | test '#render errors include stack traces' do 41 | err = assert_raises React::ServerRendering::PrerenderError do 42 | @renderer.render("NonExistentComponent", {}, nil) 43 | end 44 | assert_match(/ReferenceError/, err.to_s) 45 | assert_match(/NonExistentComponent/, err.to_s, "it names the component") 46 | assert_match(/\n/, err.to_s, "it includes the multi-line backtrace") 47 | end 48 | 49 | test '.new accepts any filenames' do 50 | limited_renderer = React::ServerRendering::SprocketsRenderer.new(files: ["react.js", "components/Todo.js"]) 51 | assert_match(/get a real job<\/li>/, limited_renderer.render("Todo", {todo: "get a real job"}, nil)) 52 | err = assert_raises React::ServerRendering::PrerenderError do 53 | limited_renderer.render("TodoList", {todos: []}, nil) 54 | end 55 | assert_match(/ReferenceError/, err.to_s, "it doesnt load other files") 56 | end 57 | end -------------------------------------------------------------------------------- /test/server_rendered_html_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | require 'fileutils' 3 | 4 | class ServerRenderedHtmlTest < ActionDispatch::IntegrationTest 5 | # Rails' asset pipeline has trouble picking up changes to files if they happen too fast. 6 | # By sleeping for a little bit at certain points, we can make sure that rails notices the 7 | # change in the file mtime, and calls our renderer setup functions appropriately 8 | def wait_to_ensure_asset_pipeline_detects_changes 9 | sleep(1) 10 | end 11 | 12 | test 'react server rendering reloads jsx after changes to the jsx files' do 13 | file_with_updates = File.expand_path('../helper_files/TodoListWithUpdates.js.jsx', __FILE__) 14 | file_without_updates = File.expand_path('../helper_files/TodoListWithoutUpdates.js.jsx', __FILE__) 15 | app_file = File.expand_path('../dummy/app/assets/javascripts/components/TodoList.js.jsx', __FILE__) 16 | 17 | FileUtils.cp app_file, file_without_updates 18 | FileUtils.touch app_file 19 | wait_to_ensure_asset_pipeline_detects_changes 20 | 21 | begin 22 | get '/server/1' 23 | refute_match(/Updated/, response.body) 24 | 25 | FileUtils.cp file_with_updates, app_file 26 | FileUtils.touch app_file 27 | wait_to_ensure_asset_pipeline_detects_changes 28 | 29 | get '/server/1' 30 | assert_match(/Updated/, response.body) 31 | ensure 32 | # if we have a test failure, we want to make sure that we revert the dummy file 33 | FileUtils.mv file_without_updates, app_file 34 | FileUtils.touch app_file 35 | wait_to_ensure_asset_pipeline_detects_changes 36 | end 37 | end 38 | 39 | test 'react server rendering shows console output as html comment' do 40 | # Make sure console messages are replayed when requested 41 | get '/server/console_example' 42 | assert_match(/Console Logged/, response.body) 43 | assert_match(/console.log.apply\(console, \["got initial state"\]\)/, response.body) 44 | assert_match(/console.warn.apply\(console, \["mounted component"\]\)/, response.body) 45 | assert_match(/console.error.apply\(console, \["rendered!","foo"\]\)/, response.body) 46 | 47 | # Make sure they're not when we don't ask for them 48 | get '/server/console_example_suppressed' 49 | assert_match(/Console Logged/, response.body) 50 | assert_no_match(/console.log/, response.body) 51 | assert_no_match(/console.warn/, response.body) 52 | assert_no_match(/console.error/, response.body) 53 | end 54 | 55 | test 'react inline component rendering' do 56 | get '/server/inline_component' 57 | assert_match(//, response.body) 60 | # make sure that the layout is rendered with the component 61 | assert_match(/Dummy<\/title>/, response.body) 62 | end 63 | end 64 | -------------------------------------------------------------------------------- /test/dummy/config/environments/production.rb: -------------------------------------------------------------------------------- 1 | Dummy::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 thread 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 | # Enable Rack::Cache to put a simple HTTP cache in front of your application 18 | # Add `rack-cache` to your Gemfile before enabling this. 19 | # For large-scale production use, consider using a caching reverse proxy like nginx, varnish or squid. 20 | # config.action_dispatch.rack_cache = true 21 | 22 | # Disable Rails's static asset server (Apache or nginx will already do this). 23 | config.serve_static_assets = false 24 | 25 | # Compress JavaScripts and CSS. 26 | config.assets.js_compressor = :uglifier 27 | # config.assets.css_compressor = :sass 28 | 29 | # Do not fallback to assets pipeline if a precompiled asset is missed. 30 | config.assets.compile = false 31 | 32 | # Generate digests for assets URLs. 33 | config.assets.digest = true 34 | 35 | # Version of your assets, change this if you want to expire all your assets. 36 | config.assets.version = '1.0' 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 | # Force all access to the app over SSL, use Strict-Transport-Security, and use secure cookies. 43 | # config.force_ssl = true 44 | 45 | # Set to :debug to see everything in the log. 46 | config.log_level = :info 47 | 48 | # Prepend all log lines with the following tags. 49 | # config.log_tags = [ :subdomain, :uuid ] 50 | 51 | # Use a different logger for distributed setups. 52 | # config.logger = ActiveSupport::TaggedLogging.new(SyslogLogger.new) 53 | 54 | # Use a different cache store in production. 55 | # config.cache_store = :mem_cache_store 56 | 57 | # Enable serving of images, stylesheets, and JavaScripts from an asset server. 58 | # config.action_controller.asset_host = "http://assets.example.com" 59 | 60 | # Precompile additional assets. 61 | # application.js, application.css, and all non-JS/CSS in app/assets folder are already added. 62 | # config.assets.precompile += %w( search.js ) 63 | 64 | # Ignore bad email addresses and do not raise email delivery errors. 65 | # Set this to true and configure the email server for immediate delivery to raise delivery errors. 66 | # config.action_mailer.raise_delivery_errors = false 67 | 68 | # Enable locale fallbacks for I18n (makes lookups for any locale fall back to 69 | # the I18n.default_locale when a translation can not be found). 70 | config.i18n.fallbacks = true 71 | 72 | # Send deprecation notices to registered listeners. 73 | config.active_support.deprecation = :notify 74 | 75 | # Disable automatic flushing of the log to improve performance. 76 | # config.autoflush_log = false 77 | 78 | # Use default logging formatter so that PID and timestamp are not suppressed. 79 | config.log_formatter = ::Logger::Formatter.new 80 | end 81 | -------------------------------------------------------------------------------- /lib/react/rails/railtie.rb: -------------------------------------------------------------------------------- 1 | require 'rails' 2 | 3 | module React 4 | module Rails 5 | class Railtie < ::Rails::Railtie 6 | config.react = ActiveSupport::OrderedOptions.new 7 | 8 | # Sensible defaults. Can be overridden in application.rb 9 | config.react.variant = (::Rails.env.production? ? :production : :development) 10 | config.react.addons = false 11 | config.react.jsx_transform_options = {} 12 | config.react.jsx_transformer_class = nil # defaults to BabelTransformer 13 | # Server rendering: 14 | config.react.server_renderer_pool_size = 1 # increase if you're on JRuby 15 | config.react.server_renderer_timeout = 20 # seconds 16 | config.react.server_renderer = nil # defaults to SprocketsRenderer 17 | config.react.server_renderer_options = {} # SprocketsRenderer provides defaults 18 | 19 | # Watch .jsx files for changes in dev, so we can reload the JS VMs with the new JS code. 20 | initializer "react_rails.add_watchable_files", group: :all do |app| 21 | app.config.watchable_files.concat Dir["#{app.root}/app/assets/javascripts/**/*.jsx*"] 22 | end 23 | 24 | # Include the react-rails view helper lazily 25 | initializer "react_rails.setup_view_helpers", group: :all do |app| 26 | app.config.react.jsx_transformer_class ||= React::JSX::DEFAULT_TRANSFORMER 27 | React::JSX.transformer_class = app.config.react.jsx_transformer_class 28 | React::JSX.transform_options = app.config.react.jsx_transform_options 29 | 30 | ActiveSupport.on_load(:action_view) do 31 | include ::React::Rails::ViewHelper 32 | end 33 | end 34 | 35 | initializer "react_rails.add_component_renderer", group: :all do |app| 36 | ActionController::Renderers.add :component do |component_name, options| 37 | html = ::React::Rails::ControllerRenderer.call(component_name, options) 38 | render_options = options.merge(inline: html) 39 | render(render_options) 40 | end 41 | end 42 | 43 | initializer "react_rails.bust_cache", group: :all do |app| 44 | asset_variant = React::Rails::AssetVariant.new({ 45 | variant: app.config.react.variant, 46 | addons: app.config.react.addons, 47 | }) 48 | 49 | sprockets_env = app.assets || app.config.assets # sprockets-rails 3.x attaches this at a different config 50 | sprockets_env.version = [sprockets_env.version, "react-#{asset_variant.react_build}",].compact.join('-') 51 | 52 | end 53 | 54 | config.before_initialize do |app| 55 | asset_variant = React::Rails::AssetVariant.new({ 56 | variant: app.config.react.variant, 57 | addons: app.config.react.addons, 58 | }) 59 | 60 | app.config.assets.paths << asset_variant.react_directory 61 | app.config.assets.paths << asset_variant.jsx_directory 62 | end 63 | 64 | config.after_initialize do |app| 65 | # The class isn't accessible in the configure block, so assign it here if it wasn't overridden: 66 | app.config.react.server_renderer ||= React::ServerRendering::SprocketsRenderer 67 | 68 | React::ServerRendering.pool_size = app.config.react.server_renderer_pool_size 69 | React::ServerRendering.pool_timeout = app.config.react.server_renderer_timeout 70 | React::ServerRendering.renderer_options = app.config.react.server_renderer_options 71 | React::ServerRendering.renderer = app.config.react.server_renderer 72 | 73 | React::ServerRendering.reset_pool 74 | # Reload renderers in dev when files change 75 | ActionDispatch::Reloader.to_prepare { React::ServerRendering.reset_pool } 76 | end 77 | end 78 | end 79 | end 80 | -------------------------------------------------------------------------------- /lib/assets/javascripts/react_ujs.js.erb: -------------------------------------------------------------------------------- 1 | /*globals React, Turbolinks*/ 2 | 3 | // Unobtrusive scripting adapter for React 4 | ;(function(document, window) { 5 | // jQuery is optional. Use it to support legacy browsers. 6 | var $ = (typeof window.jQuery !== 'undefined') && window.jQuery; 7 | 8 | // create the namespace 9 | window.ReactRailsUJS = { 10 | CLASS_NAME_ATTR: 'data-react-class', 11 | PROPS_ATTR: 'data-react-props', 12 | RAILS_ENV_DEVELOPMENT: <%= Rails.env == "development" %>, 13 | // helper method for the mount and unmount methods to find the 14 | // `data-react-class` DOM elements 15 | findDOMNodes: function(searchSelector) { 16 | // we will use fully qualified paths as we do not bind the callbacks 17 | var selector; 18 | if (typeof searchSelector === 'undefined') { 19 | var selector = '[' + window.ReactRailsUJS.CLASS_NAME_ATTR + ']'; 20 | } else { 21 | var selector = searchSelector + ' [' + window.ReactRailsUJS.CLASS_NAME_ATTR + ']'; 22 | } 23 | 24 | if ($) { 25 | return $(selector); 26 | } else { 27 | return document.querySelectorAll(selector); 28 | } 29 | }, 30 | 31 | mountComponents: function(searchSelector) { 32 | var nodes = window.ReactRailsUJS.findDOMNodes(searchSelector); 33 | 34 | for (var i = 0; i < nodes.length; ++i) { 35 | var node = nodes[i]; 36 | var className = node.getAttribute(window.ReactRailsUJS.CLASS_NAME_ATTR); 37 | 38 | // Assume className is simple and can be found at top-level (window). 39 | // Fallback to eval to handle cases like 'My.React.ComponentName'. 40 | var constructor = window[className] || eval.call(window, className); 41 | var propsJson = node.getAttribute(window.ReactRailsUJS.PROPS_ATTR); 42 | var props = propsJson && JSON.parse(propsJson); 43 | 44 | React.render(React.createElement(constructor, props), node); 45 | } 46 | }, 47 | 48 | unmountComponents: function(searchSelector) { 49 | var nodes = window.ReactRailsUJS.findDOMNodes(searchSelector); 50 | 51 | for (var i = 0; i < nodes.length; ++i) { 52 | var node = nodes[i]; 53 | 54 | React.unmountComponentAtNode(node); 55 | } 56 | } 57 | }; 58 | 59 | // functions not exposed publicly 60 | function handleTurbolinksEvents () { 61 | var handleEvent; 62 | var unmountEvent; 63 | 64 | if ($) { 65 | handleEvent = function(eventName, callback) { 66 | $(document).on(eventName, callback); 67 | }; 68 | 69 | } else { 70 | handleEvent = function(eventName, callback) { 71 | document.addEventListener(eventName, callback); 72 | }; 73 | } 74 | 75 | if (Turbolinks.EVENTS) { 76 | unmountEvent = Turbolinks.EVENTS.BEFORE_UNLOAD; 77 | } else { 78 | unmountEvent = 'page:receive'; 79 | Turbolinks.pagesCached(0); 80 | 81 | if (window.ReactRailsUJS.RAILS_ENV_DEVELOPMENT) { 82 | console.warn('The Turbolinks cache has been disabled (Turbolinks >= 2.4.0 is recommended). See https://github.com/reactjs/react-rails/issues/87 for more information.'); 83 | } 84 | } 85 | handleEvent('page:change', function() {window.ReactRailsUJS.mountComponents()}); 86 | handleEvent(unmountEvent, function() {window.ReactRailsUJS.unmountComponents()}); 87 | } 88 | 89 | function handleNativeEvents() { 90 | if ($) { 91 | $(function() {window.ReactRailsUJS.mountComponents()}); 92 | } else { 93 | document.addEventListener('DOMContentLoaded', function() {window.ReactRailsUJS.mountComponents()}); 94 | } 95 | } 96 | 97 | if (typeof Turbolinks !== 'undefined' && Turbolinks.supported) { 98 | handleTurbolinksEvents(); 99 | } else { 100 | handleNativeEvents(); 101 | } 102 | })(document, window); 103 | -------------------------------------------------------------------------------- /lib/generators/react/component_generator.rb: -------------------------------------------------------------------------------- 1 | module React 2 | module Generators 3 | class ComponentGenerator < ::Rails::Generators::NamedBase 4 | source_root File.expand_path '../../templates', __FILE__ 5 | desc <<-DESC.strip_heredoc 6 | Description: 7 | Scaffold a react component into app/assets/javascripts/components. 8 | The generated component will include a basic render function and a PropTypes 9 | hash to help with development. 10 | 11 | Available field types: 12 | 13 | Basic prop types do not take any additional arguments. If you do not specify 14 | a prop type, the generic node will be used. The basic types available are: 15 | 16 | any 17 | array 18 | bool 19 | element 20 | func 21 | number 22 | object 23 | node 24 | shape 25 | string 26 | 27 | Special PropTypes take additional arguments in {}, and must be enclosed in 28 | single quotes to keep bash from expanding the arguments in {}. 29 | 30 | instanceOf 31 | takes an optional class name in the form of {className} 32 | 33 | oneOf 34 | behaves like an enum, and takes an optional list of strings that will 35 | be allowed in the form of 'name:oneOf{one,two,three}'. 36 | 37 | oneOfType. 38 | oneOfType takes an optional list of react and custom types in the form of 39 | 'model:oneOfType{string,number,OtherType}' 40 | 41 | Examples: 42 | rails g react:component person name 43 | rails g react:component restaurant name:string rating:number owner:instanceOf{Person} 44 | rails g react:component food 'kind:oneOf{meat,cheese,vegetable}' 45 | rails g react:component events 'location:oneOfType{string,Restaurant}' 46 | DESC 47 | 48 | argument :attributes, 49 | :type => :array, 50 | :default => [], 51 | :banner => "field[:type] field[:type] ..." 52 | 53 | class_option :es6, 54 | type: :boolean, 55 | default: false, 56 | desc: 'Output es6 class based component' 57 | 58 | REACT_PROP_TYPES = { 59 | "node" => 'React.PropTypes.node', 60 | "bool" => 'React.PropTypes.bool', 61 | "boolean" => 'React.PropTypes.bool', 62 | "string" => 'React.PropTypes.string', 63 | "number" => 'React.PropTypes.number', 64 | "object" => 'React.PropTypes.object', 65 | "array" => 'React.PropTypes.array', 66 | "shape" => 'React.PropTypes.shape({})', 67 | "element" => 'React.PropTypes.element', 68 | "func" => 'React.PropTypes.func', 69 | "function" => 'React.PropTypes.func', 70 | "any" => 'React.PropTypes.any', 71 | 72 | "instanceOf" => ->(type) { 73 | 'React.PropTypes.instanceOf(%s)' % type.to_s.camelize 74 | }, 75 | 76 | "oneOf" => ->(*options) { 77 | enums = options.map{|k| "'#{k.to_s}'"}.join(',') 78 | 'React.PropTypes.oneOf([%s])' % enums 79 | }, 80 | 81 | "oneOfType" => ->(*options) { 82 | types = options.map{|k| "#{lookup(k.to_s, k.to_s)}" }.join(',') 83 | 'React.PropTypes.oneOfType([%s])' % types 84 | }, 85 | } 86 | 87 | def create_component_file 88 | extension = options[:es6] ? "es6.jsx" : "js.jsx" 89 | file_path = File.join('app/assets/javascripts/components', "#{file_name}.#{extension}") 90 | template("component.#{extension}", file_path) 91 | end 92 | 93 | private 94 | 95 | def parse_attributes! 96 | self.attributes = (attributes || []).map do |attr| 97 | name, type, options = "", "", "" 98 | options_regex = /(?<options>{.*})/ 99 | 100 | name, type = attr.split(':') 101 | 102 | if matchdata = options_regex.match(type) 103 | options = matchdata[:options] 104 | type = type.gsub(options_regex, '') 105 | end 106 | 107 | { :name => name, :type => lookup(type, options) } 108 | end 109 | end 110 | 111 | def self.lookup(type = "node", options = "") 112 | react_prop_type = REACT_PROP_TYPES[type] 113 | if react_prop_type.blank? 114 | if type =~ /^[[:upper:]]/ 115 | react_prop_type = REACT_PROP_TYPES['instanceOf'] 116 | else 117 | react_prop_type = REACT_PROP_TYPES['node'] 118 | end 119 | end 120 | 121 | options = options.to_s.gsub(/[{}]/, '').split(',') 122 | 123 | react_prop_type = react_prop_type.call(*options) if react_prop_type.respond_to? :call 124 | react_prop_type 125 | end 126 | 127 | def lookup(type = "node", options = "") 128 | self.class.lookup(type, options) 129 | end 130 | end 131 | end 132 | end 133 | -------------------------------------------------------------------------------- /test/react/rails/view_helper_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | require 'capybara/rails' 4 | require 'capybara/poltergeist' 5 | 6 | Capybara.javascript_driver = :poltergeist 7 | Capybara.app = Rails.application 8 | 9 | # Useful for debugging. 10 | # Just put page.driver.debug in your test and it will 11 | # pause and throw up a browser 12 | Capybara.register_driver :poltergeist_debug do |app| 13 | Capybara::Poltergeist::Driver.new(app, :inspector => true) 14 | end 15 | Capybara.javascript_driver = :poltergeist_debug 16 | 17 | class ViewHelperTest < ActionDispatch::IntegrationTest 18 | include Capybara::DSL 19 | 20 | setup do 21 | @helper = ActionView::Base.new.extend(React::Rails::ViewHelper) 22 | Capybara.current_driver = Capybara.javascript_driver 23 | end 24 | 25 | test 'react_component accepts React props' do 26 | html = @helper.react_component('Foo', {bar: 'value'}) 27 | %w(data-react-class="Foo" data-react-props="{"bar":"value"}").each do |segment| 28 | assert html.include?(segment) 29 | end 30 | end 31 | 32 | test 'react_component accepts jbuilder-based strings as properties' do 33 | jbuilder_json = Jbuilder.new do |json| 34 | json.bar 'value' 35 | end.target! 36 | 37 | html = @helper.react_component('Foo', jbuilder_json) 38 | %w(data-react-class="Foo" data-react-props="{"bar":"value"}").each do |segment| 39 | assert html.include?(segment), "expected #{html} to include #{segment}" 40 | end 41 | end 42 | 43 | test 'react_component accepts string props with prerender: true' do 44 | html = @helper.react_component('Todo', {todo: 'render on the server'}.to_json, prerender: true) 45 | assert(html.include?('data-react-class="Todo"'), "it includes attrs for UJS") 46 | assert(html.include?('>render on the server</li>'), "it includes rendered HTML") 47 | assert(html.include?('data-reactid'), "it includes React properties") 48 | end 49 | 50 | test 'react_component passes :static to SprocketsRenderer' do 51 | html = @helper.react_component('Todo', {todo: 'render on the server'}.to_json, prerender: :static) 52 | assert(html.include?('>render on the server</li>'), "it includes rendered HTML") 53 | assert(!html.include?('data-reactid'), "it DOESNT INCLUDE React properties") 54 | end 55 | 56 | test 'react_component accepts HTML options and HTML tag' do 57 | assert @helper.react_component('Foo', {}, :span).match(/<span\s.*><\/span>/) 58 | 59 | html = @helper.react_component('Foo', {}, {:class => 'test', :tag => :span, :data => {:foo => 1}}) 60 | assert html.match(/<span\s.*><\/span>/) 61 | assert html.include?('class="test"') 62 | assert html.include?('data-foo="1"') 63 | end 64 | 65 | test 'ujs object present on the global React object and has our methods' do 66 | visit '/pages/1' 67 | assert page.has_content?('Hello Bob') 68 | 69 | # the exposed ujs object is present 70 | ujs_present = page.evaluate_script('typeof ReactRailsUJS === "object";') 71 | assert_equal(ujs_present, true) 72 | 73 | # it contains the constants 74 | class_name_present = page.evaluate_script('ReactRailsUJS.CLASS_NAME_ATTR === "data-react-class";') 75 | assert_equal(class_name_present, true) 76 | props_present = page.evaluate_script('ReactRailsUJS.PROPS_ATTR === "data-react-props";') 77 | assert_equal(props_present, true) 78 | 79 | #it contains the methods 80 | find_dom_nodes_present = page.evaluate_script('typeof ReactRailsUJS.findDOMNodes === "function";') 81 | assert_equal(find_dom_nodes_present, true) 82 | mount_components_present = page.evaluate_script('typeof ReactRailsUJS.mountComponents === "function";') 83 | assert_equal(mount_components_present, true) 84 | unmount_components_present = page.evaluate_script('typeof ReactRailsUJS.unmountComponents === "function";') 85 | assert_equal(unmount_components_present, true) 86 | end 87 | 88 | test 'react_ujs works with rendered HTML' do 89 | visit '/pages/1' 90 | assert page.has_content?('Hello Bob') 91 | 92 | page.click_button 'Goodbye' 93 | assert page.has_no_content?('Hello Bob') 94 | assert page.has_content?('Goodbye Bob') 95 | end 96 | 97 | test 'react_ujs works with Turbolinks' do 98 | visit '/pages/1' 99 | assert page.has_content?('Hello Bob') 100 | 101 | # Try clicking links. 102 | page.click_link('Alice') 103 | assert page.has_content?('Hello Alice') 104 | 105 | page.click_link('Bob') 106 | assert page.has_content?('Hello Bob') 107 | 108 | # Try going back. 109 | page.execute_script('history.back();') 110 | assert page.has_content?('Hello Alice') 111 | 112 | wait_for_turbolinks_to_be_available() 113 | 114 | # Try Turbolinks javascript API. 115 | page.execute_script('Turbolinks.visit("/pages/2");') 116 | assert page.has_content?('Hello Alice') 117 | 118 | wait_for_turbolinks_to_be_available() 119 | 120 | page.execute_script('Turbolinks.visit("/pages/1");') 121 | assert page.has_content?('Hello Bob') 122 | 123 | # Component state is not persistent after clicking current page link. 124 | page.click_button 'Goodbye' 125 | assert page.has_content?('Goodbye Bob') 126 | 127 | page.click_link('Bob') 128 | assert page.has_content?('Hello Bob') 129 | end 130 | 131 | test 'react_ujs can unount at node' do 132 | visit '/pages/1' 133 | assert page.has_content?('Hello Bob') 134 | 135 | page.click_link 'Unmount at #test-component' 136 | assert page.has_no_content?('Hello Bob') 137 | 138 | page.click_link 'Mount at #test-component' 139 | assert page.has_content?('Hello Bob') 140 | end 141 | 142 | test 'react server rendering also gets mounted on client' do 143 | visit '/server/1' 144 | assert_match(/data-react-class=\"TodoList\"/, page.html) 145 | assert_match(/yep/, page.find("#status").text) 146 | end 147 | 148 | test 'react server rendering does not include internal properties' do 149 | visit '/server/1' 150 | assert_no_match(/tag=/, page.html) 151 | assert_no_match(/prerender=/, page.html) 152 | end 153 | end 154 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Gem](https://img.shields.io/gem/v/react-rails.svg?style=flat-square)](http://rubygems.org/gems/react-rails) 2 | [![Build Status](https://img.shields.io/travis/reactjs/react-rails/master.svg?style=flat-square)](https://travis-ci.org/reactjs/react-rails) 3 | [![Gemnasium](https://img.shields.io/gemnasium/reactjs/react-rails.svg?style=flat-square)](https://gemnasium.com/reactjs/react-rails) 4 | [![Code Climate](https://img.shields.io/codeclimate/github/reactjs/react-rails.svg?style=flat-square)](https://codeclimate.com/github/reactjs/react-rails) 5 | [![Test Coverage](https://img.shields.io/codeclimate/coverage/github/reactjs/react-rails.svg?style=flat-square)](https://codeclimate.com/github/reactjs/react-rails/coverage) 6 | 7 | * * * 8 | 9 | # react-rails 10 | 11 | 12 | `react-rails` makes it easy to use [React](http://facebook.github.io/react/) and [JSX](http://facebook.github.io/react/docs/jsx-in-depth.html) 13 | in your Ruby on Rails (3.2+) application. `react-rails` can: 14 | 15 | - Provide [various `react` builds](#reactjs-builds) to your asset bundle 16 | - Transform [`.jsx` in the asset pipeline](#jsx) 17 | - [Render components into views and mount them](#rendering--mounting) via view helper & `react_ujs` 18 | - [Render components server-side](#server-rendering) with `prerender: true` 19 | - [Generate components](#component-generator) with a Rails generator 20 | 21 | ## Installation 22 | 23 | Add `react-rails` to your gemfile: 24 | 25 | ```ruby 26 | gem 'react-rails', '~> 1.0' 27 | ``` 28 | 29 | Next, run the installation script: 30 | 31 | ```bash 32 | rails g react:install 33 | ``` 34 | 35 | This will: 36 | - create a `components.js` manifest file and a `app/assets/javascripts/components/` directory, 37 | where you will put your components 38 | - place the following in your `application.js`: 39 | 40 | ```js 41 | //= require react 42 | //= require react_ujs 43 | //= require components 44 | ``` 45 | 46 | ## Usage 47 | 48 | ### React.js builds 49 | 50 | You can pick which React.js build (development, production, with or without [add-ons]((http://facebook.github.io/react/docs/addons.html))) 51 | to serve in each environment by adding a config. Here are the defaults: 52 | 53 | ```ruby 54 | # config/environments/development.rb 55 | MyApp::Application.configure do 56 | config.react.variant = :development 57 | end 58 | 59 | # config/environments/production.rb 60 | MyApp::Application.configure do 61 | config.react.variant = :production 62 | end 63 | ``` 64 | 65 | To include add-ons, use this config: 66 | 67 | ```ruby 68 | MyApp::Application.configure do 69 | config.react.addons = true # defaults to false 70 | end 71 | ``` 72 | 73 | After restarting your Rails server, `//= require react` will provide the build of React.js which 74 | was specified by the configurations. 75 | 76 | `react-rails` offers a few other options for versions & builds of React.js. 77 | See [VERSIONS.md](https://github.com/reactjs/react-rails/blob/master/VERSIONS.md) for more info about 78 | using the `react-source` gem or dropping in your own copies of React.js. 79 | 80 | ### JSX 81 | 82 | After installing `react-rails`, restart your server. Now, `.js.jsx` files will be transformed in the asset pipeline. 83 | 84 | `react-rails` currently ships with two transformers, to convert jsx code - 85 | 86 | * `BabelTransformer` using [Babel](http://babeljs.io), which is the default transformer. 87 | * `JSXTransformer` using `JSXTransformer.js` 88 | 89 | You can use the deprecated `JSXTransformer` by setting it in an application config: 90 | 91 | ```ruby 92 | config.react.jsx_transformer_class = React::JSX::JSXTransformer 93 | ``` 94 | 95 | #### BabelTransformer options 96 | 97 | You can use babel's [transformers](http://babeljs.io/docs/advanced/transformers/) and [custom plugins](http://babeljs.io/docs/advanced/plugins/), 98 | and pass [options](http://babeljs.io/docs/usage/options/) to the babel transpiler adding following configurations: 99 | 100 | ```ruby 101 | config.react.jsx_transform_options = { 102 | blacklist: ['spec.functionName', 'validation.react'], # default options 103 | optional: ["transformerName"], # pass extra babel options 104 | whitelist: ["useStrict"] # even more options 105 | } 106 | ``` 107 | Under the hood, `react-rails` uses [ruby-babel-transpiler](https://github.com/babel/ruby-babel-transpiler), for transformation. 108 | 109 | #### JSXTransformer options 110 | 111 | You can use JSX `--harmony` or `--strip-types` options by adding a configuration: 112 | 113 | ```ruby 114 | config.react.jsx_transform_options = { 115 | harmony: true, 116 | strip_types: true, # for removing Flow type annotations 117 | asset_path: "path/to/JSXTransformer.js", # if your JSXTransformer is somewhere else 118 | } 119 | ``` 120 | 121 | ### Rendering & mounting 122 | 123 | `react-rails` includes a view helper (`react_component`) and an unobtrusive JavaScript driver (`react_ujs`) 124 | which work together to put React components on the page. You should require the UJS driver 125 | in your manifest after `react` (and after `turbolinks` if you use [Turbolinks](https://github.com/rails/turbolinks)). 126 | 127 | The __view helper__ puts a `div` on the page with the requested component class & props. For example: 128 | 129 | ```erb 130 | <%= react_component('HelloMessage', name: 'John') %> 131 | <!-- becomes: --> 132 | <div data-react-class="HelloMessage" data-react-props="{"name":"John"}"></div> 133 | ``` 134 | 135 | On page load, the __`react_ujs` driver__ will scan the page and mount components using `data-react-class` 136 | and `data-react-props`. 137 | 138 | If Turbolinks is present components are mounted on the `page:change` event, and unmounted on `page:before-unload`. 139 | __Turbolinks >= 2.4.0__ is recommended because it exposes better events. 140 | 141 | The view helper's signature is: 142 | 143 | ```ruby 144 | react_component(component_class_name, props={}, html_options={}) 145 | ``` 146 | 147 | - `component_class_name` is a string which names a globally-accessible component class. It may have dots (eg, `"MyApp.Header.MenuItem"`). 148 | - `props` is either an object that responds to `#to_json` or an already-stringified JSON object (eg, made with Jbuilder, see note below). 149 | - `html_options` may include: 150 | - `tag:` to use an element other than a `div` to embed `data-react-class` and `-props`. 151 | - `prerender: true` to render the component on the server. 152 | - `**other` Any other arguments (eg `class:`, `id:`) are passed through to [`content_tag`](http://api.rubyonrails.org/classes/ActionView/Helpers/TagHelper.html#method-i-content_tag). 153 | 154 | 155 | ### Server rendering 156 | 157 | To render components on the server, pass `prerender: true` to `react_component`: 158 | 159 | ```erb 160 | <%= react_component('HelloMessage', {name: 'John'}, {prerender: true}) %> 161 | <!-- becomes: --> 162 | <div data-react-class="HelloMessage" data-react-props="{"name":"John"}"> 163 | <h1>Hello, John!</h1> 164 | </div> 165 | ``` 166 | 167 | _(It will be also be mounted by the UJS on page load.)_ 168 | 169 | There are some requirements for this to work: 170 | 171 | - `react-rails` must load your code. By convention it uses `components.js`, which was created 172 | by the install task. This file must include your components _and_ their dependencies (eg, Underscore.js). 173 | - Your components must be accessible in the global scope. 174 | If you are using `.js.jsx.coffee` files then the wrapper function needs to be taken into account: 175 | 176 | ```coffee 177 | # @ is `window`: 178 | @Component = React.createClass 179 | render: -> 180 | `<ExampleComponent videos={this.props.videos} />` 181 | ``` 182 | - Your code can't reference `document`. Prerender processes don't have access to `document`, 183 | so jQuery and some other libs won't work in this environment :( 184 | 185 | You can configure your pool of JS virtual machines and specify where it should load code: 186 | 187 | ```ruby 188 | # config/environments/application.rb 189 | # These are the defaults if you dont specify any yourself 190 | MyApp::Application.configure do 191 | # Settings for the pool of renderers: 192 | config.react.server_renderer_pool_size ||= 1 # ExecJS doesn't allow more than one on MRI 193 | config.react.server_renderer_timeout ||= 20 # seconds 194 | config.react.server_renderer = React::ServerRendering::SprocketsRenderer 195 | config.react.server_renderer_options = { 196 | files: ["react.js", "components.js"], # files to load for prerendering 197 | replay_console: true, # if true, console.* will be replayed client-side 198 | } 199 | end 200 | ``` 201 | 202 | - On MRI, use `therubyracer` for the best performance (see [discussion](https://github.com/reactjs/react-rails/pull/290)) 203 | - On MRI, you'll get a deadlock with `pool_size` > 1 204 | - If you're using JRuby, you can increase `pool_size` to have real multi-threaded rendering. 205 | 206 | ### Component generator 207 | 208 | `react-rails` ships with a Rails generator to help you get started with a simple component scaffold. 209 | You can run it using `rails generate react:component ComponentName (--es6)`. 210 | The generator takes an optional list of arguments for default propTypes, 211 | which follow the conventions set in the [Reusable Components](http://facebook.github.io/react/docs/reusable-components.html) 212 | section of the React documentation. 213 | 214 | For example: 215 | 216 | ```shell 217 | rails generate react:component Post title:string body:string published:bool published_by:instanceOf{Person} 218 | ``` 219 | 220 | would generate the following in `app/assets/javascripts/components/post.js.jsx`: 221 | 222 | ```jsx 223 | var Post = React.createClass({ 224 | propTypes: { 225 | title: React.PropTypes.string, 226 | body: React.PropTypes.string, 227 | published: React.PropTypes.bool, 228 | publishedBy: React.PropTypes.instanceOf(Person) 229 | }, 230 | 231 | render: function() { 232 | return ( 233 | <div> 234 | <div>Title: {this.props.title}</div> 235 | <div>Body: {this.props.body}</div> 236 | <div>Published: {this.props.published}</div> 237 | <div>Published By: {this.props.publishedBy}</div> 238 | </div> 239 | ); 240 | } 241 | }); 242 | ``` 243 | 244 | #### Options 245 | 246 | **--es6** : Generate the same component but using cutting edge es6 class 247 | 248 | For example: 249 | 250 | ```shell 251 | rails generate react:component Label label:string --es6 252 | ``` 253 | 254 | #### Arguments 255 | 256 | The generator can use the following arguments to create basic propTypes: 257 | 258 | * any 259 | * array 260 | * bool 261 | * element 262 | * func 263 | * number 264 | * object 265 | * node 266 | * shape 267 | * string 268 | 269 | The following additional arguments have special behavior: 270 | 271 | * `instanceOf` takes an optional class name in the form of {className}. 272 | * `oneOf` behaves like an enum, and takes an optional list of strings in the form of `'name:oneOf{one,two,three}'`. 273 | * `oneOfType` takes an optional list of react and custom types in the form of `'model:oneOfType{string,number,OtherType}'`. 274 | 275 | Note that the arguments for `oneOf` and `oneOfType` must be enclosed in single quotes 276 | to prevent your terminal from expanding them into an argument list. 277 | 278 | ### Jbuilder & react-rails 279 | 280 | If you use Jbuilder to pass a JSON string to `react_component`, make sure your JSON is a stringified hash, 281 | not an array. This is not the Rails default -- you should add the root node yourself. For example: 282 | 283 | ```ruby 284 | # BAD: returns a stringified array 285 | json.array!(@messages) do |message| 286 | json.extract! message, :id, :name 287 | json.url message_url(message, format: :json) 288 | end 289 | 290 | # GOOD: returns a stringified hash 291 | json.messages(@messages) do |message| 292 | json.extract! message, :id, :name 293 | json.url message_url(message, format: :json) 294 | end 295 | ``` 296 | 297 | ## CoffeeScript 298 | 299 | It is possible to use JSX with CoffeeScript. To use CoffeeScript, create files with an extension `.js.jsx.coffee`. 300 | We also need to embed JSX code inside backticks so that CoffeeScript ignores the syntax it doesn't understand. 301 | Here's an example: 302 | 303 | ```coffee 304 | Component = React.createClass 305 | render: -> 306 | `<ExampleComponent videos={this.props.videos} />` 307 | ``` 308 | --------------------------------------------------------------------------------