Skip to content

Commit da5589c

Browse files
committed
Add command-line component generator
Adds a generator that scaffolds up a complete, but minimal, component using JSX and a propTypes hash. Run `rails generate react:component --help` for usage notes.
1 parent 9d9c41e commit da5589c

File tree

3 files changed

+213
-0
lines changed

3 files changed

+213
-0
lines changed
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 renderable will be used. The basic types available are:
15+
16+
any
17+
array
18+
bool
19+
component
20+
func
21+
number
22+
object
23+
renderable
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+
"renderable" => 'React.PropTypes.renderable',
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+
"component" => 'React.PropTypes.component',
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 = "renderable", 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['renderable']
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 = "renderable", options = "")
123+
self.class.lookup(type, options)
124+
end
125+
end
126+
end
127+
end
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
/** @jsx React.DOM */
2+
var <%= file_name.camelize %> = React.createClass({
3+
<% if attributes.size > 0 -%>
4+
propTypes: {
5+
<% attributes.each_with_index do |attribute, idx| -%>
6+
<%= attribute[:name].camelize(:lower) %>: <%= attribute[:type] %><% if (idx < attributes.length-1) %>,<% end %>
7+
<% end -%>
8+
},
9+
<% end -%>
10+
11+
render: function() {
12+
<% if attributes.size > 0 -%>
13+
return <div>
14+
<% attributes.each do |attribute| -%>
15+
<div><%= attribute[:name].titleize %>: {this.props.<%= attribute[:name] %>}</div>
16+
<% end -%>
17+
</div>;
18+
<% else -%>
19+
return <div />;
20+
<% end -%>
21+
}
22+
})
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
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 jsx pragma" do
20+
run_generator %w(GeneratedComponent)
21+
assert_file filename, %r{/\*\* @jsx React\.DOM \*/}
22+
end
23+
24+
test "creates the component file with a renderable argument" do
25+
run_generator %w(GeneratedComponent name)
26+
assert_file filename, %r{name: React.PropTypes.renderable}
27+
end
28+
29+
test "creates the component file with various standard proptypes" do
30+
proptypes = %w(string bool number array func number object any)
31+
run_generator %w(GeneratedComponent) + proptypes.map { |type| "my_#{type}:#{type}" }
32+
proptypes.each do |type|
33+
assert_file filename, %r(my#{type.capitalize}: React.PropTypes.#{type})
34+
end
35+
end
36+
37+
test "creates a component file with an instanceOf property" do
38+
run_generator %w(GeneratedComponent favorite_food:instanceOf{food})
39+
assert_file filename, /favoriteFood: React.PropTypes.instanceOf\(Food\)/
40+
end
41+
42+
test "creates a component file with a oneOf property" do
43+
run_generator %w(GeneratedComponent favorite_food:oneOf{pizza,hamburgers})
44+
assert_file filename, /favoriteFood: React.PropTypes.oneOf\(\['pizza','hamburgers'\]\)/
45+
end
46+
47+
test "creates a component file with a oneOfType property" do
48+
run_generator %w(GeneratedComponent favorite_food:oneOfType{string,Food})
49+
expected_property = "favoriteFood: React.PropTypes.oneOfType([React.PropTypes.string,React.PropTypes.instanceOf(Food)])"
50+
51+
assert_file filename, Regexp.new(Regexp.quote(expected_property))
52+
end
53+
54+
test "generates working jsx" do
55+
expected_name_div = Regexp.escape('React.DOM.div(null, "Name: ", this.props.name)')
56+
expected_shape_div = Regexp.escape('React.DOM.div(null, "Address: ", this.props.address)')
57+
58+
run_generator %w(GeneratedComponent name:string address:shape)
59+
jsx = React::JSX.transform(File.read(File.join(destination_root, filename)))
60+
61+
assert_match(Regexp.new(expected_name_div), jsx)
62+
assert_match(Regexp.new(expected_shape_div), jsx)
63+
end
64+
end

0 commit comments

Comments
 (0)