Skip to content

View helper to render react component #17

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 6 commits into from
Feb 10, 2014
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions Appraisals
Original file line number Diff line number Diff line change
@@ -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
Expand Down
41 changes: 40 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,11 +43,50 @@ 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.

### Unobtrusive javascript

`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
<!-- react_ujs will execute `React.renderComponent(HelloMessage({name:"Bob"}), element)` -->
<div data-react-class="HelloMessage" data-react-props="<%= {:name => 'Bob'}.to_json %>" />
```

`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
```

### 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:

```ruby
react_component('HelloMessage', :name => 'John')
# <div data-react-class="HelloMessage" data-react-props="{&quot;name&quot;:&quot;John&quot;}"></div>
```

By default, a `<div>` element is used. Other tag and HTML attributes can be specified:

```ruby
react_component('HelloMessage', {:name => 'John'}, :span)
# <span data-...></span>

react_component('HelloMessage', {:name => 'John'}, {:id => 'hello', :class => 'foo', :tag => :span})
# <span class="foo" id="hello" data-...></span>
```


## Configuring

Expand Down
1 change: 1 addition & 0 deletions gemfiles/rails_3.1.gemfile
Original file line number Diff line number Diff line change
Expand Up @@ -3,5 +3,6 @@
source "http://rubygems.org"

gem "rails", "~> 3.1"
gem "sprockets", ">= 2.2.1"

gemspec :path=>"../"
1 change: 1 addition & 0 deletions lib/react/rails.rb
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
require 'react/rails/railtie'
require 'react/rails/engine'
require 'react/rails/view_helper'
53 changes: 53 additions & 0 deletions lib/react/rails/view_helper.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
module React
module Rails
module ViewHelper
# Render a react component named +name+. Returns a HTML tag and some
# 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
#
# # // <HelloMessage> defined in a .jsx file:
# # var HelloMessage = React.createClass({
# # render: function() {
# # return <div>{'Hello ' + this.props.name}</div>;
# # }
# # });
# react_component(:HelloMessage, :name => 'John')
#
# # Use <span> instead of <div>:
# 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)
options = {:tag => options} if options.is_a?(Symbol)

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)
end
end
end
end

ActionView::Base.class_eval do
include ::React::Rails::ViewHelper
end
9 changes: 6 additions & 3 deletions react-rails.gemspec
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down
7 changes: 7 additions & 0 deletions test/dummy/app/assets/javascripts/application.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,4 +10,11 @@
// 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 react_ujs
//= require_tree .
14 changes: 14 additions & 0 deletions test/dummy/app/assets/javascripts/pages.js
Original file line number Diff line number Diff line change
@@ -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')
);
}
});
5 changes: 5 additions & 0 deletions test/dummy/app/controllers/pages_controller.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
class PagesController < ApplicationController
def show
@name = %w(Alice Bob)[params[:id].to_i % 2]
end
end
5 changes: 5 additions & 0 deletions test/dummy/app/views/pages/show.html.erb
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
<ul>
<li><%= link_to 'Alice', page_path(:id => 0) %></li>
<li><%= link_to 'Bob', page_path(:id => 1) %></li>
</ul>
<%= react_component 'HelloMessage', :name => @name %>
6 changes: 4 additions & 2 deletions test/dummy/config/application.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
55 changes: 1 addition & 54 deletions test/dummy/config/routes.rb
Original file line number Diff line number Diff line change
@@ -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
11 changes: 8 additions & 3 deletions test/jsxtransform_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,7 @@
eos

EXPECTED_JS_2 = <<eos
/** @jsx React.DOM*/

/** @jsx React.DOM */

(function() {
var Component;
Expand All @@ -34,7 +33,13 @@ class JSXTransformTest < ActionDispatch::IntegrationTest
test 'asset pipeline should transform JSX + Coffeescript' do
get 'assets/example2.js'
assert_response :success
assert_equal EXPECTED_JS_2, @response.body
# Different coffee-script may generate slightly different outputs:
# 1. Some version inserts an extra "\n" at the beginning.
# 2. "/** @jsx React.DOM */" and "/** @jsx React.DOM*/" are both possible.
#
# Because appraisal is used, multiple versions of coffee-script are treated
# together. Remove all spaces to make test pass.
assert_equal EXPECTED_JS_2.gsub(/\s/, ''), @response.body.gsub(/\s/, '')
end

end
4 changes: 2 additions & 2 deletions test/test_helper.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
67 changes: 67 additions & 0 deletions test/view_helper_test.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
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
@helper = ActionView::Base.new.extend(React::Rails::ViewHelper)
Capybara.current_driver = Capybara.javascript_driver
end

test 'react_component accepts React props' do
html = @helper.react_component('Foo', {bar: 'value'})
%w(data-react-class="Foo" data-react-props="{&quot;bar&quot;:&quot;value&quot;}").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\s.*><\/span>/)

html = @helper.react_component('Foo', {}, {:class => 'test', :tag => :span, :data => {:foo => 1}})
assert html.match(/<span\s.*><\/span>/)
assert html.include?('class="test"')
assert html.include?('data-foo="1"')
end

test 'react_ujs works with rendered HTML' 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 'react_ujs 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
Loading