From 9d4620c8ac4723bf0c9bb31eabed4c55da6e7417 Mon Sep 17 00:00:00 2001 From: WU Jun Date: Tue, 3 Dec 2013 08:26:25 +0800 Subject: [PATCH 1/6] Add a view helper to render react component. --- Appraisals | 3 + README.md | 33 +++++++ gemfiles/rails_3.1.gemfile | 1 + lib/react/rails.rb | 1 + lib/react/rails/view_helper.rb | 98 +++++++++++++++++++ react-rails.gemspec | 9 +- .../app/assets/javascripts/application.js | 6 ++ test/dummy/app/assets/javascripts/pages.js | 14 +++ .../dummy/app/controllers/pages_controller.rb | 5 + test/dummy/app/views/pages/show.html.erb | 5 + test/dummy/config/application.rb | 6 +- test/dummy/config/routes.rb | 55 +---------- test/test_helper.rb | 4 +- test/view_helper_test.rb | 50 ++++++++++ 14 files changed, 229 insertions(+), 61 deletions(-) create mode 100644 lib/react/rails/view_helper.rb create mode 100644 test/dummy/app/assets/javascripts/pages.js create mode 100644 test/dummy/app/controllers/pages_controller.rb create mode 100644 test/dummy/app/views/pages/show.html.erb create mode 100644 test/view_helper_test.rb diff --git a/Appraisals b/Appraisals index 5086c4d45..0c14cd5e1 100644 --- a/Appraisals +++ b/Appraisals @@ -1,5 +1,8 @@ appraise "rails-3.1" do gem 'rails', '~> 3.1' + # Buggy sprockets can break tests under Ruby 2.0. Use a decent version. + # See https://github.com/sstephenson/sprockets/issues/352 + gem 'sprockets', '>= 2.2.2' end appraise "rails-3.2" do diff --git a/README.md b/README.md index 5d6cf9433..2348d1f69 100644 --- a/README.md +++ b/README.md @@ -48,6 +48,39 @@ Alternatively, you can include it directly as a separate script tag: To transform your JSX into JS, simply create `.js.jsx` files, and ensure that the file has the `/** @jsx React.DOM */` docblock. These files will be transformed on request, or precompiled as part of the `assets:precompile` task. +### Viewer Helper + +There is a viewer helper called `react_component`. Suppose you have a `HelloMessage` React component defined: + +```js +/** @jsx React.DOM */ +var HelloMessage = React.createClass({ + render: function() { + return
{'Hello ' + this.props.name}
; + } +}); +``` + +You can render it in your views like this: + +```erb +<%= react_component('HelloMessage', :name => 'John') %> +``` + +By default, `react_component` will use a `
` element. You can use other tag: + +```erb +<%= react_component('HelloMessage', {:name => 'John'}, :span) %> +``` + +And you can specify HTML attributes: + +```erb +<%= react_component('HelloMessage', {:name => 'John'}, {:id => 'hello', :class => 'foo', :tag => :span}) %> +``` + +The helper is Turbolinks-aware. If you have trouble, make sure to include React in ``, not in ``. + ## Configuring diff --git a/gemfiles/rails_3.1.gemfile b/gemfiles/rails_3.1.gemfile index 58db7a878..365e8838d 100644 --- a/gemfiles/rails_3.1.gemfile +++ b/gemfiles/rails_3.1.gemfile @@ -3,5 +3,6 @@ source "http://rubygems.org" gem "rails", "~> 3.1" +gem "sprockets", ">= 2.2.1" gemspec :path=>"../" \ No newline at end of file diff --git a/lib/react/rails.rb b/lib/react/rails.rb index 21190dd43..18466040c 100644 --- a/lib/react/rails.rb +++ b/lib/react/rails.rb @@ -1,2 +1,3 @@ require 'react/rails/railtie' require 'react/rails/engine' +require 'react/rails/view_helper' diff --git a/lib/react/rails/view_helper.rb b/lib/react/rails/view_helper.rb new file mode 100644 index 000000000..905af61c7 --- /dev/null +++ b/lib/react/rails/view_helper.rb @@ -0,0 +1,98 @@ +module React + module Rails + module ViewHelper + # Render a react component named +name+. Returns a HTML tag and some + # javascript to render the component. The HTML tag is +div+ by default and + # can be changed by +options[:tag]+. If +options[:tag]+ is a symbol, use + # it as +options[:tag]+. HTML attributes can be specified by +options+. + # The javascript will encode +args+ to JSON and use it to construct the + # component. Use a block to render child elements, be aware that they will + # be replaced once the javascript gets executed. + # + # ==== Examples + # + # # // defined in a .jsx file: + # # var HelloMessage = React.createClass({ + # # render: function() { + # # return
{'Hello ' + this.props.name}
; + # # } + # # }); + # react_component(:HelloMessage, :name => 'John') + # + # # Use instead of
: + # react_component(:HelloMessage, {:name => 'John'}, :span) + # react_component(:HelloMessage, {:name => 'John'}, :tag => :span) + # + # # Add HTML attributes: + # react_component(:HelloMessage, {}, {:class => 'c', :id => 'i'}) + # + # # (ERB) Customize child elements: + # <%= react_component :HelloMessage do -%> + # Loading... + # <% end -%> + def react_component(name, args = {}, options = {}, &block) + html_tag, html_options = *react_parse_options(options) + + result = content_tag(html_tag, '', html_options, &block) + result << react_javascript_tag(name, args, html_options[:id]) + end + + private + + # Returns +[html_tag, html_options]+. + def react_parse_options(options) + # Syntactic sugar for specifying html tag. + return [options, {:id => SecureRandom::hex}] if options.is_a?(Symbol) + + # Assign a random id if missing. + options = options.reverse_merge(:id => SecureRandom::hex) + + # Use
by default. + tag = options[:tag] || :div + options.delete(:tag) + + [tag, options] + end + + def react_javascript_tag(name, args, element_id) + @@react_javascript_template ||= build_react_javascript_template + + javascript_tag(@@react_javascript_template % { + :element_id => element_id, + :name => name, + :args => args.to_json, + }) + end + + def build_react_javascript_template + load_events = [%w[document DOMContentLoaded]] + unload_events = [%w[window unload]] + if defined?(::Turbolinks) + load_events << %w[document page:load] + unload_events << %w[document page:before-change] + end + + # Minify javascript by removing spaces. + <<-"!".gsub(/(var\s)|(?:\s+)/, '\1') + (function() { + var e = document.getElementById('%{element_id}'), + f = function() { + e && React.renderComponent(%{name}(%{args}), e); + #{load_events.map {|e| "#{e[0]}.removeEventListener('#{e[1]}', f)"} * ';'} + }, + g = function() { + e && React.unmountComponentAtNode(e); + #{unload_events.map {|e| "#{e[0]}.removeEventListener('#{e[1]}', g)"} * ';'} + }; + #{load_events.map {|e| "#{e[0]}.addEventListener('#{e[1]}', f)"} * ';'}; + #{unload_events.map {|e| "#{e[0]}.addEventListener('#{e[1]}', g)"} * ';'} + })() + ! + end + end + end +end + +ActionView::Base.class_eval do + include ::React::Rails::ViewHelper +end diff --git a/react-rails.gemspec b/react-rails.gemspec index 8948485cf..8f13609bd 100644 --- a/react-rails.gemspec +++ b/react-rails.gemspec @@ -14,9 +14,12 @@ Gem::Specification.new do |s| s.author = ['Paul O’Shannessy'] s.email = ['paul@oshannessy.com'] - s.add_development_dependency "bundler", [">= 1.2.2"] - s.add_development_dependency "appraisal" - s.add_development_dependency "coffee-rails" + s.add_development_dependency 'bundler', '>= 1.2.2' + s.add_development_dependency 'appraisal' + s.add_development_dependency 'coffee-rails' + s.add_development_dependency 'turbolinks', '>= 2.0.0' + s.add_development_dependency 'es5-shim-rails', '>= 2.0.5' + s.add_development_dependency 'poltergeist', '>= 0.3.3' s.add_dependency 'execjs' s.add_dependency 'rails', '>= 3.1' diff --git a/test/dummy/app/assets/javascripts/application.js b/test/dummy/app/assets/javascripts/application.js index 5bc2e1c8b..1bc19ac8f 100644 --- a/test/dummy/app/assets/javascripts/application.js +++ b/test/dummy/app/assets/javascripts/application.js @@ -10,4 +10,10 @@ // Read Sprockets README (https://github.com/sstephenson/sprockets#sprockets-directives) for details // about supported directives. // +// +// es5-shim is necessary for PhantomJS to pass tests. See https://github.com/facebook/react/issues/303 +// +//= require turbolinks +//= require es5-shim/es5-shim +//= require react //= require_tree . diff --git a/test/dummy/app/assets/javascripts/pages.js b/test/dummy/app/assets/javascripts/pages.js new file mode 100644 index 000000000..79cc0a10e --- /dev/null +++ b/test/dummy/app/assets/javascripts/pages.js @@ -0,0 +1,14 @@ +var HelloMessage = React.createClass({ + getInitialState: function() { + return {greeting: 'Hello'}; + }, + goodbye: function() { + this.setState({greeting: 'Goodbye'}); + }, + render: function() { + return React.DOM.div({}, + React.DOM.div({}, this.state.greeting, ' ', this.props.name), + React.DOM.button({onClick: this.goodbye}, 'Goodbye') + ); + } +}); diff --git a/test/dummy/app/controllers/pages_controller.rb b/test/dummy/app/controllers/pages_controller.rb new file mode 100644 index 000000000..cdba6a88a --- /dev/null +++ b/test/dummy/app/controllers/pages_controller.rb @@ -0,0 +1,5 @@ +class PagesController < ApplicationController + def show + @name = %w(Alice Bob)[params[:id].to_i % 2] + end +end diff --git a/test/dummy/app/views/pages/show.html.erb b/test/dummy/app/views/pages/show.html.erb new file mode 100644 index 000000000..2c7ac3dfe --- /dev/null +++ b/test/dummy/app/views/pages/show.html.erb @@ -0,0 +1,5 @@ +
    +
  • <%= link_to 'Alice', page_path(:id => 0) %>
  • +
  • <%= link_to 'Bob', page_path(:id => 1) %>
  • +
+<%= react_component 'HelloMessage', :name => @name %> diff --git a/test/dummy/config/application.rb b/test/dummy/config/application.rb index e619601c7..af83c9f3e 100644 --- a/test/dummy/config/application.rb +++ b/test/dummy/config/application.rb @@ -7,8 +7,10 @@ require "sprockets/railtie" require "rails/test_unit/railtie" -Bundler.require(*Rails.groups) -require "react-rails" +# Make sure gems in development group are required, for example, react-rails and turbolinks. +# These gems are specified in .gemspec file by add_development_dependency. They are not runtime +# dependencies for react-rails project but probably runtime dependencies for this dummy rails app. +Bundler.require(*(Rails.groups | ['development'])) module Dummy class Application < Rails::Application diff --git a/test/dummy/config/routes.rb b/test/dummy/config/routes.rb index 76dbac302..7dd746607 100644 --- a/test/dummy/config/routes.rb +++ b/test/dummy/config/routes.rb @@ -1,56 +1,3 @@ Dummy::Application.routes.draw do - # The priority is based upon order of creation: first created -> highest priority. - # See how all your routes lay out with "rake routes". - - # You can have the root of your site routed with "root" - # root 'welcome#index' - - # Example of regular route: - # get 'products/:id' => 'catalog#view' - - # Example of named route that can be invoked with purchase_url(id: product.id) - # get 'products/:id/purchase' => 'catalog#purchase', as: :purchase - - # Example resource route (maps HTTP verbs to controller actions automatically): - # resources :products - - # Example resource route with options: - # resources :products do - # member do - # get 'short' - # post 'toggle' - # end - # - # collection do - # get 'sold' - # end - # end - - # Example resource route with sub-resources: - # resources :products do - # resources :comments, :sales - # resource :seller - # end - - # Example resource route with more complex sub-resources: - # resources :products do - # resources :comments - # resources :sales do - # get 'recent', on: :collection - # end - # end - - # Example resource route with concerns: - # concern :toggleable do - # post 'toggle' - # end - # resources :posts, concerns: :toggleable - # resources :photos, concerns: :toggleable - - # Example resource route within a namespace: - # namespace :admin do - # # Directs /admin/products/* to Admin::ProductsController - # # (app/controllers/admin/products_controller.rb) - # resources :products - # end + resources :pages, :only => [:show] end diff --git a/test/test_helper.rb b/test/test_helper.rb index c900e9690..2b562e98a 100644 --- a/test/test_helper.rb +++ b/test/test_helper.rb @@ -7,8 +7,8 @@ Rails.backtrace_cleaner.remove_silencers! # Remove cached files -Rails.root.join('tmp').tap do |tmp| - tmp.rmtree +Rails.root.join('tmp/cache').tap do |tmp| + tmp.rmtree if tmp.exist? tmp.mkpath end diff --git a/test/view_helper_test.rb b/test/view_helper_test.rb new file mode 100644 index 000000000..ccafccc83 --- /dev/null +++ b/test/view_helper_test.rb @@ -0,0 +1,50 @@ +require 'test_helper' + +require 'capybara/rails' +require 'capybara/poltergeist' + +Capybara.javascript_driver = :poltergeist +Capybara.app = Rails.application + +class ViewHelperTest < ActionDispatch::IntegrationTest + include Capybara::DSL + + setup do + Capybara.current_driver = Capybara.javascript_driver + end + + test 'assert render_component works' do + visit '/pages/1' + assert page.has_content?('Hello Bob') + + page.click_button 'Goodbye' + assert page.has_no_content?('Hello Bob') + assert page.has_content?('Goodbye Bob') + end + + test 'assert render_component works with turbolinks' do + visit '/pages/1' + assert page.has_content?('Hello Bob') + + # Try clicking links. + page.click_link('Alice') + assert page.has_content?('Hello Alice') + + page.click_link('Bob') + assert page.has_content?('Hello Bob') + + # Try Turbolinks javascript API. + page.execute_script('Turbolinks.visit("/pages/2");') + assert page.has_content?('Hello Alice') + + page.execute_script('Turbolinks.visit("/pages/1");') + assert page.has_content?('Hello Bob') + + # Component state is not persistent after clicking current page link. + page.click_button 'Goodbye' + assert page.has_content?('Goodbye Bob') + + page.click_link('Bob') + assert page.has_content?('Hello Bob') + end +end From 34244ba0599a5a7188f8ab36c39de7761294731c Mon Sep 17 00:00:00 2001 From: "Jun, WU" Date: Thu, 30 Jan 2014 23:02:35 +0800 Subject: [PATCH 2/6] Introduce react_ujs. * Implement react_ujs. * Replace inline scripts with react_ujs style data attributes. * Update tests and README. --- README.md | 46 ++++++----- lib/react/rails/view_helper.rb | 78 ++++--------------- .../app/assets/javascripts/application.js | 1 + test/view_helper_test.rb | 20 ++++- vendor/assets/javascripts/react_ujs.js | 62 +++++++++++++++ 5 files changed, 122 insertions(+), 85 deletions(-) create mode 100644 vendor/assets/javascripts/react_ujs.js diff --git a/README.md b/README.md index 2348d1f69..a9149f7a9 100644 --- a/README.md +++ b/README.md @@ -43,44 +43,48 @@ Alternatively, you can include it directly as a separate script tag: <%= javascript_include_tag "react" %> ``` - ### JSX To transform your JSX into JS, simply create `.js.jsx` files, and ensure that the file has the `/** @jsx React.DOM */` docblock. These files will be transformed on request, or precompiled as part of the `assets:precompile` task. -### Viewer Helper +### Unobtrusive javascript -There is a viewer helper called `react_component`. Suppose you have a `HelloMessage` React component defined: +Like [jquery-rails](https://github.com/rails/jquery-rails), there is `react_ujs`. It will call `React.renderComponent` for every elements with `data-react-class` attribute. Properties can be specified by `data-react-props` attribute in JSON format. For example: -```js -/** @jsx React.DOM */ -var HelloMessage = React.createClass({ - render: function() { - return
{'Hello ' + this.props.name}
; - } -}); +```erb + +
``` -You can render it in your views like this: +To use it, simply `require` it after `react.js`: -```erb -<%= react_component('HelloMessage', :name => 'John') %> +```js +// app/assets/application.js + +//= require react +//= require react_ujs ``` -By default, `react_component` will use a `
` element. You can use other tag: +If you want `react_ujs` to work with [Turbolinks](https://github.com/rails/turbolinks), make sure `= require react_ujs` is after `= require turbolinks`. -```erb -<%= react_component('HelloMessage', {:name => 'John'}, :span) %> -``` +### Viewer helper -And you can specify HTML attributes: +There is a viewer helper method `react_component`. It is designed to work with `react_ujs` and takes React class name, properties, HTML options as arguments: -```erb -<%= react_component('HelloMessage', {:name => 'John'}, {:id => 'hello', :class => 'foo', :tag => :span}) %> +```ruby +react_component('HelloMessage', :name => 'John') +#
``` -The helper is Turbolinks-aware. If you have trouble, make sure to include React in ``, not in ``. +By default, a `
` element is used. Other tag and HTML attributes can be specified: +```ruby +react_component('HelloMessage', {:name => 'John'}, :span) +# + +react_component('HelloMessage', {:name => 'John'}, {:id => 'hello', :class => 'foo', :tag => :span}) +# +``` ## Configuring diff --git a/lib/react/rails/view_helper.rb b/lib/react/rails/view_helper.rb index 905af61c7..d06f6a7fd 100644 --- a/lib/react/rails/view_helper.rb +++ b/lib/react/rails/view_helper.rb @@ -2,12 +2,14 @@ module React module Rails module ViewHelper # Render a react component named +name+. Returns a HTML tag and some - # javascript to render the component. The HTML tag is +div+ by default and - # can be changed by +options[:tag]+. If +options[:tag]+ is a symbol, use - # it as +options[:tag]+. HTML attributes can be specified by +options+. - # The javascript will encode +args+ to JSON and use it to construct the - # component. Use a block to render child elements, be aware that they will - # be replaced once the javascript gets executed. + # javascript to render the component. + # + # HTML attributes can be specified by +options+. The HTML tag is +div+ + # by default and can be changed by +options[:tag]+. If +options+ is a + # symbol, use it as +options[:tag]+. + # + # Static child elements can be rendered by using a block. Be aware that + # they will be replaced once javascript gets executed. # # ==== Examples # @@ -31,64 +33,16 @@ module ViewHelper # Loading... # <% end -%> def react_component(name, args = {}, options = {}, &block) - html_tag, html_options = *react_parse_options(options) - - result = content_tag(html_tag, '', html_options, &block) - result << react_javascript_tag(name, args, html_options[:id]) - end - - private - - # Returns +[html_tag, html_options]+. - def react_parse_options(options) - # Syntactic sugar for specifying html tag. - return [options, {:id => SecureRandom::hex}] if options.is_a?(Symbol) + options = {:tag => options} if options.is_a?(Symbol) - # Assign a random id if missing. - options = options.reverse_merge(:id => SecureRandom::hex) + html_options = options.merge({:data => { + :react_class => name, + :react_props => args.to_json, + }}) + html_tag = html_options.delete(:tag) || :div - # Use
by default. - tag = options[:tag] || :div - options.delete(:tag) - - [tag, options] - end - - def react_javascript_tag(name, args, element_id) - @@react_javascript_template ||= build_react_javascript_template - - javascript_tag(@@react_javascript_template % { - :element_id => element_id, - :name => name, - :args => args.to_json, - }) - end - - def build_react_javascript_template - load_events = [%w[document DOMContentLoaded]] - unload_events = [%w[window unload]] - if defined?(::Turbolinks) - load_events << %w[document page:load] - unload_events << %w[document page:before-change] - end - - # Minify javascript by removing spaces. - <<-"!".gsub(/(var\s)|(?:\s+)/, '\1') - (function() { - var e = document.getElementById('%{element_id}'), - f = function() { - e && React.renderComponent(%{name}(%{args}), e); - #{load_events.map {|e| "#{e[0]}.removeEventListener('#{e[1]}', f)"} * ';'} - }, - g = function() { - e && React.unmountComponentAtNode(e); - #{unload_events.map {|e| "#{e[0]}.removeEventListener('#{e[1]}', g)"} * ';'} - }; - #{load_events.map {|e| "#{e[0]}.addEventListener('#{e[1]}', f)"} * ';'}; - #{unload_events.map {|e| "#{e[0]}.addEventListener('#{e[1]}', g)"} * ';'} - })() - ! - end + content_tag(html_tag, '', html_options, &block) + end end end end diff --git a/test/dummy/app/assets/javascripts/application.js b/test/dummy/app/assets/javascripts/application.js index 1bc19ac8f..8e6679e04 100644 --- a/test/dummy/app/assets/javascripts/application.js +++ b/test/dummy/app/assets/javascripts/application.js @@ -16,4 +16,5 @@ //= require turbolinks //= require es5-shim/es5-shim //= require react +//= require react_ujs //= require_tree . diff --git a/test/view_helper_test.rb b/test/view_helper_test.rb index ccafccc83..6c2e61e88 100644 --- a/test/view_helper_test.rb +++ b/test/view_helper_test.rb @@ -10,10 +10,26 @@ class ViewHelperTest < ActionDispatch::IntegrationTest include Capybara::DSL setup do + @helper = ActionView::Base.new.extend(React::Rails::ViewHelper) Capybara.current_driver = Capybara.javascript_driver end - test 'assert render_component works' do + test 'react_component accepts React props' do + html = @helper.react_component('Foo', {bar: 'value'}) + %w(data-react-class="Foo" data-react-props="{"bar":"value"}").each do |segment| + assert html.include?(segment) + end + end + + test 'react_component accepts HTML options and HTML tag' do + assert @helper.react_component('Foo', {}, :span).match(/<\/span>/) + + html = @helper.react_component('Foo', {}, {:class => 'test', :tag => :span}) + assert html.match(/<\/span>/) + assert html.include?('class="test"') + end + + test 'react_ujs works with rendered HTML' do visit '/pages/1' assert page.has_content?('Hello Bob') @@ -22,7 +38,7 @@ class ViewHelperTest < ActionDispatch::IntegrationTest assert page.has_content?('Goodbye Bob') end - test 'assert render_component works with turbolinks' do + test 'react_ujs works with Turbolinks' do visit '/pages/1' assert page.has_content?('Hello Bob') diff --git a/vendor/assets/javascripts/react_ujs.js b/vendor/assets/javascripts/react_ujs.js new file mode 100644 index 000000000..00a35fda5 --- /dev/null +++ b/vendor/assets/javascripts/react_ujs.js @@ -0,0 +1,62 @@ +// Unobtrusive scripting adapter for React +(function(document, window, React) { + var CLASS_NAME_ATTR = 'data-react-class'; + var PROPS_ATTR = 'data-react-props'; + + // jQuery is optional. Use it to support legacy browsers. + var $ = (typeof jQuery !== 'undefined') && jQuery; + + var findReactDOMNodes = function() { + var SELECTOR = '[' + CLASS_NAME_ATTR + ']'; + if ($) { + return $(SELECTOR); + } else { + return document.querySelectorAll(SELECTOR); + } + }; + + var mountReactComponents = function() { + var nodes = findReactDOMNodes(); + for (var i = 0; i < nodes.length; ++i) { + var node = nodes[i]; + var className = node.getAttribute(CLASS_NAME_ATTR); + // Assume className is simple and can be found at top-level (window). + // Fallback to eval to handle cases like 'My.React.ComponentName'. + var constructor = window[className] || eval(className); + var props = JSON.parse(node.getAttribute(PROPS_ATTR) || '{}'); + React.renderComponent(constructor(props), node); + } + }; + + var unmountReactComponents = function() { + var nodes = findReactDOMNodes(); + for (var i = 0; i < nodes.length; ++i) { + React.unmountComponentAtNode(nodes[i]); + } + }; + + // Register page load & unload events + if ($) { + $(mountReactComponents); + $(window).unload(unmountReactComponents); + } else { + document.addEventListener('DOMContentLoaded', mountReactComponents); + window.addEventListener('unload', unmountReactComponents); + } + + // Turbolinks specified events + if (typeof Turbolinks !== 'undefined') { + var handleEvent; + if ($) { + handleEvent = function(eventName, callback) { + $(document).on(eventName, callback); + } + } else { + handleEvent = function(eventName, callback) { + document.addEventListener(eventName, callback); + } + } + handleEvent('page:load', mountReactComponents); + handleEvent('page:before-change', unmountReactComponents); + } +})(document, window, React); From 56f33a4c81d6d2da1158cbe96a6497d6e7fc7f35 Mon Sep 17 00:00:00 2001 From: WU Jun Date: Thu, 30 Jan 2014 23:46:55 +0800 Subject: [PATCH 3/6] Update JSXTransform test case, fix travis test. Newer coffee-script generates slightly different output, accept it in JSX coffee script test case. --- test/jsxtransform_test.rb | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/test/jsxtransform_test.rb b/test/jsxtransform_test.rb index e65d2dd55..973344aad 100644 --- a/test/jsxtransform_test.rb +++ b/test/jsxtransform_test.rb @@ -8,8 +8,7 @@ eos EXPECTED_JS_2 = < Date: Sun, 2 Feb 2014 14:14:33 +0800 Subject: [PATCH 4/6] Address issues about empty props. * react_ujs: use `null` instead of `{}` as default props. * view helper: remove 'data-react-props' if props is empty. * view helper: allow 'data' in HTML options. --- lib/react/rails/view_helper.rb | 9 +++++---- test/view_helper_test.rb | 3 ++- vendor/assets/javascripts/react_ujs.js | 3 ++- 3 files changed, 9 insertions(+), 6 deletions(-) diff --git a/lib/react/rails/view_helper.rb b/lib/react/rails/view_helper.rb index d06f6a7fd..8863bb3bb 100644 --- a/lib/react/rails/view_helper.rb +++ b/lib/react/rails/view_helper.rb @@ -35,10 +35,11 @@ module ViewHelper def react_component(name, args = {}, options = {}, &block) options = {:tag => options} if options.is_a?(Symbol) - html_options = options.merge({:data => { - :react_class => name, - :react_props => args.to_json, - }}) + html_options = options.reverse_merge(:data => {}) + html_options[:data].tap do |data| + data[:react_class] = name + data[:react_props] = args.to_json unless args.empty? + end html_tag = html_options.delete(:tag) || :div content_tag(html_tag, '', html_options, &block) diff --git a/test/view_helper_test.rb b/test/view_helper_test.rb index 6c2e61e88..40fa564aa 100644 --- a/test/view_helper_test.rb +++ b/test/view_helper_test.rb @@ -24,9 +24,10 @@ class ViewHelperTest < ActionDispatch::IntegrationTest test 'react_component accepts HTML options and HTML tag' do assert @helper.react_component('Foo', {}, :span).match(/<\/span>/) - html = @helper.react_component('Foo', {}, {:class => 'test', :tag => :span}) + html = @helper.react_component('Foo', {}, {:class => 'test', :tag => :span, :data => {:foo => 1}}) assert html.match(/<\/span>/) assert html.include?('class="test"') + assert html.include?('data-foo="1"') end test 'react_ujs works with rendered HTML' do diff --git a/vendor/assets/javascripts/react_ujs.js b/vendor/assets/javascripts/react_ujs.js index 00a35fda5..8f8426719 100644 --- a/vendor/assets/javascripts/react_ujs.js +++ b/vendor/assets/javascripts/react_ujs.js @@ -23,7 +23,8 @@ // Assume className is simple and can be found at top-level (window). // Fallback to eval to handle cases like 'My.React.ComponentName'. var constructor = window[className] || eval(className); - var props = JSON.parse(node.getAttribute(PROPS_ATTR) || '{}'); + var propsJson = node.getAttribute(PROPS_ATTR); + var props = propsJson && JSON.parse(propsJson); React.renderComponent(constructor(props), node); } }; From 2a56e8ff3742c7a2a081b8c6a80310429836295f Mon Sep 17 00:00:00 2001 From: WU Jun Date: Mon, 3 Feb 2014 14:27:54 +0800 Subject: [PATCH 5/6] Set `eval` context to `window`. This allows better minification. --- vendor/assets/javascripts/react_ujs.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/vendor/assets/javascripts/react_ujs.js b/vendor/assets/javascripts/react_ujs.js index 8f8426719..ed576e862 100644 --- a/vendor/assets/javascripts/react_ujs.js +++ b/vendor/assets/javascripts/react_ujs.js @@ -22,7 +22,7 @@ var className = node.getAttribute(CLASS_NAME_ATTR); // Assume className is simple and can be found at top-level (window). // Fallback to eval to handle cases like 'My.React.ComponentName'. - var constructor = window[className] || eval(className); + var constructor = window[className] || eval.call(window, className); var propsJson = node.getAttribute(PROPS_ATTR); var props = propsJson && JSON.parse(propsJson); React.renderComponent(constructor(props), node); From 8120d1e348fa548cb9ecd41aa492e7a4f8bd6580 Mon Sep 17 00:00:00 2001 From: WU Jun Date: Mon, 10 Feb 2014 14:39:15 +0800 Subject: [PATCH 6/6] Update README * Fix a comment about React.renderComponent where the order of arguments is wrong * Mention that react_ujs will unmount components and how to avoid it --- README.md | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index a9149f7a9..32a3d3e7a 100644 --- a/README.md +++ b/README.md @@ -49,24 +49,25 @@ To transform your JSX into JS, simply create `.js.jsx` files, and ensure that th ### Unobtrusive javascript -Like [jquery-rails](https://github.com/rails/jquery-rails), there is `react_ujs`. It will call `React.renderComponent` for every elements with `data-react-class` attribute. Properties can be specified by `data-react-props` attribute in JSON format. For example: +`react_ujs` will call `React.renderComponent` for every element with `data-react-class` attribute. React properties can be specified by `data-react-props` attribute in JSON format. For example: ```erb - +
``` -To use it, simply `require` it after `react.js`: +`react_ujs` will also scan DOM elements and call `React.unmountComponentAtNode` on page unload. If you want to disable this behavior, remove `data-react-class` attribute in `componentDidMount`. + +To use `react_ujs`, simply `require` it after `react` (and after `turbolinks` if [Turbolinks](https://github.com/rails/turbolinks) is used): ```js // app/assets/application.js +//= require turbolinks //= require react //= require react_ujs ``` -If you want `react_ujs` to work with [Turbolinks](https://github.com/rails/turbolinks), make sure `= require react_ujs` is after `= require turbolinks`. - ### Viewer helper There is a viewer helper method `react_component`. It is designed to work with `react_ujs` and takes React class name, properties, HTML options as arguments: @@ -86,6 +87,7 @@ react_component('HelloMessage', {:name => 'John'}, {:id => 'hello', :class => 'f # ``` + ## Configuring ### Variants