rack-component 0.4.2 → 0.5.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|
- - ">="
|