Skip to content
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
55 changes: 55 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -205,6 +205,61 @@ react_component('HelloMessage', {name: 'John'}, {prerender: true})
```
This will return the fully rendered component markup, and as long as you have included the `react_ujs` script in your page, then the component will also be instantiated and mounted on the client.

### Component Generator

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.

For example:

```shell
rails generate react:component Post title:string body:string published:bool published_by:instanceOf{Person}
```

would generate the following in `app/assets/javascripts/components/post.js.jsx`:

```jsx
var Post = React.createClass({
propTypes: {
title: React.PropTypes.string,
body: React.PropTypes.string,
published: React.PropTypes.bool,
publishedBy: React.PropTypes.instanceOf(Person)
},

render: function() {
return (
<div>
<div>Title: {this.props.title}</div>
<div>Body: {this.props.body}</div>
<div>Published: {this.props.published}</div>
<div>Published By: {this.props.published_by}</div>
</div>
);
}
});
```

The generator can use the following arguments to create basic propTypes:

* any
* array
* bool
* element
* func
* number
* object
* node
* shape
* string

The following additional arguments have special behavior:

* `instanceOf` takes an optional class name in the form of {className}
* `oneOf` behaves like an enum, and takes an optional list of strings in the form of `'name:oneOf{one,two,three}'`.
* `oneOfType` takes an optional list of react and custom types in the form of `'model:oneOfType{string,number,OtherType}'`

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.

## Configuring

### Variants
Expand Down
127 changes: 127 additions & 0 deletions lib/generators/react/component_generator.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
module React
module Generators
class ComponentGenerator < ::Rails::Generators::NamedBase
source_root File.expand_path '../../templates', __FILE__
desc <<-DESC.strip_heredoc
Description:
Scaffold a react component into app/assets/javascripts/components.
The generated component will include a basic render function and a PropTypes
hash to help with development.

Available field types:

Basic prop types do not take any additional arguments. If you do not specify
a prop type, the generic node will be used. The basic types available are:

any
array
bool
element
func
number
object
node
shape
string

Special PropTypes take additional arguments in {}, and must be enclosed in
single quotes to keep bash from expanding the arguments in {}.

instanceOf
takes an optional class name in the form of {className}

oneOf
behaves like an enum, and takes an optional list of strings that will
be allowed in the form of 'name:oneOf{one,two,three}'.

oneOfType.
oneOfType takes an optional list of react and custom types in the form of
'model:oneOfType{string,number,OtherType}'

Examples:
rails g react:component person name
rails g react:component restaurant name:string rating:number owner:instanceOf{Person}
rails g react:component food 'kind:oneOf{meat,cheese,vegetable}'
rails g react:component events 'location:oneOfType{string,Restaurant}'
DESC

argument :attributes,
:type => :array,
:default => [],
:banner => "field[:type] field[:type] ..."

REACT_PROP_TYPES = {
"node" => 'React.PropTypes.node',
"bool" => 'React.PropTypes.bool',
"boolean" => 'React.PropTypes.bool',
"string" => 'React.PropTypes.string',
"number" => 'React.PropTypes.number',
"object" => 'React.PropTypes.object',
"array" => 'React.PropTypes.array',
"shape" => 'React.PropTypes.shape({})',
"element" => 'React.PropTypes.element',
"func" => 'React.PropTypes.func',
"function" => 'React.PropTypes.func',
"any" => 'React.PropTypes.any',

"instanceOf" => ->(type) {
'React.PropTypes.instanceOf(%s)' % type.to_s.camelize
},

"oneOf" => ->(*options) {
enums = options.map{|k| "'#{k.to_s}'"}.join(',')
'React.PropTypes.oneOf([%s])' % enums
},

"oneOfType" => ->(*options) {
types = options.map{|k| "#{lookup(k.to_s, k.to_s)}" }.join(',')
'React.PropTypes.oneOfType([%s])' % types
},
}

def create_component_file
extension = "js.jsx"
file_path = File.join('app/assets/javascripts/components', "#{file_name}.#{extension}")
template("component.#{extension}", file_path)
end

private

def parse_attributes!
self.attributes = (attributes || []).map do |attr|
name, type, options = "", "", ""
options_regex = /(?<options>{.*})/

name, type = attr.split(':')

if matchdata = options_regex.match(type)
options = matchdata[:options]
type = type.gsub(options_regex, '')
end

{ :name => name, :type => lookup(type, options) }
end
end

def self.lookup(type = "node", options = "")
react_prop_type = REACT_PROP_TYPES[type]
if react_prop_type.blank?
if type =~ /^[[:upper:]]/
react_prop_type = REACT_PROP_TYPES['instanceOf']
else
react_prop_type = REACT_PROP_TYPES['node']
end
end

options = options.to_s.gsub(/[{}]/, '').split(',')

react_prop_type = react_prop_type.call(*options) if react_prop_type.respond_to? :call
react_prop_type
end

def lookup(type = "node", options = "")
self.class.lookup(type, options)
end
end
end
end
23 changes: 23 additions & 0 deletions lib/generators/templates/component.js.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
var <%= file_name.camelize %> = React.createClass({
<% if attributes.size > 0 -%>
propTypes: {
<% attributes.each_with_index do |attribute, idx| -%>
<%= attribute[:name].camelize(:lower) %>: <%= attribute[:type] %><% if (idx < attributes.length-1) %>,<% end %>
<% end -%>
},
<% end -%>

render: function() {
<% if attributes.size > 0 -%>
return (
<div>
<% attributes.each do |attribute| -%>
<div><%= attribute[:name].titleize %>: {this.props.<%= attribute[:name] %>}</div>
<% end -%>
</div>
);
<% else -%>
return <div />;
<% end -%>
}
});
59 changes: 59 additions & 0 deletions test/generators/component_generator_test.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
require 'test_helper'
require 'generators/react/component_generator'

class ComponentGeneratorTest < Rails::Generators::TestCase
destination File.join(Rails.root, 'tmp', 'component_generator_test_output')
setup :prepare_destination
tests React::Generators::ComponentGenerator

def filename
'app/assets/javascripts/components/generated_component.js.jsx'
end

test "creates the component file" do
run_generator %w(GeneratedComponent)

assert_file filename
end

test "creates the component file with a node argument" do
run_generator %w(GeneratedComponent name)
assert_file filename, %r{name: React.PropTypes.node}
end

test "creates the component file with various standard proptypes" do
proptypes = %w(string bool number array func number object any)
run_generator %w(GeneratedComponent) + proptypes.map { |type| "my_#{type}:#{type}" }
proptypes.each do |type|
assert_file filename, %r(my#{type.capitalize}: React.PropTypes.#{type})
end
end

test "creates a component file with an instanceOf property" do
run_generator %w(GeneratedComponent favorite_food:instanceOf{food})
assert_file filename, /favoriteFood: React.PropTypes.instanceOf\(Food\)/
end

test "creates a component file with a oneOf property" do
run_generator %w(GeneratedComponent favorite_food:oneOf{pizza,hamburgers})
assert_file filename, /favoriteFood: React.PropTypes.oneOf\(\['pizza','hamburgers'\]\)/
end

test "creates a component file with a oneOfType property" do
run_generator %w(GeneratedComponent favorite_food:oneOfType{string,Food})
expected_property = "favoriteFood: React.PropTypes.oneOfType([React.PropTypes.string,React.PropTypes.instanceOf(Food)])"

assert_file filename, Regexp.new(Regexp.quote(expected_property))
end

test "generates working jsx" do
expected_name_div = Regexp.escape('React.createElement("div", null, "Name: ", this.props.name)')
expected_shape_div = Regexp.escape('React.createElement("div", null, "Address: ", this.props.address)')

run_generator %w(GeneratedComponent name:string address:shape)
jsx = React::JSX.transform(File.read(File.join(destination_root, filename)))

assert_match(Regexp.new(expected_name_div), jsx)
assert_match(Regexp.new(expected_shape_div), jsx)
end
end