├── 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 = /(?{.*})/
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'), "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'), "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>/)
58 |
59 | html = @helper.react_component('Foo', {}, {:class => 'test', :tag => :span, :data => {:foo => 1}})
60 | assert html.match(/<\/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 | [](http://rubygems.org/gems/react-rails)
2 | [](https://travis-ci.org/reactjs/react-rails)
3 | [](https://gemnasium.com/reactjs/react-rails)
4 | [](https://codeclimate.com/github/reactjs/react-rails)
5 | [](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 |
132 |
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 |
162 |
163 |
Hello, John!
164 |
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 | ``
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 |
234 |
Title: {this.props.title}
235 |
Body: {this.props.body}
236 |
Published: {this.props.published}
237 |
Published By: {this.props.publishedBy}
238 |
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 | ``
307 | ```
308 |
--------------------------------------------------------------------------------