Skip to content

Commit dda3c8d

Browse files
committed
Merge pull request #96 from xionon/component-generator
Add command-line component generator
2 parents 5ad3c77 + 9e0ad7d commit dda3c8d

File tree

4 files changed

+264
-0
lines changed

4 files changed

+264
-0
lines changed

README.md

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -205,6 +205,61 @@ react_component('HelloMessage', {name: 'John'}, {prerender: true})
205205
```
206206
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.
207207

208+
### Component Generator
209+
210+
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.
211+
212+
For example:
213+
214+
```shell
215+
rails generate react:component Post title:string body:string published:bool published_by:instanceOf{Person}
216+
```
217+
218+
would generate the following in `app/assets/javascripts/components/post.js.jsx`:
219+
220+
```jsx
221+
var Post = React.createClass({
222+
propTypes: {
223+
title: React.PropTypes.string,
224+
body: React.PropTypes.string,
225+
published: React.PropTypes.bool,
226+
publishedBy: React.PropTypes.instanceOf(Person)
227+
},
228+
229+
render: function() {
230+
return (
231+
<div>
232+
<div>Title: {this.props.title}</div>
233+
<div>Body: {this.props.body}</div>
234+
<div>Published: {this.props.published}</div>
235+
<div>Published By: {this.props.published_by}</div>
236+
</div>
237+
);
238+
}
239+
});
240+
```
241+
242+
The generator can use the following arguments to create basic propTypes:
243+
244+
* any
245+
* array
246+
* bool
247+
* element
248+
* func
249+
* number
250+
* object
251+
* node
252+
* shape
253+
* string
254+
255+
The following additional arguments have special behavior:
256+
257+
* `instanceOf` takes an optional class name in the form of {className}
258+
* `oneOf` behaves like an enum, and takes an optional list of strings in the form of `'name:oneOf{one,two,three}'`.
259+
* `oneOfType` takes an optional list of react and custom types in the form of `'model:oneOfType{string,number,OtherType}'`
260+
261+
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.
262+
208263
## Configuring
209264

210265
### Variants
Lines changed: 127 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -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
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -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+
});
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
require 'test_helper'
2+
require 'generators/react/component_generator'
3+
4+
class ComponentGeneratorTest < Rails::Generators::TestCase
5+
destination File.join(Rails.root, 'tmp', 'component_generator_test_output')
6+
setup :prepare_destination
7+
tests React::Generators::ComponentGenerator
8+
9+
def filename
10+
'app/assets/javascripts/components/generated_component.js.jsx'
11+
end
12+
13+
test "creates the component file" do
14+
run_generator %w(GeneratedComponent)
15+
16+
assert_file filename
17+
end
18+
19+
test "creates the component file with a node argument" do
20+
run_generator %w(GeneratedComponent name)
21+
assert_file filename, %r{name: React.PropTypes.node}
22+
end
23+
24+
test "creates the component file with various standard proptypes" do
25+
proptypes = %w(string bool number array func number object any)
26+
run_generator %w(GeneratedComponent) + proptypes.map { |type| "my_#{type}:#{type}" }
27+
proptypes.each do |type|
28+
assert_file filename, %r(my#{type.capitalize}: React.PropTypes.#{type})
29+
end
30+
end
31+
32+
test "creates a component file with an instanceOf property" do
33+
run_generator %w(GeneratedComponent favorite_food:instanceOf{food})
34+
assert_file filename, /favoriteFood: React.PropTypes.instanceOf\(Food\)/
35+
end
36+
37+
test "creates a component file with a oneOf property" do
38+
run_generator %w(GeneratedComponent favorite_food:oneOf{pizza,hamburgers})
39+
assert_file filename, /favoriteFood: React.PropTypes.oneOf\(\['pizza','hamburgers'\]\)/
40+
end
41+
42+
test "creates a component file with a oneOfType property" do
43+
run_generator %w(GeneratedComponent favorite_food:oneOfType{string,Food})
44+
expected_property = "favoriteFood: React.PropTypes.oneOfType([React.PropTypes.string,React.PropTypes.instanceOf(Food)])"
45+
46+
assert_file filename, Regexp.new(Regexp.quote(expected_property))
47+
end
48+
49+
test "generates working jsx" do
50+
expected_name_div = Regexp.escape('React.createElement("div", null, "Name: ", this.props.name)')
51+
expected_shape_div = Regexp.escape('React.createElement("div", null, "Address: ", this.props.address)')
52+
53+
run_generator %w(GeneratedComponent name:string address:shape)
54+
jsx = React::JSX.transform(File.read(File.join(destination_root, filename)))
55+
56+
assert_match(Regexp.new(expected_name_div), jsx)
57+
assert_match(Regexp.new(expected_shape_div), jsx)
58+
end
59+
end

0 commit comments

Comments
 (0)