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 &lt;kirk@starfleet.gov&gt;.</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&lt;&gt;</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