README.md in rack-component-0.4.2 vs README.md in rack-component-0.5.0
- old
+ new
@@ -11,49 +11,48 @@
```
gem 'rack-component'
```
## Quickstart with Sinatra
+
```ruby
# config.ru
require 'sinatra'
require 'rack/component'
class Hello < Rack::Component
render do |env|
- "<h1>Hello, #{h(env[:name])}</h1>"
+ "<h1>Hello, #{h env[:name]}</h1>"
end
end
get '/hello/:name' do
Hello.call(name: params[:name])
end
run Sinatra::Application
```
-**Note that Rack::Component currently does not escape strings by default**. To
-escape strings, you must use the `#h` helper, as in the example above.
+**Note that Rack::Component does not escape strings by default**. To escape
+strings, you can either use the `#h` helper like in the example above, or you
+can configure your components to render a template that escapes automatically.
+See the [Recipes](#recipes) section for details.
-There is an [issue open](https://github.com/chrisfrank/rack-component/issues/4)
-to discuss how to enable escaping by default. If you have ideas or opinions, I'd
-love to hear about them there.
-
## Table of Contents
* [Getting Started](#getting-started)
* [Components as plain functions](#components-as-plain-functions)
* [Components as Rack::Components](#components-as-rackcomponents)
- * [Components that re-render instantly](#components-that-re-render-instantly)
+ * [Components if you hate inheritance](#components-if-you-hate-inheritance)
* [Recipes](#recipes)
* [Render one component inside another](#render-one-component-inside-another)
- * [Memoize an expensive component for one minute](#memoize-an-expensive-component-for-one-minute)
- * [Memoize an expensive component until its content changes](#memoize-an-expensive-component-until-its-content-changes)
+ * [Render a template that escapes output by default via Tilt](#render-a-template-that-escapes-output-by-default-via-tilt)
* [Render an HTML list from an array](#render-an-html-list-from-an-array)
* [Render a Rack::Component from a Rails controller](#render-a-rackcomponent-from-a-rails-controller)
* [Mount a Rack::Component as a Rack app](#mount-a-rackcomponent-as-a-rack-app)
* [Build an entire App out of Rack::Components](#build-an-entire-app-out-of-rackcomponents)
+ * [Define `#render` at the instance level instead of via `render do`](#define-render-at-the-instance-level-instead-of-via-render-do)
* [API Reference](#api-reference)
* [Performance](#performance)
* [Compatibility](#compatibility)
* [Anybody using this in production?](#anybody-using-this-in-production)
* [Ruby reference](#ruby-reference)
@@ -75,72 +74,55 @@
Greeter.call(name: 'Mina') #=> '<h1>Hi, Mina.</h1>'
```
### Components as Rack::Components
-Convert your lambda to a `Rack::Component` when it needs instance methods or
-state:
+Upgrade your lambda to a `Rack::Component` when it needs HTML escaping, instance
+methods, or state:
```ruby
require 'rack/component'
class FormalGreeter < Rack::Component
render do |env|
- "<h1>Hi, #{title} #{env[:name]}.</h1>"
+ "<h1>Hi, #{h title} #{h env[:name]}.</h1>"
end
+ # +env+ is available in instance methods too
def title
- # the hash you pass to `call` is available as `env` in instance methods
- env[:title] || "President"
+ env[:title] || "Queen"
end
end
-FormalGreeter.call(name: 'Macron') #=> "<h1>Hi, President Macron.</h1>"
-FormalGreeter.call(name: 'Merkel', title: 'Chancellor') #=> "<h1>Hi, Chancellor Merkel.</h1>"
+FormalGreeter.call(name: 'Franklin') #=> "<h1>Hi, Queen Franklin.</h1>"
+FormalGreeter.call(
+ title: 'Captain',
+ name: 'Kirk <kirk@starfleet.gov>'
+) #=> <h1>Hi, Captain Kirk <kirk@starfleet.gov>.</h1>
```
-### Components that re-render instantly
+#### Components if you hate inheritance
-Replace `#call` with `#memoized` to make re-renders with the same `env` instant:
+Instead of inheriting from `Rack::Component`, you can `extend` its methods:
```ruby
-require 'rack/component'
-require 'net/http'
-class NetworkGreeter < Rack::Component
- render do |env|
- "Hi, #{get_job_title_from_api} #{env[:name]}."
- end
-
- def get_job_title_from_api
- endpoint = URI("http://api.heads-of-state.gov/")
- Net::HTTP.get("#{endpoint}?q=#{env[:name]}")
- end
+class SoloComponent
+ extend Rack::Component::Methods
+ render { "Family is complicated" }
end
-
-NetworkGreeter.memoized(name: 'Macron')
-# ...after a slow network call to our fictional Heads Of State API
-#=> "Hi, President Macron."
-
-NetworkGreeter.memoized(name: 'Macron') # subsequent calls with the same env are instant.
-#=> "Hi, President Macron."
-
-NetworkGreeter.memoized(name: 'Merkel')
-# ...this env is new, so NetworkGreeter makes another network call
-#=> "Hi, Chancellor Merkel."
-
-NetworkGreeter.memoized(name: 'Merkel') #=> instant! "Hi, Chancellor Merkel."
-NetworkGreeter.memoized(name: 'Macron') #=> instant! "Hi, President Macron."
```
## Recipes
### Render one component inside another
You can nest Rack::Components as if they were [React Children][jsx children] by
calling them with a block.
```ruby
-Layout.call(title: 'Home') { Content.call }
+Layout.call(title: 'Home') do
+ Content.call
+end
```
Here's a more fully fleshed example:
```ruby
@@ -149,15 +131,16 @@
# let's say this is a Sinatra app:
get '/posts/:id' do
PostPage.call(id: params[:id])
end
-# fetch a post from the database and render it inside a layout
+# Fetch a post from the database and render it inside a Layout
class PostPage < Rack::Component
render do |env|
- post = Post.find(id: env[:id])
- # Nest a PostContent instance inside a Layout instance, with some arbitrary HTML too
+ post = Post.find env[:id]
+ # Nest a PostContent instance inside a Layout instance,
+ # with some arbitrary HTML too
Layout.call(title: post.title) do
<<~HTML
<main>
#{PostContent.call(title: post.title, body: post.body)}
<footer>
@@ -167,93 +150,135 @@
HTML
end
end
end
-class PostContent < Rack::Component
- render do |env|
- <<~HTML
- <article>
- <h1>#{env[:title]}</h1>
- #{env[:body]}
- </article>
- HTML
- end
-end
-
class Layout < Rack::Component
- render do |env, &children|
- # the `&children` param is just a standard ruby block
+ # The +render+ macro supports Ruby's keyword arguments, and, like any other
+ # Ruby function, can accept a block via the & operator.
+ # Here, :title is a required key in +env+, and &child is just a regular Ruby
+ # block that could be named anything.
+ render do |title:, **, &child|
<<~HTML
<!DOCTYPE html>
<html>
<head>
- <title>#{env[:title]}</title>
+ <title>#{h title}</title>
</head>
<body>
- #{children.call}
+ #{child.call}
</body>
</html>
HTML
end
end
+
+class PostContent < Rack::Component
+ render do |title:, body:, **|
+ <<~HTML
+ <article>
+ <h1>#{h title}</h1>
+ #{h body}
+ </article>
+ HTML
+ end
+end
```
-### Memoize an expensive component for one minute
+### Render a template that escapes output by default via Tilt
-You can use `memoized` as a time-based cache by passing a timestamp to `env`:
+If you add [Tilt][tilt] and `erubi` to your Gemfile, you can use the `render`
+macro with an automatically-escaped template instead of a block.
```ruby
-require 'rack/component'
+# Gemfile
+gem 'tilt'
+gem 'erubi'
+gem 'rack-component'
-# Render one million posts as JSON
-class MillionPosts < Rack::Component
- render { |env| Post.limit(1_000_000).to_json }
+# my_component.rb
+class TemplateComponent < Rack::Component
+ render erb: <<~ERB
+ <h1>Hello, <%= name %></h1>
+ ERB
+
+ def name
+ env[:name] || 'Someone'
+ end
end
-MillionPosts.memoized(Time.now.to_i / 60) #=> first call is slow
-MillionPosts.memoized(Time.now.to_i / 60) #=> next calls in same minute are quick
+TemplateComponent.call #=> <h1>Hello, Someone</h1>
+TemplateComponent.call(name: 'Spock<>') #=> <h1>Hello, Spock<></h1>
```
-### Memoize an expensive component until its content changes
+Rack::Component passes `{ escape_html: true }` to Tilt by default, which enables
+automatic escaping in ERB (via erubi) Haml, and Markdown. To disable automatic
+escaping, or to pass other tilt options, use an `opts: {}` key in `render`:
-This recipe will speed things up when your database calls are fast but your
-render method is slow:
+```ruby
+class OptionsComponent < Rack::Component
+ render opts: { escape_html: false, trim: false }, erb: <<~ERB
+ <article>
+ Hi there, <%= {env[:name] %>
+ <%== yield %>
+ </article>
+ ERB
+end
+```
+Template components support using the `yield` keyword to render child
+components, but note the double-equals `<%==` in the example above. If your
+component escapes HTML, and you're yielding to a component that renders HTML,
+you probably want to disable escaping via `==`, just for the `<%== yield %>`
+call. This is safe, as long as the component you're yielding to uses escaping.
+
+Using `erb` as a key for the inline template is a shorthand, which also works
+with `haml` and `markdown`. But you can also specify `engine` and `template`
+explicitly.
+
```ruby
-require 'rack/component'
-class PostAnalysis < Rack::Component
- render do |env|
- <<~HTML
- <h1>#{env[:post].title}</h1>
- <article>#{env[:post].content}</article>
- <aside>#{expensive_natural_language_analysis}</aside>
- HTML
- end
+require 'haml'
+class HamlComponent < Rack::Component
+ # Note the special HEREDOC syntax for inline Haml templates! Without the
+ # single-quotes, Ruby will interpret #{strings} before Haml does.
+ render engine: 'haml', template: <<~'HAML'
+ %h1 Hi #{env[:name]}.
+ HAML
+end
+```
- def expensive_natural_language_analysis
- FancyNaturalLanguageLibrary.analyze(env[:post].content)
+Using a template instead of raw string interpolation is a safer default, but it
+can make it less convenient to do logic while rendering. Feel free to override
+your Component's `#initialize` method and do logic there:
+
+```ruby
+class EscapedPostView < Rack::Component
+ def initialize(env)
+ @post = Post.find(env[:id])
+ # calling `super` will populate the instance-level `env` hash, making
+ # `env` available outside this method. But it's fine to skip it.
+ super
end
-end
-PostAnalysis.memoized(post: Post.find(1)) #=> slow, because it runs an expensive natural language analysis
-PostAnalysis.memoized(post: Post.find(1)) #=> instant, because the content of :post has not changed
+ render erb: <<~ERB
+ <article>
+ <h1><%= @post.title %></h1>
+ <%= @post.body %>
+ </article>
+ ERB
+end
```
-This recipe works with any Ruby object that implements a `#hash` method based
-on the object's content, including instances of `ActiveRecord::Base` and
-`Sequel::Model`.
-
### Render an HTML list from an array
[JSX Lists][jsx lists] use JavaScript's `map` function. Rack::Component does
likewise, only you need to call `join` on the array:
```ruby
require 'rack/component'
class PostsList < Rack::Component
- render do |env|
+ render do
<<~HTML
<h1>This is a list of posts</h1>
<ul>
#{render_items}
</ul>
@@ -262,16 +287,16 @@
def render_items
env[:posts].map { |post|
<<~HTML
<li class="item">
- <a href="#{post[:url]}">
+ <a href="/posts/#{post[:id]}">
#{post[:name]}
</a>
</li>
HTML
- }.join #unlike JSX, you need to call `join` on your array
+ }.join # unlike JSX, you need to call `join` on your array
end
end
posts = [{ name: 'First Post', id: 1 }, { name: 'Second', id: 2 }]
PostsList.call(posts: posts) #=> <h1>This is a list of posts</h1> <ul>...etc
@@ -287,30 +312,28 @@
end
end
# app/components/posts_list.rb
class PostsList < Rack::Component
- render { |env| posts.to_json }
-
- def posts
- Post.magically_filter_via_params(env)
+ def render
+ Post.magically_filter_via_params(env).to_json
end
end
```
### Mount a Rack::Component as a Rack app
-Because Rack::Components follow the same API as a Rack app, you can mount them
-anywhere you can mount a Rack app. It's up to you to return a valid rack
-tuple, though.
+Because Rack::Components have the same signature as Rack app, you can mount them
+anywhere you can mount a Rack app. It's up to you to return a valid rack tuple,
+though.
```ruby
# config.ru
require 'rack/component'
class Posts < Rack::Component
- render do |env|
+ def render
[status, headers, [body]]
end
def status
200
@@ -333,70 +356,106 @@
In real life, maybe don't do this. Use [Roda] or [Sinatra] for routing, and use
Rack::Component instead of Controllers, Views, and templates. But to see an
entire app built only out of Rack::Components, see
[the example spec](https://github.com/chrisfrank/rack-component/blob/master/spec/raw_rack_example_spec.rb).
+### Define `#render` at the instance level instead of via `render do`
+
+The class-level `render` macro exists to make using templates easy, and to lean
+on Ruby's keyword arguments as a limited imitation of React's `defaultProps` and
+`PropTypes`. But you can define render at the instance level instead.
+
+```ruby
+# these two components render identical output
+
+class MacroComponent < Rack::Component
+ render do |name:, dept: 'Engineering'|
+ "#{name} - #{dept}"
+ end
+end
+
+class ExplicitComponent < Rack::Component
+ def initialize(name:, dept: 'Engineering')
+ @name = name
+ @dept = dept
+ # calling `super` will populate the instance-level `env` hash, making
+ # `env` available outside this method. But it's fine to skip it.
+ super
+ end
+
+ def render
+ "#{@name} - #{@dept}"
+ end
+end
+```
+
## API Reference
The full API reference is available here:
https://www.rubydoc.info/gems/rack-component
-For info on how to clear or change the size of the memoziation cache, please see
-[the spec][spec].
-
## Performance
-On my machine, Rendering a Rack::Component is almost 10x faster than rendering a
-comparable Tilt template, and almost 100x faster than ERB from the Ruby standard
-library. Run `ruby spec/benchmarks.rb` to see what to expect in your env.
+Run `ruby spec/benchmarks.rb` to see what to expect in your environment. These
+results are from a 2015 iMac:
```
$ ruby spec/benchmarks.rb
Warming up --------------------------------------
- Ruby stdlib ERB 2.807k i/100ms
- Tilt (cached) 28.611k i/100ms
- Lambda 249.958k i/100ms
- Component 161.176k i/100ms
-Component [memoized] 94.586k i/100ms
+ stdlib ERB 2.682k i/100ms
+ Tilt ERB 15.958k i/100ms
+ Bare lambda 77.124k i/100ms
+ RC [def render] 64.905k i/100ms
+ RC [render do] 57.725k i/100ms
+ RC [render erb:] 15.595k i/100ms
Calculating -------------------------------------
- Ruby stdlib ERB 29.296k (± 2.0%) i/s - 148.771k in 5.080274s
- Tilt (cached) 319.935k (± 2.8%) i/s - 1.602M in 5.012009s
- Lambda 6.261M (± 1.2%) i/s - 31.495M in 5.031302s
- Component 2.773M (± 1.8%) i/s - 14.022M in 5.057528s
-Component [memoized] 1.276M (± 0.9%) i/s - 6.432M in 5.041348s
+ stdlib ERB 27.423k (± 1.8%) i/s - 139.464k in 5.087391s
+ Tilt ERB 169.351k (± 2.2%) i/s - 861.732k in 5.090920s
+ Bare lambda 929.473k (± 3.0%) i/s - 4.705M in 5.065991s
+ RC [def render] 775.176k (± 1.1%) i/s - 3.894M in 5.024347s
+ RC [render do] 686.653k (± 2.3%) i/s - 3.464M in 5.046728s
+ RC [render erb:] 165.113k (± 1.7%) i/s - 826.535k in 5.007444s
```
-Notice that using `Component#memoized` is _slower_ than using `Component#call`
-in this benchmark. Because these components do almost nothing, it's more work to
-check the memoziation cache than to just render. For components that don't
-access a database, don't do network I/O, and aren't very CPU-intensive, it's
-probably fastest not to memoize. For components that do I/O, using `#memoize`
-can speed things up by several orders of magnitude.
+Every component in the benchmark is configured to escape HTML when rendering.
+When rendering via a block, Rack::Component is about 25x faster than ERB and 4x
+faster than Tilt. When rendering a template via Tilt, it (unsurprisingly)
+performs roughly at tilt-speed.
## Compatibility
-Rack::Component has zero dependencies, and will work in any Rack app. It should
-even work _outside_ a Rack app, because it's not actually dependent on Rack. I
-packaged it under the Rack namespace because it follows the Rack `call`
-specification, and because that's where I use and test it.
+When not rendering Tilt templates, Rack::Component has zero dependencies,
+and will work in any Rack app. It should even work _outside_ a Rack app, because
+it's not actually dependent on Rack. I packaged it under the Rack namespace
+because it follows the Rack `call` specification, and because that's where I
+use and test it.
+When using Tilt templates, you will need `tilt` and a templating gem in your
+`Gemfile`:
+
+```ruby
+gem 'tilt'
+gem 'erubi' # or gem 'haml', etc
+gem 'rack-component'
+```
+
## Anybody using this in production?
Aye:
-- [future.com](https://www.future.com/)
-- [Seattle & King County Homelessness Response System](https://hrs.kc.future.com/)
+* [future.com](https://www.future.com/)
+* [Seattle & King County Homelessness Response System](https://hrs.kc.future.com/)
## Ruby reference
Where React uses [JSX] to make components more ergonomic, Rack::Component leans
heavily on some features built into the Ruby language, specifically:
-- [Heredocs]
-- [String Interpolation]
-- [Calling methods with a block][ruby blocks]
+* [Heredocs]
+* [String Interpolation]
+* [Calling methods with a block][ruby blocks]
## Development
After checking out the repo, run `bin/setup` to install dependencies. Then, run
`rake spec` to run the tests. You can also run `bin/console` for an interactive
@@ -424,5 +483,6 @@
[heredocs]: https://ruby-doc.org/core-2.5.0/doc/syntax/literals_rdoc.html#label-Here+Documents
[string interpolation]: http://ruby-for-beginners.rubymonstas.org/bonus/string_interpolation.html
[ruby blocks]: https://mixandgo.com/learn/mastering-ruby-blocks-in-less-than-5-minutes
[roda]: http://roda.jeremyevans.net
[sinatra]: http://sinatrarb.com
+[tilt]: https://github.com/rtomayko/tilt