rack-component 0.4.2 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.rubocop.yml +0 -2
- data/.travis.yml +3 -1
- data/CHANGELOG.md +52 -0
- data/Gemfile.lock +22 -11
- data/README.md +194 -134
- data/lib/rack/component.rb +64 -109
- data/lib/rack/component/renderer.rb +31 -0
- data/lib/rack/component/version.rb +1 -1
- data/rack-component.gemspec +5 -2
- metadata +49 -5
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: f4f080e40de93c62a1a6b1aa9aabf3ee3f2a92fa10823f52c9dade5b075d440f
|
4
|
+
data.tar.gz: '08d3866748d7e14320a274f67e3682b67a51448154ecdae5034c2c01ec5a3e3f'
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 6e5c02e8c994ed4fa58e2a62004d248033d796c4f7163217c8a164c127a7ccf26c01a99c3a1fd0415aecff7af2ceae07eec800b2d04ab755da77bb3cd1dd4ab5
|
7
|
+
data.tar.gz: 96405e2d4e8b8f9add4d47f1129f035f00a5a9fbf00d6c363c8f056db88d43c962c1c43d367f88cf7557efccfb11386ae760a73b69cab20a795387efd0bab1f2
|
data/.rubocop.yml
CHANGED
data/.travis.yml
CHANGED
data/CHANGELOG.md
ADDED
@@ -0,0 +1,52 @@
|
|
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
|
data/Gemfile.lock
CHANGED
@@ -21,26 +21,33 @@ GEM
|
|
21
21
|
thread_safe (~> 0.3, >= 0.3.1)
|
22
22
|
diff-lcs (1.3)
|
23
23
|
equalizer (0.0.11)
|
24
|
+
erubi (1.8.0)
|
25
|
+
haml (5.0.4)
|
26
|
+
temple (>= 0.8.0)
|
27
|
+
tilt
|
24
28
|
ice_nine (0.11.2)
|
25
|
-
jaro_winkler (1.5.
|
29
|
+
jaro_winkler (1.5.2)
|
26
30
|
kwalify (0.7.2)
|
27
|
-
|
31
|
+
liquid (4.0.1)
|
32
|
+
method_source (0.9.2)
|
28
33
|
parallel (1.12.1)
|
29
|
-
parser (2.5.
|
34
|
+
parser (2.5.3.0)
|
30
35
|
ast (~> 2.4.0)
|
31
36
|
powerpack (0.1.2)
|
32
|
-
pry (0.
|
37
|
+
pry (0.12.2)
|
33
38
|
coderay (~> 1.1.0)
|
34
39
|
method_source (~> 0.9.0)
|
40
|
+
psych (3.1.0)
|
35
41
|
rack (2.0.6)
|
36
42
|
rack-test (0.8.3)
|
37
43
|
rack (>= 1.0, < 3)
|
38
44
|
rainbow (3.0.0)
|
39
45
|
rake (10.5.0)
|
40
|
-
reek (5.
|
46
|
+
reek (5.3.0)
|
41
47
|
codeclimate-engine-rb (~> 0.4.0)
|
42
48
|
kwalify (~> 0.7.0)
|
43
49
|
parser (>= 2.5.0.0, < 2.6, != 2.5.1.1)
|
50
|
+
psych (~> 3.1.0)
|
44
51
|
rainbow (>= 2.0, < 4.0)
|
45
52
|
rspec (3.8.0)
|
46
53
|
rspec-core (~> 3.8.0)
|
@@ -55,18 +62,19 @@ GEM
|
|
55
62
|
diff-lcs (>= 1.2.0, < 2.0)
|
56
63
|
rspec-support (~> 3.8.0)
|
57
64
|
rspec-support (3.8.0)
|
58
|
-
rubocop (0.
|
65
|
+
rubocop (0.62.0)
|
59
66
|
jaro_winkler (~> 1.5.1)
|
60
67
|
parallel (~> 1.10)
|
61
68
|
parser (>= 2.5, != 2.5.1.1)
|
62
69
|
powerpack (~> 0.1)
|
63
70
|
rainbow (>= 2.2.2, < 4.0)
|
64
71
|
ruby-progressbar (~> 1.7)
|
65
|
-
unicode-display_width (~> 1.
|
72
|
+
unicode-display_width (~> 1.4.0)
|
66
73
|
ruby-progressbar (1.10.0)
|
74
|
+
temple (0.8.0)
|
67
75
|
thread_safe (0.3.6)
|
68
|
-
tilt (2.0.
|
69
|
-
unicode-display_width (1.4.
|
76
|
+
tilt (2.0.9)
|
77
|
+
unicode-display_width (1.4.1)
|
70
78
|
virtus (1.0.5)
|
71
79
|
axiom-types (~> 0.1)
|
72
80
|
coercible (~> 1.0)
|
@@ -79,7 +87,10 @@ PLATFORMS
|
|
79
87
|
|
80
88
|
DEPENDENCIES
|
81
89
|
benchmark-ips (~> 2.7)
|
82
|
-
bundler (~>
|
90
|
+
bundler (~> 2)
|
91
|
+
erubi (~> 1.8)
|
92
|
+
haml (~> 5)
|
93
|
+
liquid (~> 4)
|
83
94
|
pry (~> 0.11)
|
84
95
|
rack (~> 2.0.6)
|
85
96
|
rack-component!
|
@@ -92,4 +103,4 @@ DEPENDENCIES
|
|
92
103
|
yard (~> 0.9)
|
93
104
|
|
94
105
|
BUNDLED WITH
|
95
|
-
|
106
|
+
2.0.1
|
data/README.md
CHANGED
@@ -13,6 +13,7 @@ gem 'rack-component'
|
|
13
13
|
```
|
14
14
|
|
15
15
|
## Quickstart with Sinatra
|
16
|
+
|
16
17
|
```ruby
|
17
18
|
# config.ru
|
18
19
|
require 'sinatra'
|
@@ -20,7 +21,7 @@ require 'rack/component'
|
|
20
21
|
|
21
22
|
class Hello < Rack::Component
|
22
23
|
render do |env|
|
23
|
-
"<h1>Hello, #{h
|
24
|
+
"<h1>Hello, #{h env[:name]}</h1>"
|
24
25
|
end
|
25
26
|
end
|
26
27
|
|
@@ -31,27 +32,25 @@ end
|
|
31
32
|
run Sinatra::Application
|
32
33
|
```
|
33
34
|
|
34
|
-
**Note that Rack::Component
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
to discuss how to enable escaping by default. If you have ideas or opinions, I'd
|
39
|
-
love to hear about them there.
|
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.
|
40
39
|
|
41
40
|
## Table of Contents
|
42
41
|
|
43
42
|
* [Getting Started](#getting-started)
|
44
43
|
* [Components as plain functions](#components-as-plain-functions)
|
45
44
|
* [Components as Rack::Components](#components-as-rackcomponents)
|
46
|
-
|
45
|
+
* [Components if you hate inheritance](#components-if-you-hate-inheritance)
|
47
46
|
* [Recipes](#recipes)
|
48
47
|
* [Render one component inside another](#render-one-component-inside-another)
|
49
|
-
* [
|
50
|
-
* [Memoize an expensive component until its content changes](#memoize-an-expensive-component-until-its-content-changes)
|
48
|
+
* [Render a template that escapes output by default via Tilt](#render-a-template-that-escapes-output-by-default-via-tilt)
|
51
49
|
* [Render an HTML list from an array](#render-an-html-list-from-an-array)
|
52
50
|
* [Render a Rack::Component from a Rails controller](#render-a-rackcomponent-from-a-rails-controller)
|
53
51
|
* [Mount a Rack::Component as a Rack app](#mount-a-rackcomponent-as-a-rack-app)
|
54
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)
|
55
54
|
* [API Reference](#api-reference)
|
56
55
|
* [Performance](#performance)
|
57
56
|
* [Compatibility](#compatibility)
|
@@ -77,57 +76,38 @@ Greeter.call(name: 'Mina') #=> '<h1>Hi, Mina.</h1>'
|
|
77
76
|
|
78
77
|
### Components as Rack::Components
|
79
78
|
|
80
|
-
|
81
|
-
state:
|
79
|
+
Upgrade your lambda to a `Rack::Component` when it needs HTML escaping, instance
|
80
|
+
methods, or state:
|
82
81
|
|
83
82
|
```ruby
|
84
83
|
require 'rack/component'
|
85
84
|
class FormalGreeter < Rack::Component
|
86
85
|
render do |env|
|
87
|
-
"<h1>Hi, #{title} #{env[:name]}.</h1>"
|
86
|
+
"<h1>Hi, #{h title} #{h env[:name]}.</h1>"
|
88
87
|
end
|
89
88
|
|
89
|
+
# +env+ is available in instance methods too
|
90
90
|
def title
|
91
|
-
|
92
|
-
env[:title] || "President"
|
91
|
+
env[:title] || "Queen"
|
93
92
|
end
|
94
93
|
end
|
95
94
|
|
96
|
-
FormalGreeter.call(name: '
|
97
|
-
FormalGreeter.call(
|
95
|
+
FormalGreeter.call(name: 'Franklin') #=> "<h1>Hi, Queen Franklin.</h1>"
|
96
|
+
FormalGreeter.call(
|
97
|
+
title: 'Captain',
|
98
|
+
name: 'Kirk <kirk@starfleet.gov>'
|
99
|
+
) #=> <h1>Hi, Captain Kirk <kirk@starfleet.gov>.</h1>
|
98
100
|
```
|
99
101
|
|
100
|
-
|
102
|
+
#### Components if you hate inheritance
|
101
103
|
|
102
|
-
|
104
|
+
Instead of inheriting from `Rack::Component`, you can `extend` its methods:
|
103
105
|
|
104
106
|
```ruby
|
105
|
-
|
106
|
-
|
107
|
-
|
108
|
-
render do |env|
|
109
|
-
"Hi, #{get_job_title_from_api} #{env[:name]}."
|
110
|
-
end
|
111
|
-
|
112
|
-
def get_job_title_from_api
|
113
|
-
endpoint = URI("http://api.heads-of-state.gov/")
|
114
|
-
Net::HTTP.get("#{endpoint}?q=#{env[:name]}")
|
115
|
-
end
|
107
|
+
class SoloComponent
|
108
|
+
extend Rack::Component::Methods
|
109
|
+
render { "Family is complicated" }
|
116
110
|
end
|
117
|
-
|
118
|
-
NetworkGreeter.memoized(name: 'Macron')
|
119
|
-
# ...after a slow network call to our fictional Heads Of State API
|
120
|
-
#=> "Hi, President Macron."
|
121
|
-
|
122
|
-
NetworkGreeter.memoized(name: 'Macron') # subsequent calls with the same env are instant.
|
123
|
-
#=> "Hi, President Macron."
|
124
|
-
|
125
|
-
NetworkGreeter.memoized(name: 'Merkel')
|
126
|
-
# ...this env is new, so NetworkGreeter makes another network call
|
127
|
-
#=> "Hi, Chancellor Merkel."
|
128
|
-
|
129
|
-
NetworkGreeter.memoized(name: 'Merkel') #=> instant! "Hi, Chancellor Merkel."
|
130
|
-
NetworkGreeter.memoized(name: 'Macron') #=> instant! "Hi, President Macron."
|
131
111
|
```
|
132
112
|
|
133
113
|
## Recipes
|
@@ -138,7 +118,9 @@ You can nest Rack::Components as if they were [React Children][jsx children] by
|
|
138
118
|
calling them with a block.
|
139
119
|
|
140
120
|
```ruby
|
141
|
-
Layout.call(title: 'Home')
|
121
|
+
Layout.call(title: 'Home') do
|
122
|
+
Content.call
|
123
|
+
end
|
142
124
|
```
|
143
125
|
|
144
126
|
Here's a more fully fleshed example:
|
@@ -151,11 +133,12 @@ get '/posts/:id' do
|
|
151
133
|
PostPage.call(id: params[:id])
|
152
134
|
end
|
153
135
|
|
154
|
-
#
|
136
|
+
# Fetch a post from the database and render it inside a Layout
|
155
137
|
class PostPage < Rack::Component
|
156
138
|
render do |env|
|
157
|
-
post = Post.find
|
158
|
-
# Nest a PostContent instance inside a Layout instance,
|
139
|
+
post = Post.find env[:id]
|
140
|
+
# Nest a PostContent instance inside a Layout instance,
|
141
|
+
# with some arbitrary HTML too
|
159
142
|
Layout.call(title: post.title) do
|
160
143
|
<<~HTML
|
161
144
|
<main>
|
@@ -169,80 +152,122 @@ class PostPage < Rack::Component
|
|
169
152
|
end
|
170
153
|
end
|
171
154
|
|
172
|
-
class PostContent < Rack::Component
|
173
|
-
render do |env|
|
174
|
-
<<~HTML
|
175
|
-
<article>
|
176
|
-
<h1>#{env[:title]}</h1>
|
177
|
-
#{env[:body]}
|
178
|
-
</article>
|
179
|
-
HTML
|
180
|
-
end
|
181
|
-
end
|
182
|
-
|
183
155
|
class Layout < Rack::Component
|
184
|
-
render
|
185
|
-
|
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|
|
186
161
|
<<~HTML
|
187
162
|
<!DOCTYPE html>
|
188
163
|
<html>
|
189
164
|
<head>
|
190
|
-
<title>#{
|
165
|
+
<title>#{h title}</title>
|
191
166
|
</head>
|
192
167
|
<body>
|
193
|
-
|
168
|
+
#{child.call}
|
194
169
|
</body>
|
195
170
|
</html>
|
196
171
|
HTML
|
197
172
|
end
|
198
173
|
end
|
174
|
+
|
175
|
+
class PostContent < Rack::Component
|
176
|
+
render do |title:, body:, **|
|
177
|
+
<<~HTML
|
178
|
+
<article>
|
179
|
+
<h1>#{h title}</h1>
|
180
|
+
#{h body}
|
181
|
+
</article>
|
182
|
+
HTML
|
183
|
+
end
|
184
|
+
end
|
199
185
|
```
|
200
186
|
|
201
|
-
###
|
187
|
+
### Render a template that escapes output by default via Tilt
|
202
188
|
|
203
|
-
|
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.
|
204
191
|
|
205
192
|
```ruby
|
206
|
-
|
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
|
+
<h1>Hello, <%= name %></h1>
|
202
|
+
ERB
|
207
203
|
|
208
|
-
|
209
|
-
|
210
|
-
|
204
|
+
def name
|
205
|
+
env[:name] || 'Someone'
|
206
|
+
end
|
211
207
|
end
|
212
208
|
|
213
|
-
|
214
|
-
|
209
|
+
TemplateComponent.call #=> <h1>Hello, Someone</h1>
|
210
|
+
TemplateComponent.call(name: 'Spock<>') #=> <h1>Hello, Spock<></h1>
|
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
|
+
<article>
|
221
|
+
Hi there, <%= {env[:name] %>
|
222
|
+
<%== yield %>
|
223
|
+
</article>
|
224
|
+
ERB
|
225
|
+
end
|
215
226
|
```
|
216
227
|
|
217
|
-
|
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.
|
218
233
|
|
219
|
-
|
220
|
-
|
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.
|
221
237
|
|
222
238
|
```ruby
|
223
|
-
require '
|
224
|
-
class
|
225
|
-
|
226
|
-
|
227
|
-
|
228
|
-
|
229
|
-
|
230
|
-
|
231
|
-
|
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
|
+
```
|
232
248
|
|
233
|
-
|
234
|
-
|
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
|
235
260
|
end
|
236
|
-
end
|
237
261
|
|
238
|
-
|
239
|
-
|
262
|
+
render erb: <<~ERB
|
263
|
+
<article>
|
264
|
+
<h1><%= @post.title %></h1>
|
265
|
+
<%= @post.body %>
|
266
|
+
</article>
|
267
|
+
ERB
|
268
|
+
end
|
240
269
|
```
|
241
270
|
|
242
|
-
This recipe works with any Ruby object that implements a `#hash` method based
|
243
|
-
on the object's content, including instances of `ActiveRecord::Base` and
|
244
|
-
`Sequel::Model`.
|
245
|
-
|
246
271
|
### Render an HTML list from an array
|
247
272
|
|
248
273
|
[JSX Lists][jsx lists] use JavaScript's `map` function. Rack::Component does
|
@@ -251,7 +276,7 @@ likewise, only you need to call `join` on the array:
|
|
251
276
|
```ruby
|
252
277
|
require 'rack/component'
|
253
278
|
class PostsList < Rack::Component
|
254
|
-
render do
|
279
|
+
render do
|
255
280
|
<<~HTML
|
256
281
|
<h1>This is a list of posts</h1>
|
257
282
|
<ul>
|
@@ -264,12 +289,12 @@ class PostsList < Rack::Component
|
|
264
289
|
env[:posts].map { |post|
|
265
290
|
<<~HTML
|
266
291
|
<li class="item">
|
267
|
-
<a href="
|
292
|
+
<a href="/posts/#{post[:id]}">
|
268
293
|
#{post[:name]}
|
269
294
|
</a>
|
270
295
|
</li>
|
271
296
|
HTML
|
272
|
-
}.join #unlike JSX, you need to call `join` on your array
|
297
|
+
}.join # unlike JSX, you need to call `join` on your array
|
273
298
|
end
|
274
299
|
end
|
275
300
|
|
@@ -289,26 +314,24 @@ end
|
|
289
314
|
|
290
315
|
# app/components/posts_list.rb
|
291
316
|
class PostsList < Rack::Component
|
292
|
-
render
|
293
|
-
|
294
|
-
def posts
|
295
|
-
Post.magically_filter_via_params(env)
|
317
|
+
def render
|
318
|
+
Post.magically_filter_via_params(env).to_json
|
296
319
|
end
|
297
320
|
end
|
298
321
|
```
|
299
322
|
|
300
323
|
### Mount a Rack::Component as a Rack app
|
301
324
|
|
302
|
-
Because Rack::Components
|
303
|
-
anywhere you can mount a Rack app. It's up to you to return a valid rack
|
304
|
-
|
325
|
+
Because Rack::Components have the same signature as Rack app, you can mount them
|
326
|
+
anywhere you can mount a Rack app. It's up to you to return a valid rack tuple,
|
327
|
+
though.
|
305
328
|
|
306
329
|
```ruby
|
307
330
|
# config.ru
|
308
331
|
require 'rack/component'
|
309
332
|
|
310
333
|
class Posts < Rack::Component
|
311
|
-
render
|
334
|
+
def render
|
312
335
|
[status, headers, [body]]
|
313
336
|
end
|
314
337
|
|
@@ -335,66 +358,102 @@ Rack::Component instead of Controllers, Views, and templates. But to see an
|
|
335
358
|
entire app built only out of Rack::Components, see
|
336
359
|
[the example spec](https://github.com/chrisfrank/rack-component/blob/master/spec/raw_rack_example_spec.rb).
|
337
360
|
|
361
|
+
### Define `#render` at the instance level instead of via `render do`
|
362
|
+
|
363
|
+
The class-level `render` macro exists to make using templates easy, and to lean
|
364
|
+
on Ruby's keyword arguments as a limited imitation of React's `defaultProps` and
|
365
|
+
`PropTypes`. But you can define render at the instance level instead.
|
366
|
+
|
367
|
+
```ruby
|
368
|
+
# these two components render identical output
|
369
|
+
|
370
|
+
class MacroComponent < Rack::Component
|
371
|
+
render do |name:, dept: 'Engineering'|
|
372
|
+
"#{name} - #{dept}"
|
373
|
+
end
|
374
|
+
end
|
375
|
+
|
376
|
+
class ExplicitComponent < Rack::Component
|
377
|
+
def initialize(name:, dept: 'Engineering')
|
378
|
+
@name = name
|
379
|
+
@dept = dept
|
380
|
+
# calling `super` will populate the instance-level `env` hash, making
|
381
|
+
# `env` available outside this method. But it's fine to skip it.
|
382
|
+
super
|
383
|
+
end
|
384
|
+
|
385
|
+
def render
|
386
|
+
"#{@name} - #{@dept}"
|
387
|
+
end
|
388
|
+
end
|
389
|
+
```
|
390
|
+
|
338
391
|
## API Reference
|
339
392
|
|
340
393
|
The full API reference is available here:
|
341
394
|
|
342
395
|
https://www.rubydoc.info/gems/rack-component
|
343
396
|
|
344
|
-
For info on how to clear or change the size of the memoziation cache, please see
|
345
|
-
[the spec][spec].
|
346
|
-
|
347
397
|
## Performance
|
348
398
|
|
349
|
-
|
350
|
-
|
351
|
-
library. Run `ruby spec/benchmarks.rb` to see what to expect in your env.
|
399
|
+
Run `ruby spec/benchmarks.rb` to see what to expect in your environment. These
|
400
|
+
results are from a 2015 iMac:
|
352
401
|
|
353
402
|
```
|
354
403
|
$ ruby spec/benchmarks.rb
|
355
404
|
Warming up --------------------------------------
|
356
|
-
|
357
|
-
|
358
|
-
|
359
|
-
|
360
|
-
|
405
|
+
stdlib ERB 2.682k i/100ms
|
406
|
+
Tilt ERB 15.958k i/100ms
|
407
|
+
Bare lambda 77.124k i/100ms
|
408
|
+
RC [def render] 64.905k i/100ms
|
409
|
+
RC [render do] 57.725k i/100ms
|
410
|
+
RC [render erb:] 15.595k i/100ms
|
361
411
|
Calculating -------------------------------------
|
362
|
-
|
363
|
-
|
364
|
-
|
365
|
-
|
366
|
-
|
412
|
+
stdlib ERB 27.423k (± 1.8%) i/s - 139.464k in 5.087391s
|
413
|
+
Tilt ERB 169.351k (± 2.2%) i/s - 861.732k in 5.090920s
|
414
|
+
Bare lambda 929.473k (± 3.0%) i/s - 4.705M in 5.065991s
|
415
|
+
RC [def render] 775.176k (± 1.1%) i/s - 3.894M in 5.024347s
|
416
|
+
RC [render do] 686.653k (± 2.3%) i/s - 3.464M in 5.046728s
|
417
|
+
RC [render erb:] 165.113k (± 1.7%) i/s - 826.535k in 5.007444s
|
367
418
|
```
|
368
419
|
|
369
|
-
|
370
|
-
|
371
|
-
|
372
|
-
|
373
|
-
probably fastest not to memoize. For components that do I/O, using `#memoize`
|
374
|
-
can speed things up by several orders of magnitude.
|
420
|
+
Every component in the benchmark is configured to escape HTML when rendering.
|
421
|
+
When rendering via a block, Rack::Component is about 25x faster than ERB and 4x
|
422
|
+
faster than Tilt. When rendering a template via Tilt, it (unsurprisingly)
|
423
|
+
performs roughly at tilt-speed.
|
375
424
|
|
376
425
|
## Compatibility
|
377
426
|
|
378
|
-
Rack::Component has zero dependencies,
|
379
|
-
|
380
|
-
|
381
|
-
specification, and because that's where I
|
427
|
+
When not rendering Tilt templates, Rack::Component has zero dependencies,
|
428
|
+
and will work in any Rack app. It should even work _outside_ a Rack app, because
|
429
|
+
it's not actually dependent on Rack. I packaged it under the Rack namespace
|
430
|
+
because it follows the Rack `call` specification, and because that's where I
|
431
|
+
use and test it.
|
432
|
+
|
433
|
+
When using Tilt templates, you will need `tilt` and a templating gem in your
|
434
|
+
`Gemfile`:
|
435
|
+
|
436
|
+
```ruby
|
437
|
+
gem 'tilt'
|
438
|
+
gem 'erubi' # or gem 'haml', etc
|
439
|
+
gem 'rack-component'
|
440
|
+
```
|
382
441
|
|
383
442
|
## Anybody using this in production?
|
384
443
|
|
385
444
|
Aye:
|
386
445
|
|
387
|
-
|
388
|
-
|
446
|
+
* [future.com](https://www.future.com/)
|
447
|
+
* [Seattle & King County Homelessness Response System](https://hrs.kc.future.com/)
|
389
448
|
|
390
449
|
## Ruby reference
|
391
450
|
|
392
451
|
Where React uses [JSX] to make components more ergonomic, Rack::Component leans
|
393
452
|
heavily on some features built into the Ruby language, specifically:
|
394
453
|
|
395
|
-
|
396
|
-
|
397
|
-
|
454
|
+
* [Heredocs]
|
455
|
+
* [String Interpolation]
|
456
|
+
* [Calling methods with a block][ruby blocks]
|
398
457
|
|
399
458
|
## Development
|
400
459
|
|
@@ -426,3 +485,4 @@ MIT
|
|
426
485
|
[ruby blocks]: https://mixandgo.com/learn/mastering-ruby-blocks-in-less-than-5-minutes
|
427
486
|
[roda]: http://roda.jeremyevans.net
|
428
487
|
[sinatra]: http://sinatrarb.com
|
488
|
+
[tilt]: https://github.com/rtomayko/tilt
|
data/lib/rack/component.rb
CHANGED
@@ -1,131 +1,86 @@
|
|
1
1
|
require_relative 'component/version'
|
2
|
-
require_relative 'component/
|
2
|
+
require_relative 'component/renderer'
|
3
3
|
require 'cgi'
|
4
4
|
|
5
5
|
module Rack
|
6
6
|
# Subclass Rack::Component to compose functional, declarative responses to
|
7
7
|
# HTTP requests.
|
8
|
+
# @example Subclass Rack::Component to compose functional, declarative
|
9
|
+
# responses to HTTP requests.
|
10
|
+
# class Greeter < Rack::Component
|
11
|
+
# render { "Hi, #{env[:name]" }
|
12
|
+
# end
|
8
13
|
class Component
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
# <title>#{env[:title]}</title>
|
19
|
-
# </head>
|
20
|
-
# <body>#{child.call}</body>
|
21
|
-
# </html>
|
22
|
-
# HTML
|
23
|
-
# end
|
24
|
-
# end
|
25
|
-
#
|
26
|
-
# Layout.call(title: 'Hello') { "<h1>Hello from Rack::Component" } #=>
|
27
|
-
# # <!DOCTYPE html>
|
28
|
-
# # <html>
|
29
|
-
# # <head>
|
30
|
-
# # <title>Hello</title>
|
31
|
-
# # </head>
|
32
|
-
# # <body><h1>Hello from Rack::Component</h1></body>
|
33
|
-
# # </html>
|
34
|
-
def call(env = {}, &child)
|
35
|
-
new(env).call env, &child
|
14
|
+
# @example If you don't want to subclass, you can extend
|
15
|
+
# Rack::Component::Methods instead.
|
16
|
+
# class POROGreeter
|
17
|
+
# extend Rack::Component::Methods
|
18
|
+
# render { "Hi, #{env[:name]" }
|
19
|
+
# end
|
20
|
+
module Methods
|
21
|
+
def self.extended(base)
|
22
|
+
base.include(InstanceMethods)
|
36
23
|
end
|
37
24
|
|
38
|
-
|
39
|
-
|
40
|
-
# will be read from a threadsafe in-memory cache, not computed.
|
41
|
-
# @example Cache a slow network call
|
42
|
-
# class Fetcher < Rack::Component
|
43
|
-
# render do |env|
|
44
|
-
# Net::HTTP.get(env[:uri]).to_json
|
45
|
-
# end
|
46
|
-
# end
|
47
|
-
#
|
48
|
-
# Fetcher.memoized(uri: '/slow/api.json')
|
49
|
-
# # ...
|
50
|
-
# # many seconds later...
|
51
|
-
# # => { some: "data" }
|
52
|
-
#
|
53
|
-
# Fetcher.memoized(uri: '/slow/api.json') #=> instant! { some: "data" }
|
54
|
-
# Fetcher.memoized(uri: '/other/source.json') #=> slow again!
|
55
|
-
def memoized(env = {}, &child)
|
56
|
-
cache.fetch(env.hash) { call(env, &child) }
|
25
|
+
def render(opts = {})
|
26
|
+
block_given? ? configure_block(Proc.new) : configure_template(opts)
|
57
27
|
end
|
58
28
|
|
59
|
-
|
60
|
-
|
61
|
-
cache.flush
|
29
|
+
def call(env = {}, &children)
|
30
|
+
new(env).render(&children)
|
62
31
|
end
|
63
32
|
|
64
|
-
#
|
65
|
-
#
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
33
|
+
# Instances of Rack::Component come with these methods.
|
34
|
+
# :reek:ModuleInitialize
|
35
|
+
module InstanceMethods
|
36
|
+
# +env+ is Rack::Component's version of React's +props+ hash.
|
37
|
+
def initialize(env)
|
38
|
+
@env = env
|
39
|
+
end
|
40
|
+
|
41
|
+
# +env+ can be an empty hash, but cannot be nil
|
42
|
+
# @return [Hash]
|
43
|
+
def env
|
44
|
+
@env || {}
|
45
|
+
end
|
77
46
|
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
# cache { MemoryCache.new(length: 2000) }
|
84
|
-
# end
|
85
|
-
def cache
|
86
|
-
@cache ||= (block_given? ? yield : MemoryCache.new(length: 100))
|
47
|
+
# +h+ removes HTML characters from strings via +CGI.escapeHTML+.
|
48
|
+
# @return [String]
|
49
|
+
def h(obj)
|
50
|
+
CGI.escapeHTML(obj.to_s)
|
51
|
+
end
|
87
52
|
end
|
88
|
-
end
|
89
53
|
|
90
|
-
|
91
|
-
@env = env
|
92
|
-
end
|
54
|
+
private
|
93
55
|
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
# Useless = Class.new(Rack::Component)
|
102
|
-
# Useless.call(number: 1) #=> { number: 1 }
|
103
|
-
# Useless.call(number: 2) #=> { number: 2 }
|
104
|
-
# Useless.call(number: 2) { |env| "the number was #{env[:number]" }
|
105
|
-
# #=> 'the number was 2'
|
106
|
-
#
|
107
|
-
# @example a useful component
|
108
|
-
# class Greeter < Rack::Component
|
109
|
-
# render do |env|
|
110
|
-
# "Hi, #{env[:name]}"
|
111
|
-
# end
|
112
|
-
# end
|
113
|
-
#
|
114
|
-
# Greeter.call(name: 'Jim') #=> 'Hi, Jim'
|
115
|
-
# Greeter.call(name: 'Bones') #=> 'Hi, Bones'
|
116
|
-
def call(*)
|
117
|
-
block_given? ? yield(env) : env
|
118
|
-
end
|
56
|
+
# :reek:TooManyStatements
|
57
|
+
# :reek:DuplicateMethodCall
|
58
|
+
def configure_block(block)
|
59
|
+
# Convert the block to an instance method, because instance_exec
|
60
|
+
# doesn't allow passing an &child param, and because it's faster.
|
61
|
+
define_method :_rc_render, &block
|
62
|
+
private :_rc_render
|
119
63
|
|
120
|
-
|
64
|
+
# Now that the block is a method, it must be called with the correct
|
65
|
+
# number of arguments. Ruby's +arity+ method is unreliable when keyword
|
66
|
+
# args are involved, so we count arity by hand.
|
67
|
+
arity = block.parameters.reject { |type, _| type == :block }.length
|
121
68
|
|
122
|
-
|
123
|
-
|
124
|
-
|
125
|
-
|
126
|
-
|
127
|
-
|
128
|
-
|
69
|
+
# Reek hates this DuplicateMethodCall, but fixing it would mean checking
|
70
|
+
# arity at runtime, rather than when the render macro is called.
|
71
|
+
if arity.zero?
|
72
|
+
define_method(:render) { |&child| _rc_render(&child) }
|
73
|
+
else
|
74
|
+
define_method(:render) { |&child| _rc_render(env, &child) }
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
def configure_template(options)
|
79
|
+
renderer = Renderer.new(options)
|
80
|
+
define_method(:render) { |&child| renderer.call(self, &child) }
|
81
|
+
end
|
129
82
|
end
|
83
|
+
|
84
|
+
extend Methods
|
130
85
|
end
|
131
86
|
end
|
@@ -0,0 +1,31 @@
|
|
1
|
+
module Rack
|
2
|
+
class Component
|
3
|
+
# Compile a Tilt template, which a component will render
|
4
|
+
class Renderer
|
5
|
+
DEFAULT_TILT_OPTIONS = { escape_html: true }.freeze
|
6
|
+
FORMATS = %i[erb rhtml erubis haml liquid markdown md mkd].freeze
|
7
|
+
|
8
|
+
def initialize(options = {})
|
9
|
+
require 'tilt'
|
10
|
+
engine, template, @config = OptionParser.call(options)
|
11
|
+
require 'erubi' if engine == 'erb' && @config[:escape_html]
|
12
|
+
@template = Tilt[engine].new(@config) { template }
|
13
|
+
end
|
14
|
+
|
15
|
+
def call(scope, &child)
|
16
|
+
@template.render(scope, &child)
|
17
|
+
end
|
18
|
+
|
19
|
+
OptionParser = lambda do |opts|
|
20
|
+
tilt_options = DEFAULT_TILT_OPTIONS.merge(opts.delete(:opts) || {})
|
21
|
+
engine, template =
|
22
|
+
opts.find { |key, _| FORMATS.include?(key) } ||
|
23
|
+
[opts[:engine], opts[:template]]
|
24
|
+
|
25
|
+
[engine.to_s, template, tilt_options]
|
26
|
+
end
|
27
|
+
|
28
|
+
private_constant :OptionParser
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
data/rack-component.gemspec
CHANGED
@@ -30,10 +30,13 @@ Gem::Specification.new do |spec|
|
|
30
30
|
spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
|
31
31
|
spec.require_paths = ['lib']
|
32
32
|
|
33
|
-
spec.required_ruby_version = '>= 2.
|
33
|
+
spec.required_ruby_version = '>= 2.3'
|
34
34
|
|
35
35
|
spec.add_development_dependency 'benchmark-ips', '~> 2.7'
|
36
|
-
spec.add_development_dependency 'bundler', '~>
|
36
|
+
spec.add_development_dependency 'bundler', '~> 2'
|
37
|
+
spec.add_development_dependency 'erubi', '~> 1.8'
|
38
|
+
spec.add_development_dependency 'haml', '~> 5'
|
39
|
+
spec.add_development_dependency 'liquid', '~> 4'
|
37
40
|
spec.add_development_dependency 'pry', '~> 0.11'
|
38
41
|
spec.add_development_dependency 'rack', '~> 2.0.6'
|
39
42
|
spec.add_development_dependency 'rack-test', '~> 0'
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: rack-component
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.5.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Chris Frank
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2019-01-
|
11
|
+
date: 2019-01-14 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: benchmark-ips
|
@@ -30,14 +30,56 @@ dependencies:
|
|
30
30
|
requirements:
|
31
31
|
- - "~>"
|
32
32
|
- !ruby/object:Gem::Version
|
33
|
-
version: '
|
33
|
+
version: '2'
|
34
|
+
type: :development
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - "~>"
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '2'
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: erubi
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - "~>"
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '1.8'
|
48
|
+
type: :development
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - "~>"
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '1.8'
|
55
|
+
- !ruby/object:Gem::Dependency
|
56
|
+
name: haml
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
58
|
+
requirements:
|
59
|
+
- - "~>"
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: '5'
|
62
|
+
type: :development
|
63
|
+
prerelease: false
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
65
|
+
requirements:
|
66
|
+
- - "~>"
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: '5'
|
69
|
+
- !ruby/object:Gem::Dependency
|
70
|
+
name: liquid
|
71
|
+
requirement: !ruby/object:Gem::Requirement
|
72
|
+
requirements:
|
73
|
+
- - "~>"
|
74
|
+
- !ruby/object:Gem::Version
|
75
|
+
version: '4'
|
34
76
|
type: :development
|
35
77
|
prerelease: false
|
36
78
|
version_requirements: !ruby/object:Gem::Requirement
|
37
79
|
requirements:
|
38
80
|
- - "~>"
|
39
81
|
- !ruby/object:Gem::Version
|
40
|
-
version: '
|
82
|
+
version: '4'
|
41
83
|
- !ruby/object:Gem::Dependency
|
42
84
|
name: pry
|
43
85
|
requirement: !ruby/object:Gem::Requirement
|
@@ -176,6 +218,7 @@ files:
|
|
176
218
|
- ".rspec"
|
177
219
|
- ".rubocop.yml"
|
178
220
|
- ".travis.yml"
|
221
|
+
- CHANGELOG.md
|
179
222
|
- Gemfile
|
180
223
|
- Gemfile.lock
|
181
224
|
- README.md
|
@@ -184,6 +227,7 @@ files:
|
|
184
227
|
- bin/setup
|
185
228
|
- lib/rack/component.rb
|
186
229
|
- lib/rack/component/memory_cache.rb
|
230
|
+
- lib/rack/component/renderer.rb
|
187
231
|
- lib/rack/component/version.rb
|
188
232
|
- rack-component.gemspec
|
189
233
|
homepage: https://www.github.com/chrisfrank/rack-component
|
@@ -199,7 +243,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
199
243
|
requirements:
|
200
244
|
- - ">="
|
201
245
|
- !ruby/object:Gem::Version
|
202
|
-
version: '2.
|
246
|
+
version: '2.3'
|
203
247
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
204
248
|
requirements:
|
205
249
|
- - ">="
|