├── .gitignore
├── .reek.yml
├── .rspec
├── .rubocop.yml
├── .travis.yml
├── CHANGELOG.md
├── Gemfile
├── README.md
├── Rakefile
├── bin
├── console
└── setup
├── lib
└── rack
│ ├── component.rb
│ └── component
│ ├── memory_cache.rb
│ ├── renderer.rb
│ └── version.rb
├── rack-component.gemspec
└── spec
├── benchmarks.rb
├── components.rb
├── rack
└── component_spec.rb
├── raw_rack_example_spec.rb
├── readme_spec.rb
└── spec_helper.rb
/.gitignore:
--------------------------------------------------------------------------------
1 | /.bundle/
2 | /.yardoc
3 | /_yardoc/
4 | /coverage/
5 | /doc/
6 | /pkg/
7 | /spec/reports/
8 | /tmp/
9 | *.gem
10 | Gemfile.lock
11 |
12 | # rspec failure tracking
13 | .rspec_status
14 |
--------------------------------------------------------------------------------
/.reek.yml:
--------------------------------------------------------------------------------
1 | detectors:
2 | IrresponsibleModule:
3 | enabled: false
4 | UncommunicativeVariableName:
5 | enabled: false
6 | NestedIterators:
7 | ignore_iterators:
8 | - each_object
9 | UtilityFunction:
10 | enabled: false
11 | UncommunicativeMethodName:
12 | enabled: false
13 | exclude_paths:
14 | - bin
15 | - client
16 |
--------------------------------------------------------------------------------
/.rspec:
--------------------------------------------------------------------------------
1 | --format documentation
2 | --color
3 | --require spec_helper
4 |
--------------------------------------------------------------------------------
/.rubocop.yml:
--------------------------------------------------------------------------------
1 | AllCops:
2 | TargetRubyVersion: 2.5
3 | Layout/LeadingCommentSpace:
4 | Enabled: false
5 | Metrics/BlockLength:
6 | Enabled: false
7 | Metrics/MethodLength:
8 | ExcludedMethods:
9 | - render
10 | Style/FrozenStringLiteralComment:
11 | EnforcedStyle: never
12 | Style/AsciiComments:
13 | Enabled: false
14 | StyleGuide: http://relaxed.ruby.style/#styleasciicomments
15 | Style/TrailingCommaInArguments:
16 | Enabled: false
17 | StyleGuide: http://relaxed.ruby.style/#styletrailingcommainarguments
18 | EnforcedStyleForMultiline: consistent_comma
19 | Style/TrailingCommaInArrayLiteral:
20 | Enabled: false
21 | StyleGuide: http://relaxed.ruby.style/#styletrailingcommainliteral
22 | EnforcedStyleForMultiline: consistent_comma
23 | Style/TrailingCommaInHashLiteral:
24 | Enabled: false
25 | StyleGuide: http://relaxed.ruby.style/#styletrailingcommainliteral
26 | EnforcedStyleForMultiline: consistent_comma
27 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | ---
2 | language: ruby
3 | cache: bundler
4 | rvm:
5 | - 2.6.2
6 | - 2.5.5
7 | - 2.4
8 | - 2.3
9 | before_install:
10 | - gem update --system
11 | - gem install bundler
12 |
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # Changelog
2 | All notable changes to this project will be documented in this file.
3 |
4 | The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
5 | and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
6 |
7 | ## 0.5.0
8 | ### Fixed
9 | - The `env` argument of the `render` block is now optional, as per standard Ruby
10 | block behavior.
11 | ```ruby
12 | class WorksInThisVersion < Rack::Component
13 | render do
14 | 'This component raised an ArgumentError in old versions but works now.'
15 | end
16 | end
17 |
18 | class StillWorks < Rack::Component
19 | render do |env|
20 | 'This style still works. Using |keyword:, arguments:| in env is nice.'
21 | end
22 | end
23 | ```
24 |
25 | ### Added
26 | - A changelog
27 | - Templating via [tilt](https://github.com/rtomayko/tilt), with support for
28 | escaping HTML by default
29 |
30 | ### Removed
31 | - Calling `Component.memoized(env)` is no longer supported. Use Sam Saffron's
32 | [lru_redux](https://github.com/SamSaffron/lru_redux) as an almost drop-in
33 | replacement, like this:
34 |
35 | ```ruby
36 | require 'rack/component'
37 | require 'lru_redux'
38 | class MyComponent < Rack::Component
39 | Cache = LruRedux::ThreadSafeCache.new(100)
40 |
41 | render do |env|
42 | Cache.getset(env) { 'this block will render after checking the cache' }
43 | end
44 | end
45 | ```
46 |
47 | ## 0.4.2 - 2019-01-04
48 | ### Added
49 | - `#h` method for escaping HTML inside interpolated strings
50 |
51 | ## 0.4.1 - 2019-01-02
52 | - First public, documented release
53 |
--------------------------------------------------------------------------------
/Gemfile:
--------------------------------------------------------------------------------
1 | source 'https://rubygems.org'
2 |
3 | git_source(:github) { |repo_name| "https://github.com/#{repo_name}" }
4 |
5 | # Specify your gem's dependencies in rack-component.gemspec
6 | gemspec
7 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Rack::Component
2 |
3 | Like a React.js component, a `Rack::Component` implements a `render` method that
4 | takes input data and returns what to display. You can use Components instead of
5 | Controllers, Views, Templates, and Helpers, in any Rack app.
6 |
7 | ## Install
8 |
9 | Add `rack-component` to your Gemfile and run `bundle install`:
10 |
11 | ```
12 | gem 'rack-component'
13 | ```
14 |
15 | ## Quickstart with Sinatra
16 |
17 | ```ruby
18 | # config.ru
19 | require 'sinatra'
20 | require 'rack/component'
21 |
22 | class Hello < Rack::Component
23 | render do |env|
24 | "
Hello, #{h env[:name]}
"
25 | end
26 | end
27 |
28 | get '/hello/:name' do
29 | Hello.call(name: params[:name])
30 | end
31 |
32 | run Sinatra::Application
33 | ```
34 |
35 | **Note that Rack::Component does not escape strings by default**. To escape
36 | strings, you can either use the `#h` helper like in the example above, or you
37 | can configure your components to render a template that escapes automatically.
38 | See the [Recipes](#recipes) section for details.
39 |
40 | ## Table of Contents
41 |
42 | * [Getting Started](#getting-started)
43 | * [Components as plain functions](#components-as-plain-functions)
44 | * [Components as Rack::Components](#components-as-rackcomponents)
45 | * [Components if you hate inheritance](#components-if-you-hate-inheritance)
46 | * [Recipes](#recipes)
47 | * [Render one component inside another](#render-one-component-inside-another)
48 | * [Render a template that escapes output by default via Tilt](#render-a-template-that-escapes-output-by-default-via-tilt)
49 | * [Render an HTML list from an array](#render-an-html-list-from-an-array)
50 | * [Render a Rack::Component from a Rails controller](#render-a-rackcomponent-from-a-rails-controller)
51 | * [Mount a Rack::Component as a Rack app](#mount-a-rackcomponent-as-a-rack-app)
52 | * [Build an entire App out of Rack::Components](#build-an-entire-app-out-of-rackcomponents)
53 | * [Define `#render` at the instance level instead of via `render do`](#define-render-at-the-instance-level-instead-of-via-render-do)
54 | * [API Reference](#api-reference)
55 | * [Performance](#performance)
56 | * [Compatibility](#compatibility)
57 | * [Anybody using this in production?](#anybody-using-this-in-production)
58 | * [Ruby reference](#ruby-reference)
59 | * [Development](#development)
60 | * [Contributing](#contributing)
61 | * [License](#license)
62 |
63 | ## Getting Started
64 |
65 | ### Components as plain functions
66 |
67 | The simplest component is just a lambda that takes an `env` parameter:
68 |
69 | ```ruby
70 | Greeter = lambda do |env|
71 | "
'
75 | ```
76 |
77 | ### Components as Rack::Components
78 |
79 | Upgrade your lambda to a `Rack::Component` when it needs HTML escaping, instance
80 | methods, or state:
81 |
82 | ```ruby
83 | require 'rack/component'
84 | class FormalGreeter < Rack::Component
85 | render do |env|
86 | "
Hi, #{h title} #{h env[:name]}.
"
87 | end
88 |
89 | # +env+ is available in instance methods too
90 | def title
91 | env[:title] || "Queen"
92 | end
93 | end
94 |
95 | FormalGreeter.call(name: 'Franklin') #=> "
100 | ```
101 |
102 | #### Components if you hate inheritance
103 |
104 | Instead of inheriting from `Rack::Component`, you can `extend` its methods:
105 |
106 | ```ruby
107 | class SoloComponent
108 | extend Rack::Component::Methods
109 | render { "Family is complicated" }
110 | end
111 | ```
112 |
113 | ## Recipes
114 |
115 | ### Render one component inside another
116 |
117 | You can nest Rack::Components as if they were [React Children][jsx children] by
118 | calling them with a block.
119 |
120 | ```ruby
121 | Layout.call(title: 'Home') do
122 | Content.call
123 | end
124 | ```
125 |
126 | Here's a more fully fleshed example:
127 |
128 | ```ruby
129 | require 'rack/component'
130 |
131 | # let's say this is a Sinatra app:
132 | get '/posts/:id' do
133 | PostPage.call(id: params[:id])
134 | end
135 |
136 | # Fetch a post from the database and render it inside a Layout
137 | class PostPage < Rack::Component
138 | render do |env|
139 | post = Post.find env[:id]
140 | # Nest a PostContent instance inside a Layout instance,
141 | # with some arbitrary HTML too
142 | Layout.call(title: post.title) do
143 | <<~HTML
144 |
145 | #{PostContent.call(title: post.title, body: post.body)}
146 |
149 |
150 | HTML
151 | end
152 | end
153 | end
154 |
155 | class Layout < Rack::Component
156 | # The +render+ macro supports Ruby's keyword arguments, and, like any other
157 | # Ruby function, can accept a block via the & operator.
158 | # Here, :title is a required key in +env+, and &child is just a regular Ruby
159 | # block that could be named anything.
160 | render do |title:, **, &child|
161 | <<~HTML
162 |
163 |
164 |
165 | #{h title}
166 |
167 |
168 | #{child.call}
169 |
170 |
171 | HTML
172 | end
173 | end
174 |
175 | class PostContent < Rack::Component
176 | render do |title:, body:, **|
177 | <<~HTML
178 |
179 |
#{h title}
180 | #{h body}
181 |
182 | HTML
183 | end
184 | end
185 | ```
186 |
187 | ### Render a template that escapes output by default via Tilt
188 |
189 | If you add [Tilt][tilt] and `erubi` to your Gemfile, you can use the `render`
190 | macro with an automatically-escaped template instead of a block.
191 |
192 | ```ruby
193 | # Gemfile
194 | gem 'tilt'
195 | gem 'erubi'
196 | gem 'rack-component'
197 |
198 | # my_component.rb
199 | class TemplateComponent < Rack::Component
200 | render erb: <<~ERB
201 |
Hello, <%= name %>
202 | ERB
203 |
204 | def name
205 | env[:name] || 'Someone'
206 | end
207 | end
208 |
209 | TemplateComponent.call #=>
Hello, Someone
210 | TemplateComponent.call(name: 'Spock<>') #=>
Hello, Spock<>
211 | ```
212 |
213 | Rack::Component passes `{ escape_html: true }` to Tilt by default, which enables
214 | automatic escaping in ERB (via erubi) Haml, and Markdown. To disable automatic
215 | escaping, or to pass other tilt options, use an `opts: {}` key in `render`:
216 |
217 | ```ruby
218 | class OptionsComponent < Rack::Component
219 | render opts: { escape_html: false, trim: false }, erb: <<~ERB
220 |
221 | Hi there, <%= {env[:name] %>
222 | <%== yield %>
223 |
224 | ERB
225 | end
226 | ```
227 |
228 | Template components support using the `yield` keyword to render child
229 | components, but note the double-equals `<%==` in the example above. If your
230 | component escapes HTML, and you're yielding to a component that renders HTML,
231 | you probably want to disable escaping via `==`, just for the `<%== yield %>`
232 | call. This is safe, as long as the component you're yielding to uses escaping.
233 |
234 | Using `erb` as a key for the inline template is a shorthand, which also works
235 | with `haml` and `markdown`. But you can also specify `engine` and `template`
236 | explicitly.
237 |
238 | ```ruby
239 | require 'haml'
240 | class HamlComponent < Rack::Component
241 | # Note the special HEREDOC syntax for inline Haml templates! Without the
242 | # single-quotes, Ruby will interpret #{strings} before Haml does.
243 | render engine: 'haml', template: <<~'HAML'
244 | %h1 Hi #{env[:name]}.
245 | HAML
246 | end
247 | ```
248 |
249 | Using a template instead of raw string interpolation is a safer default, but it
250 | can make it less convenient to do logic while rendering. Feel free to override
251 | your Component's `#initialize` method and do logic there:
252 |
253 | ```ruby
254 | class EscapedPostView < Rack::Component
255 | def initialize(env)
256 | @post = Post.find(env[:id])
257 | # calling `super` will populate the instance-level `env` hash, making
258 | # `env` available outside this method. But it's fine to skip it.
259 | super
260 | end
261 |
262 | render erb: <<~ERB
263 |
264 |
<%= @post.title %>
265 | <%= @post.body %>
266 |
267 | ERB
268 | end
269 | ```
270 |
271 | ### Render an HTML list from an array
272 |
273 | [JSX Lists][jsx lists] use JavaScript's `map` function. Rack::Component does
274 | likewise, only you need to call `join` on the array:
275 |
276 | ```ruby
277 | require 'rack/component'
278 | class PostsList < Rack::Component
279 | render do
280 | <<~HTML
281 |
")
10 | end
11 |
12 | it 'upgrades to a component for more complex logic' do
13 | require 'rack/component'
14 |
15 | class FormalGreeter < Rack::Component
16 | render erb: '
")
29 | end
30 |
31 | describe 'Recipes' do
32 | it 'Renders one component inside another' do
33 | Post = Struct.new(:title, :body) do
34 | def self.find(*); new('Hi', 'Hello'); end
35 | end
36 |
37 | # Fetch a post from the database and render it inside a Layout
38 | class PostPage < Rack::Component
39 | render do |env|
40 | post = Post.find env[:id]
41 | # Nest a PostContent instance inside a Layout instance,
42 | # with some arbitrary HTML too
43 | Layout.call(title: post.title) do
44 | <<~HTML
45 |
46 | #{PostContent.call(title: post.title, body: post.body)}
47 |
50 |
51 | HTML
52 | end
53 | end
54 | end
55 |
56 | class Layout < Rack::Component
57 | # Note that render blocks support Ruby's keyword arguments, and, like
58 | # any other ruby function, can accept a block.
59 | #
60 | # Here, :title is a required key in +env+, while &child
61 | # is just a regular Ruby block that could be named anything.
62 | render do |title:, **, &child|
63 | <<~HTML
64 |
65 |
66 |
67 |
68 |
69 |
70 | #{child.call}
71 |
72 |
73 | HTML
74 | end
75 | end
76 |
77 | class PostContent < Rack::Component
78 | render do |title:, body:, **|
79 | <<~HTML
80 |
81 |
#{h title}
82 | #{h body}
83 |
84 | HTML
85 | end
86 | end
87 |
88 | expect(PostPage.call(id: 1)).to include('
Hi
')
89 | end
90 |
91 | it 'renders a list of posts' do
92 | class PostsList < Rack::Component
93 | render do |posts:, **|
94 | <<~HTML
95 |