react-rails 0.13.0.0 → 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 83ce3f640e7f27ac175bc2dd031197ab9f7978e4
4
- data.tar.gz: 52408128f938aa4f6a1608fbd0eef1b73213d1d6
3
+ metadata.gz: ecb96344ad4094e2bb7d7ffe7b45455b807fc7a3
4
+ data.tar.gz: 8003608ada04a2d20573621278faa29daf6c838c
5
5
  SHA512:
6
- metadata.gz: a27db732edf425e1e10942ad7768ba5b2bb705c8a3eceae4443dd74541ab9c0871da53cf63d71a57de84af2d393332059c09bb97216d3096d47d124f4b1aa822
7
- data.tar.gz: c7fd4405e8da388da7c2c3f9b12ed5de3526596abac96f94ee2b78310e0b66014da7cda9eae6b1dc53577e7ff4b4c32a7564a3910e20c698537f1929db6ed98a
6
+ metadata.gz: fa90a0d40e3c856840d1b17bc207da5e2bc48f3b1587368eb97c6f4291776253a3b859fca7dbe01da62dd3c1ddd50dc2299bf80c3502bcc513b6e7fb3a9927b7
7
+ data.tar.gz: b126df93a0c17fba3c78e71433fc77d9688a6d3be7fb3ba45f1e968a24a8caaa151b29a5a9c3cccd29a446ddccf17f8409268c54476b7127ef78319c3ad26dfa
data/README.md CHANGED
@@ -1,90 +1,253 @@
1
- # react-rails [![Build Status](https://travis-ci.org/reactjs/react-rails.png)](https://travis-ci.org/reactjs/react-rails) [![Code Climate](https://codeclimate.com/github/reactjs/react-rails.png)](https://codeclimate.com/github/reactjs/react-rails)
1
+ [![Gem](https://img.shields.io/gem/v/react-rails.svg?style=flat-square)](http://rubygems.org/gems/react-rails)
2
+ [![Build Status](https://img.shields.io/travis/reactjs/react-rails/master.svg?style=flat-square)](https://travis-ci.org/reactjs/react-rails)
3
+ [![Gemnasium](https://img.shields.io/gemnasium/reactjs/react-rails.svg?style=flat-square)](https://gemnasium.com/reactjs/react-rails)
4
+ [![Code Climate](https://img.shields.io/codeclimate/github/reactjs/react-rails.svg?style=flat-square)](https://codeclimate.com/github/reactjs/react-rails)
2
5
 
3
- react-rails is a ruby gem which makes it easier to use [React](http://facebook.github.io/react/) and [JSX](http://facebook.github.io/react/docs/jsx-in-depth.html) in your Ruby on Rails application.
6
+ * * *
4
7
 
5
- This is done in 2 ways:
8
+ # react-rails
6
9
 
7
- 1. making it easy to include `react.js` as part of your dependencies in `application.js`.
8
- 2. transforming JSX into regular JS on request, or as part of asset precompilation.
9
10
 
11
+ `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) in your Ruby on Rails (3.1+) application. `react-rails` can:
10
12
 
11
- ## Installation
13
+ - Provide [various `react` builds](#reactjs-builds) to your asset bundle
14
+ - Transform [`.jsx` in the asset pipeline](#jsx)
15
+ - [Render components into views and mount them](#rendering--mounting) via view helper & `react_ujs`
16
+ - [Render components server-side](#server-rendering) with `prerender: true`.
17
+ - [Generate components](#component-generator) with a Rails generator
12
18
 
13
- We're specifically targeting versions of Ruby on Rails which make use of the asset pipeline, which means Rails 3.1+.
19
+ ## Installation
14
20
 
15
- As with all gem dependencies, we strongly recommend adding `react-rails` to your `Gemfile` and using `bundler` to manage your application's dependencies.
21
+ Add `react-rails` to your gemfile:
16
22
 
17
23
  ```ruby
18
- # Gemfile
24
+ gem 'react-rails', '~> 1.0'
25
+ ```
19
26
 
20
- gem 'react-rails', '~> 0.12.0.0'
27
+ Next, run the installation script.
28
+
29
+ ```bash
30
+ rails g react:install
21
31
  ```
22
32
 
33
+ This will:
34
+ - create a `components.js` manifest file and a `app/assets/javascripts/components/` directory, where you will put your components
35
+ - place the following in your `application.js`:
36
+
37
+ ```js
38
+ //= require react
39
+ //= require react_ujs
40
+ //= require components
41
+ ```
23
42
 
24
43
  ## Usage
25
44
 
26
- ### react.js
45
+ ### React.js builds
27
46
 
28
- In order to use React client-side in your application, you must make sure the browser requests it. One way to do that is to drop `react.js` into `app/assets/javascript/` and by default your application manifest will pick it up. There are downsides to this approach, so we made it even easier. Once you have `react-rails` installed, you can just add a line into your config file (see Configuring) and require react directly in your manifest:
47
+ You can pick which React.js build (development, production, with or without [add-ons]((http://facebook.github.io/react/docs/addons.html))) to serve in each environment by adding a config. Here are the defaults:
48
+
49
+ ```ruby
50
+ # config/environments/development.rb
51
+ MyApp::Application.configure do
52
+ config.react.variant = :development
53
+ end
29
54
 
30
- You can `require` it in your manifest:
55
+ # config/environments/production.rb
56
+ MyApp::Application.configure do
57
+ config.react.variant = :production
58
+ end
59
+ ```
31
60
 
32
- ```js
33
- // app/assets/application.js
61
+ To include add-ons, use this config:
34
62
 
35
- //= require react
63
+ ```ruby
64
+ MyApp::Application.configure do
65
+ config.react.addons = true # defaults to false
66
+ end
67
+ ```
68
+
69
+ After restarting your Rails server, `//= require react` will provide the build of React.js which was specified by the configurations.
70
+
71
+ `react-rails` offers a few other options for versions & builds of React.js. See [VERSIONS.md](https://github.com/reactjs/react-rails/blob/master/VERSIONS.md) for more info about using the `react-source` gem or dropping in your own copies of React.js.
72
+
73
+ ### JSX
74
+
75
+ After installing `react-rails`, restart your server. Now, `.js.jsx` files will be transformed in the asset pipeline.
76
+
77
+ You can use JSX `--harmony` or `--strip-types` options by adding a configuration:
78
+
79
+ ```ruby
80
+ config.react.jsx_transform_options = {
81
+ harmony: true,
82
+ strip_types: true, # for removing Flow type annotations
83
+ }
84
+ ```
85
+
86
+ To use CoffeeScript, create `.js.jsx.coffee` files and embed JSX inside backticks, for example:
87
+
88
+ ```coffee
89
+ Component = React.createClass
90
+ render: ->
91
+ `<ExampleComponent videos={this.props.videos} />`
36
92
  ```
37
93
 
38
- Alternatively, you can include it directly as a separate script tag:
94
+ ### Rendering & mounting
95
+
96
+ `react-rails` includes a view helper (`react_component`) and an unobtrusive JavaScript driver (`react_ujs`) which work together to put React components on the page. You should require the UJS driver in your manifest after `react` (and after `turbolinks` if you use [Turbolinks](https://github.com/rails/turbolinks))
97
+
98
+ The __view helper__ puts a `div` on the page with the requested component class & props. For example:
39
99
 
40
100
  ```erb
41
- # app/views/layouts/application.erb.html
101
+ <%= react_component('HelloMessage', name: 'John') %>
102
+ <!-- becomes: -->
103
+ <div data-react-class="HelloMessage" data-react-props="{&quot;name&quot;:&quot;John&quot;}"></div>
104
+ ```
105
+
106
+ On page load, the __`react_ujs` driver__ will scan the page and mount components using `data-react-class` and `data-react-props`. Before page unload, it will unmount components (if you want to disable this behavior, remove `data-react-class` attribute in `componentDidMount`).
107
+
108
+ `react_ujs` uses Turbolinks events if they're available, otherwise, it uses native events. __Turbolinks >= 2.4.0__ is recommended because it exposes better events.
42
109
 
43
- <%= javascript_include_tag "react" %>
110
+ The view helper's signature is
111
+
112
+ ```ruby
113
+ react_component(component_class_name, props={}, html_options={})
44
114
  ```
45
115
 
116
+ - `component_class_name` is a string which names a globally-accessible component class. It may have dots (eg, `"MyApp.Header.MenuItem"`).
117
+ - `props` is either an object that responds to `#to_json` or an already-stringified JSON object (eg, made with Jbuilder, see note below)
118
+ - `html_options` may include:
119
+ - `tag:` to use an element other than a `div` to embed `data-react-class` and `-props`.
120
+ - `prerender: true` to render the component on the server.
121
+ - `**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).
46
122
 
47
- ### JSX
48
123
 
49
- To transform your JSX into JS, simply create `.js.jsx` files. These files will be transformed on request, or precompiled as part of the `assets:precompile` task.
124
+ ### Server rendering
125
+
126
+ To render components on the server, pass `prerender: true` to `react_component`:
50
127
 
128
+ ```erb
129
+ <%= react_component('HelloMessage', {name: 'John'}, {prerender: true}) %>
130
+ <!-- becomes: -->
131
+ <div data-react-class="HelloMessage" data-react-props="{&quot;name&quot;:&quot;John&quot;}">
132
+ <h1>Hello, John!</h1>
133
+ </div>
134
+ ```
135
+
136
+ _(It will be also be mounted by the UJS on page load.)_
51
137
 
52
- ## Configuring
138
+ There are some requirements for this to work:
53
139
 
54
- ### Variants
140
+ - `react-rails` must load your code. By convention, it uses `components.js`, which was created by the install task. This file must include your components _and_ their dependencies (eg, Underscore.js).
141
+ - Your components must be accessible in the global scope. If you are using `.js.jsx.coffee` files then the wrapper function needs to be taken into account:
55
142
 
56
- There are 2 variants available. `:development` gives you the unminified version of React. This provides extra debugging and error prevention. `:production` gives you the minified version of React which strips out comments and helpful warnings, and minifies.
143
+ ```coffee
144
+ # @ is `window`:
145
+ @Component = React.createClass
146
+ render: ->
147
+ `<ExampleComponent videos={this.props.videos} />`
148
+ ```
149
+ - Your code can't reference `document`. Prerender processes don't have access to `document`, so jQuery and some other libs won't work in this environment :(
150
+
151
+ You can configure your pool of JS virtual machines and specify where it should load code:
57
152
 
58
153
  ```ruby
59
- # config/environments/development.rb
154
+ # config/environments/application.rb
155
+ # These are the defaults if you dont specify any yourself
60
156
  MyApp::Application.configure do
61
- config.react.variant = :development
157
+ # renderer pool size:
158
+ config.react.max_renderers = 10
159
+ # prerender timeout, in seconds:
160
+ config.react.timeout = 20
161
+ # where to get React.js source:
162
+ config.react.react_js = lambda { File.read(::Rails.application.assets.resolve('react.js')) }
163
+ # array of filenames that will be requested from the asset pipeline
164
+ # and concatenated:
165
+ config.react.component_filenames = ['components.js']
166
+ # server-side console.log, console.warn, and console.error messages will be replayed on the client
167
+ # (you can set this to `true` in config/enviroments/development.rb to replay in development only)
168
+ config.react.replay_console = false
62
169
  end
170
+ ```
63
171
 
64
- # config/environments/production.rb
65
- MyApp::Application.configure do
66
- config.react.variant = :production
67
- end
172
+ ### Component generator
173
+
174
+ react-rails ships with a Rails generator to help you get started with a simple component scaffold. You can run it using `rails generate react:component ComponentName`. The generator takes an optional list of arguments for default propTypes, which follow the conventions set in the [Reusable Components](http://facebook.github.io/react/docs/reusable-components.html) section of the React documentation.
175
+
176
+ For example:
177
+
178
+ ```shell
179
+ rails generate react:component Post title:string body:string published:bool published_by:instanceOf{Person}
68
180
  ```
69
181
 
70
- ### Add-ons
182
+ would generate the following in `app/assets/javascripts/components/post.js.jsx`:
183
+
184
+ ```jsx
185
+ var Post = React.createClass({
186
+ propTypes: {
187
+ title: React.PropTypes.string,
188
+ body: React.PropTypes.string,
189
+ published: React.PropTypes.bool,
190
+ publishedBy: React.PropTypes.instanceOf(Person)
191
+ },
192
+
193
+ render: function() {
194
+ return (
195
+ <div>
196
+ <div>Title: {this.props.title}</div>
197
+ <div>Body: {this.props.body}</div>
198
+ <div>Published: {this.props.published}</div>
199
+ <div>Published By: {this.props.published_by}</div>
200
+ </div>
201
+ );
202
+ }
203
+ });
204
+ ```
205
+
206
+ The generator can use the following arguments to create basic propTypes:
207
+
208
+ * any
209
+ * array
210
+ * bool
211
+ * element
212
+ * func
213
+ * number
214
+ * object
215
+ * node
216
+ * shape
217
+ * string
218
+
219
+ The following additional arguments have special behavior:
71
220
 
72
- Beginning with React v0.5, there is another type of build. This build ships with some "add-ons" that might be useful - [take a look at the React documentation for details](http://facebook.github.io/react/docs/addons.html). In order to make these available, we've added another configuration (which defaults to `false`).
221
+ * `instanceOf` takes an optional class name in the form of {className}
222
+ * `oneOf` behaves like an enum, and takes an optional list of strings in the form of `'name:oneOf{one,two,three}'`.
223
+ * `oneOfType` takes an optional list of react and custom types in the form of `'model:oneOfType{string,number,OtherType}'`
224
+
225
+ Note that the arguments for `oneOf` and `oneOfType` must be enclosed in single quotes to prevent your terminal from expanding them into an argument list.
226
+
227
+ ### Jbuilder & react-rails
228
+
229
+ If you use Jbuilder to pass JSON string to `react_component`, make sure your JSON is a stringified hash, not an array. This is not the Rails default -- you should add the root node yourself. For example:
73
230
 
74
231
  ```ruby
75
- MyApp::Application.configure do
76
- config.react.addons = true
232
+ # BAD: returns a stringified array
233
+ json.array!(@messages) do |message|
234
+ json.extract! message, :id, :name
235
+ json.url message_url(message, format: :json)
77
236
  end
78
- ```
79
237
 
238
+ # GOOD: returns a stringified hash
239
+ json.messages(@messages) do |message|
240
+ json.extract! message, :id, :name
241
+ json.url message_url(message, format: :json)
242
+ end
243
+ ```
80
244
 
81
245
  ## CoffeeScript
82
246
 
83
- It is possible to use JSX with CoffeeScript. We need to embed JSX inside backticks so CoffeeScript ignores the syntax it doesn't understand. Here's an example:
247
+ It is possible to use JSX with CoffeeScript. The caveat is that you will still need to include the docblock. Since CoffeeScript doesn't allow `/* */` style comments, we need to do something a little different. We also need to embed JSX inside backticks so CoffeeScript ignores the syntax it doesn't understand. Here's an example:
84
248
 
85
249
  ```coffee
86
250
  Component = React.createClass
87
251
  render: ->
88
252
  `<ExampleComponent videos={this.props.videos} />`
89
253
  ```
90
-
@@ -0,0 +1,104 @@
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
+ $(window).unload(function() {window.ReactRailsUJS.unmountComponents()});
93
+ } else {
94
+ document.addEventListener('DOMContentLoaded', function() {window.ReactRailsUJS.mountComponents()});
95
+ window.addEventListener('unload', function() {window.ReactRailsUJS.unmountComponents()});
96
+ }
97
+ }
98
+
99
+ if (typeof Turbolinks !== 'undefined' && Turbolinks.supported) {
100
+ handleTurbolinksEvents();
101
+ } else {
102
+ handleNativeEvents();
103
+ }
104
+ })(document, window);
@@ -0,0 +1,127 @@
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
+ REACT_PROP_TYPES = {
54
+ "node" => 'React.PropTypes.node',
55
+ "bool" => 'React.PropTypes.bool',
56
+ "boolean" => 'React.PropTypes.bool',
57
+ "string" => 'React.PropTypes.string',
58
+ "number" => 'React.PropTypes.number',
59
+ "object" => 'React.PropTypes.object',
60
+ "array" => 'React.PropTypes.array',
61
+ "shape" => 'React.PropTypes.shape({})',
62
+ "element" => 'React.PropTypes.element',
63
+ "func" => 'React.PropTypes.func',
64
+ "function" => 'React.PropTypes.func',
65
+ "any" => 'React.PropTypes.any',
66
+
67
+ "instanceOf" => ->(type) {
68
+ 'React.PropTypes.instanceOf(%s)' % type.to_s.camelize
69
+ },
70
+
71
+ "oneOf" => ->(*options) {
72
+ enums = options.map{|k| "'#{k.to_s}'"}.join(',')
73
+ 'React.PropTypes.oneOf([%s])' % enums
74
+ },
75
+
76
+ "oneOfType" => ->(*options) {
77
+ types = options.map{|k| "#{lookup(k.to_s, k.to_s)}" }.join(',')
78
+ 'React.PropTypes.oneOfType([%s])' % types
79
+ },
80
+ }
81
+
82
+ def create_component_file
83
+ extension = "js.jsx"
84
+ file_path = File.join('app/assets/javascripts/components', "#{file_name}.#{extension}")
85
+ template("component.#{extension}", file_path)
86
+ end
87
+
88
+ private
89
+
90
+ def parse_attributes!
91
+ self.attributes = (attributes || []).map do |attr|
92
+ name, type, options = "", "", ""
93
+ options_regex = /(?<options>{.*})/
94
+
95
+ name, type = attr.split(':')
96
+
97
+ if matchdata = options_regex.match(type)
98
+ options = matchdata[:options]
99
+ type = type.gsub(options_regex, '')
100
+ end
101
+
102
+ { :name => name, :type => lookup(type, options) }
103
+ end
104
+ end
105
+
106
+ def self.lookup(type = "node", options = "")
107
+ react_prop_type = REACT_PROP_TYPES[type]
108
+ if react_prop_type.blank?
109
+ if type =~ /^[[:upper:]]/
110
+ react_prop_type = REACT_PROP_TYPES['instanceOf']
111
+ else
112
+ react_prop_type = REACT_PROP_TYPES['node']
113
+ end
114
+ end
115
+
116
+ options = options.to_s.gsub(/[{}]/, '').split(',')
117
+
118
+ react_prop_type = react_prop_type.call(*options) if react_prop_type.respond_to? :call
119
+ react_prop_type
120
+ end
121
+
122
+ def lookup(type = "node", options = "")
123
+ self.class.lookup(type, options)
124
+ end
125
+ end
126
+ end
127
+ end
@@ -0,0 +1,58 @@
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
@@ -0,0 +1,23 @@
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
+ <div>
14
+ <% attributes.each do |attribute| -%>
15
+ <div><%= attribute[:name].titleize %>: {this.props.<%= attribute[:name] %>}</div>
16
+ <% end -%>
17
+ </div>
18
+ );
19
+ <% else -%>
20
+ return <div />;
21
+ <% end -%>
22
+ }
23
+ });
@@ -1,3 +1,5 @@
1
1
  require 'react/jsx'
2
+ require 'react/renderer'
2
3
  require 'react/rails'
4
+ require 'react/console'
3
5
 
@@ -0,0 +1,30 @@
1
+ module React
2
+ class Console
3
+ def self.polyfill_js
4
+ # Overwrite global `console` object with something that can capture messages
5
+ # to return to client later for debugging
6
+ <<-JS
7
+ var console = { history: [] };
8
+ ['error', 'log', 'info', 'warn'].forEach(function (fn) {
9
+ console[fn] = function () {
10
+ console.history.push({level: fn, arguments: Array.prototype.slice.call(arguments)});
11
+ };
12
+ });
13
+ JS
14
+ end
15
+
16
+ def self.replay_as_script_js
17
+ <<-JS
18
+ (function (history) {
19
+ if (history && history.length > 0) {
20
+ result += '\\n<scr'+'ipt>';
21
+ history.forEach(function (msg) {
22
+ result += '\\nconsole.' + msg.level + '.apply(console, ' + JSON.stringify(msg.arguments) + ');';
23
+ });
24
+ result += '\\n</scr'+'ipt>';
25
+ }
26
+ })(console.history);
27
+ JS
28
+ end
29
+ end
30
+ end
@@ -1,21 +1,36 @@
1
1
  require 'execjs'
2
2
  require 'react/source'
3
3
  require 'react/jsx/template'
4
+ require 'rails'
4
5
 
5
6
  module React
6
7
  module JSX
8
+ mattr_accessor :transform_options
9
+
7
10
  def self.context
8
- # TODO: create React::Source::contents_for
9
- contents =
10
- # If execjs uses therubyracer, there is no 'global'. Make sure
11
- # we have it so JSX script can work properly.
12
- 'var global = global || this;' +
13
- File.read(React::Source.bundled_path_for('JSXTransformer.js'))
14
- @context ||= ExecJS.compile(contents)
11
+ # lazily loaded during first request and reloaded every time when in dev or test
12
+ unless @context && ::Rails.env.production?
13
+ contents =
14
+ # If execjs uses therubyracer, there is no 'global'. Make sure
15
+ # we have it so JSX script can work properly.
16
+ 'var global = global || this;' +
17
+
18
+ # search for transformer file using sprockets - allows user to override
19
+ # this file in his own application
20
+ File.read(::Rails.application.assets.resolve('JSXTransformer.js'))
21
+
22
+ @context = ExecJS.compile(contents)
23
+ end
24
+
25
+ @context
15
26
  end
16
27
 
17
- def self.transform(code)
18
- result = context.call('JSXTransformer.transform', code)
28
+ def self.transform(code, options={})
29
+ js_options = {
30
+ stripTypes: options[:strip_types],
31
+ harmony: options[:harmony],
32
+ }
33
+ result = context.call('JSXTransformer.transform', code, js_options)
19
34
  return result['code']
20
35
  end
21
36
  end
@@ -9,8 +9,8 @@ module React
9
9
  def prepare
10
10
  end
11
11
 
12
- def evaluate(scopre, locals, &block)
13
- @output ||= JSX::transform(data)
12
+ def evaluate(scope, locals, &block)
13
+ @output ||= JSX::transform(data, JSX.transform_options)
14
14
  end
15
15
  end
16
16
  end
@@ -1,2 +1,3 @@
1
1
  require 'react/rails/railtie'
2
2
  require 'react/rails/engine'
3
+ require 'react/rails/view_helper'
@@ -1,7 +1,7 @@
1
1
  module React
2
2
  module Rails
3
3
  class Engine < ::Rails::Engine
4
- initializer "react_rails.setup_engine", :after => "sprockets.environment", :group => :all do |app|
4
+ initializer "react_rails.setup_engine", :group => :all do |app|
5
5
  app.assets.register_engine '.jsx', React::JSX::Template
6
6
  end
7
7
  end
@@ -5,32 +5,83 @@ module React
5
5
  class Railtie < ::Rails::Railtie
6
6
  config.react = ActiveSupport::OrderedOptions.new
7
7
 
8
- initializer "react_rails.setup_vendor", :after => "sprockets.environment", group: :all do |app|
9
- variant = app.config.react.variant
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
+ # Server-side rendering
13
+ config.react.max_renderers = 10
14
+ config.react.timeout = 20 #seconds
15
+ config.react.react_js = lambda {File.read(::Rails.application.assets.resolve('react.js'))}
16
+ config.react.component_filenames = ['components.js']
10
17
 
18
+ # Watch .jsx files for changes in dev, so we can reload the JS VMs with the new JS code.
19
+ initializer "react_rails.add_watchable_files", group: :all do |app|
20
+ app.config.watchable_files.concat Dir["#{app.root}/app/assets/javascripts/**/*.jsx*"]
21
+ end
22
+
23
+ # Include the react-rails view helper lazily
24
+ initializer "react_rails.setup_view_helpers", group: :all do |app|
25
+ React::JSX.transform_options = app.config.react.jsx_transform_options
26
+ ActiveSupport.on_load(:action_view) do
27
+ include ::React::Rails::ViewHelper
28
+ end
29
+ end
30
+
31
+ initializer "react_rails.setup_vendor", group: :all do |app|
11
32
  # Mimic behavior of ember-rails...
12
33
  # We want to include different files in dev/prod. The unminified builds
13
34
  # contain console logging for invariants and logging to help catch
14
35
  # common mistakes. These are all stripped out in the minified build.
15
- if variant = app.config.react.variant || ::Rails.env.test?
16
- variant ||= :development
17
- addons = app.config.react.addons || false
18
-
19
- # Copy over the variant into a path that sprockets will pick up.
20
- # We'll always copy to 'react.js' so that no includes need to change.
21
- # We'll also always copy of JSXTransformer.js
22
- tmp_path = app.root.join('tmp/react-rails')
23
- filename = 'react' + (addons ? '-with-addons' : '') + (variant == :production ? '.min.js' : '.js')
24
- FileUtils.mkdir_p(tmp_path)
25
- FileUtils.cp(::React::Source.bundled_path_for(filename),
26
- tmp_path.join('react.js'))
27
- FileUtils.cp(::React::Source.bundled_path_for('JSXTransformer.js'),
28
- tmp_path.join('JSXTransformer.js'))
29
-
30
- # Make sure it can be found
31
- app.assets.append_path(tmp_path)
36
+
37
+ # Copy over the variant into a path that sprockets will pick up.
38
+ # We'll always copy to 'react.js' so that no includes need to change.
39
+ # We'll also always copy of JSXTransformer.js
40
+ tmp_path = app.root.join('tmp/react-rails')
41
+ filename = 'react' +
42
+ (app.config.react.addons ? '-with-addons' : '') +
43
+ (app.config.react.variant == :production ? '.min.js' : '.js')
44
+ FileUtils.mkdir_p(tmp_path)
45
+ FileUtils.cp(::React::Source.bundled_path_for(filename),
46
+ tmp_path.join('react.js'))
47
+ FileUtils.cp(::React::Source.bundled_path_for('JSXTransformer.js'),
48
+ tmp_path.join('JSXTransformer.js'))
49
+ app.assets.prepend_path tmp_path
50
+
51
+ # Allow overriding react files that are not based on environment
52
+ # e.g. /vendor/assets/react/JSXTransformer.js
53
+ dropin_path = app.root.join("vendor/assets/react")
54
+ app.assets.prepend_path dropin_path if dropin_path.exist?
55
+
56
+ # Allow overriding react files that are based on environment
57
+ # e.g. /vendor/assets/react/development/react.js
58
+ dropin_path_env = app.root.join("vendor/assets/react/#{app.config.react.variant}")
59
+ app.assets.prepend_path dropin_path_env if dropin_path_env.exist?
60
+ end
61
+
62
+
63
+ config.after_initialize do |app|
64
+ # Server Rendering
65
+ # Concat component_filenames together for server rendering
66
+ app.config.react.components_js = lambda {
67
+ app.config.react.component_filenames.map do |filename|
68
+ app.assets[filename].to_s
69
+ end.join(";")
70
+ }
71
+
72
+ do_setup = lambda do
73
+ cfg = app.config.react
74
+ React::Renderer.setup!( cfg.react_js, cfg.components_js, cfg.replay_console,
75
+ {:size => cfg.max_renderers, :timeout => cfg.timeout})
32
76
  end
77
+
78
+ do_setup.call
79
+
80
+ # Reload the JS VMs in dev when files change
81
+ ActionDispatch::Reloader.to_prepare(&do_setup)
33
82
  end
83
+
84
+
34
85
  end
35
86
  end
36
87
  end
@@ -1,8 +1,6 @@
1
1
  module React
2
2
  module Rails
3
- # Version numbers will track react-source, but we'll add another level so
4
- # that we can increment, but have some amount of stability.
5
- VERSION = '0.13.0.0'
3
+ # If you change this, make sure to update VERSIONS.md
4
+ VERSION = '1.0.0'
6
5
  end
7
6
  end
8
-
@@ -0,0 +1,28 @@
1
+ module React
2
+ module Rails
3
+ module ViewHelper
4
+
5
+ # Render a UJS-type HTML tag annotated with data attributes, which
6
+ # are used by react_ujs to actually instantiate the React component
7
+ # on the client.
8
+ #
9
+ def react_component(name, args = {}, options = {}, &block)
10
+ options = {:tag => options} if options.is_a?(Symbol)
11
+ block = Proc.new{concat React::Renderer.render(name, args)} if options[:prerender]
12
+
13
+ html_options = options.reverse_merge(:data => {})
14
+ html_options[:data].tap do |data|
15
+ data[:react_class] = name
16
+ data[:react_props] = React::Renderer.react_props(args) unless args.empty?
17
+ end
18
+ html_tag = html_options[:tag] || :div
19
+
20
+ # remove internally used properties so they aren't rendered to DOM
21
+ html_options.except!(:tag, :prerender)
22
+
23
+ content_tag(html_tag, '', html_options, &block)
24
+ end
25
+
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,83 @@
1
+ require 'connection_pool'
2
+
3
+ module React
4
+ class Renderer
5
+
6
+ class PrerenderError < RuntimeError
7
+ def initialize(component_name, props, js_message)
8
+ message = ["Encountered error \"#{js_message}\" when prerendering #{component_name} with #{props}",
9
+ js_message.backtrace.join("\n")].join("\n")
10
+ super(message)
11
+ end
12
+ end
13
+
14
+ cattr_accessor :pool
15
+
16
+ def self.setup!(react_js, components_js, replay_console, args={})
17
+ args.assert_valid_keys(:size, :timeout)
18
+ @@react_js = react_js
19
+ @@components_js = components_js
20
+ @@replay_console = replay_console
21
+ @@pool.shutdown{} if @@pool
22
+ reset_combined_js!
23
+ default_pool_options = {:size =>10, :timeout => 20}
24
+ @@pool = ConnectionPool.new(default_pool_options.merge(args)) { self.new }
25
+ end
26
+
27
+ def self.render(component, args={})
28
+ @@pool.with do |renderer|
29
+ renderer.render(component, args)
30
+ end
31
+ end
32
+
33
+ def self.react_props(args={})
34
+ if args.is_a? String
35
+ args
36
+ else
37
+ args.to_json
38
+ end
39
+ end
40
+
41
+ def context
42
+ @context ||= ExecJS.compile(self.class.combined_js)
43
+ end
44
+
45
+ def render(component, args={})
46
+ react_props = React::Renderer.react_props(args)
47
+ jscode = <<-JS
48
+ (function () {
49
+ var result = React.renderToString(React.createElement(#{component}, #{react_props}));
50
+ #{@@replay_console ? React::Console.replay_as_script_js : ''}
51
+ return result;
52
+ })()
53
+ JS
54
+ context.eval(jscode).html_safe
55
+ rescue ExecJS::ProgramError => e
56
+ raise PrerenderError.new(component, react_props, e)
57
+ end
58
+
59
+
60
+ private
61
+
62
+ def self.setup_combined_js
63
+ <<-JS
64
+ var global = global || this;
65
+ var self = self || this;
66
+ var window = window || this;
67
+ #{React::Console.polyfill_js}
68
+ #{@@react_js.call};
69
+ React = global.React;
70
+ #{@@components_js.call};
71
+ JS
72
+ end
73
+
74
+ def self.reset_combined_js!
75
+ @@combined_js = setup_combined_js
76
+ end
77
+
78
+ def self.combined_js
79
+ @@combined_js
80
+ end
81
+
82
+ end
83
+ end
metadata CHANGED
@@ -1,15 +1,29 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: react-rails
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.13.0.0
4
+ version: 1.0.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Paul O’Shannessy
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2015-03-10 00:00:00.000000000 Z
11
+ date: 2015-04-07 00:00:00.000000000 Z
12
12
  dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: appraisal
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: '0'
20
+ type: :development
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ">="
25
+ - !ruby/object:Gem::Version
26
+ version: '0'
13
27
  - !ruby/object:Gem::Dependency
14
28
  name: bundler
15
29
  requirement: !ruby/object:Gem::Requirement
@@ -25,7 +39,7 @@ dependencies:
25
39
  - !ruby/object:Gem::Version
26
40
  version: 1.2.2
27
41
  - !ruby/object:Gem::Dependency
28
- name: appraisal
42
+ name: coffee-rails
29
43
  requirement: !ruby/object:Gem::Requirement
30
44
  requirements:
31
45
  - - ">="
@@ -39,14 +53,98 @@ dependencies:
39
53
  - !ruby/object:Gem::Version
40
54
  version: '0'
41
55
  - !ruby/object:Gem::Dependency
42
- name: coffee-rails
56
+ name: es5-shim-rails
43
57
  requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - ">="
60
+ - !ruby/object:Gem::Version
61
+ version: 2.0.5
62
+ type: :development
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - ">="
67
+ - !ruby/object:Gem::Version
68
+ version: 2.0.5
69
+ - !ruby/object:Gem::Dependency
70
+ name: jbuilder
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - ">="
74
+ - !ruby/object:Gem::Version
75
+ version: '0'
76
+ type: :development
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
44
79
  requirements:
45
80
  - - ">="
46
81
  - !ruby/object:Gem::Version
47
82
  version: '0'
83
+ - !ruby/object:Gem::Dependency
84
+ name: poltergeist
85
+ requirement: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - ">="
88
+ - !ruby/object:Gem::Version
89
+ version: 0.3.3
48
90
  type: :development
49
91
  prerelease: false
92
+ version_requirements: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - ">="
95
+ - !ruby/object:Gem::Version
96
+ version: 0.3.3
97
+ - !ruby/object:Gem::Dependency
98
+ name: test-unit
99
+ requirement: !ruby/object:Gem::Requirement
100
+ requirements:
101
+ - - "~>"
102
+ - !ruby/object:Gem::Version
103
+ version: '2.5'
104
+ type: :development
105
+ prerelease: false
106
+ version_requirements: !ruby/object:Gem::Requirement
107
+ requirements:
108
+ - - "~>"
109
+ - !ruby/object:Gem::Version
110
+ version: '2.5'
111
+ - !ruby/object:Gem::Dependency
112
+ name: turbolinks
113
+ requirement: !ruby/object:Gem::Requirement
114
+ requirements:
115
+ - - ">="
116
+ - !ruby/object:Gem::Version
117
+ version: 2.0.0
118
+ type: :development
119
+ prerelease: false
120
+ version_requirements: !ruby/object:Gem::Requirement
121
+ requirements:
122
+ - - ">="
123
+ - !ruby/object:Gem::Version
124
+ version: 2.0.0
125
+ - !ruby/object:Gem::Dependency
126
+ name: coffee-script-source
127
+ requirement: !ruby/object:Gem::Requirement
128
+ requirements:
129
+ - - "~>"
130
+ - !ruby/object:Gem::Version
131
+ version: '1.8'
132
+ type: :runtime
133
+ prerelease: false
134
+ version_requirements: !ruby/object:Gem::Requirement
135
+ requirements:
136
+ - - "~>"
137
+ - !ruby/object:Gem::Version
138
+ version: '1.8'
139
+ - !ruby/object:Gem::Dependency
140
+ name: connection_pool
141
+ requirement: !ruby/object:Gem::Requirement
142
+ requirements:
143
+ - - ">="
144
+ - !ruby/object:Gem::Version
145
+ version: '0'
146
+ type: :runtime
147
+ prerelease: false
50
148
  version_requirements: !ruby/object:Gem::Requirement
51
149
  requirements:
52
150
  - - ">="
@@ -84,16 +182,16 @@ dependencies:
84
182
  name: react-source
85
183
  requirement: !ruby/object:Gem::Requirement
86
184
  requirements:
87
- - - '='
185
+ - - "~>"
88
186
  - !ruby/object:Gem::Version
89
- version: 0.13.0
187
+ version: '0.13'
90
188
  type: :runtime
91
189
  prerelease: false
92
190
  version_requirements: !ruby/object:Gem::Requirement
93
191
  requirements:
94
- - - '='
192
+ - - "~>"
95
193
  - !ruby/object:Gem::Version
96
- version: 0.13.0
194
+ version: '0.13'
97
195
  description: Compile your JSX on demand or precompile for production.
98
196
  email:
99
197
  - paul@oshannessy.com
@@ -103,14 +201,21 @@ extra_rdoc_files: []
103
201
  files:
104
202
  - LICENSE
105
203
  - README.md
204
+ - lib/assets/javascripts/react_ujs.js.erb
205
+ - lib/generators/react/component_generator.rb
206
+ - lib/generators/react/install_generator.rb
207
+ - lib/generators/templates/component.js.jsx
106
208
  - lib/react-rails.rb
209
+ - lib/react/console.rb
107
210
  - lib/react/jsx.rb
108
211
  - lib/react/jsx/template.rb
109
212
  - lib/react/rails.rb
110
213
  - lib/react/rails/engine.rb
111
214
  - lib/react/rails/railtie.rb
112
215
  - lib/react/rails/version.rb
113
- homepage: https://github.com/facebook/react-rails
216
+ - lib/react/rails/view_helper.rb
217
+ - lib/react/renderer.rb
218
+ homepage: https://github.com/reactjs/react-rails
114
219
  licenses:
115
220
  - APL 2.0
116
221
  metadata: {}
@@ -130,7 +235,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
130
235
  version: '0'
131
236
  requirements: []
132
237
  rubyforge_project:
133
- rubygems_version: 2.2.2
238
+ rubygems_version: 2.4.5
134
239
  signing_key:
135
240
  specification_version: 4
136
241
  summary: React/JSX adapter for the Ruby on Rails asset pipeline.