Giter Club home page Giter Club logo

rack-component's Introduction

Rack::Component

Like a React.js component, a Rack::Component implements a render method that takes input data and returns what to display. You can use Components instead of Controllers, Views, Templates, and Helpers, in any Rack app.

Install

Add rack-component to your Gemfile and run bundle install:

gem 'rack-component'

Quickstart with Sinatra

# config.ru
require 'sinatra'
require 'rack/component'

class Hello < Rack::Component
  render do |env|
    "<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 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 section for details.

Table of Contents

Getting Started

Components as plain functions

The simplest component is just a lambda that takes an env parameter:

Greeter = lambda do |env|
  "<h1>Hi, #{env[:name]}.</h1>"
end

Greeter.call(name: 'Mina') #=> '<h1>Hi, Mina.</h1>'

Components as Rack::Components

Upgrade your lambda to a Rack::Component when it needs HTML escaping, instance methods, or state:

require 'rack/component'
class FormalGreeter < Rack::Component
  render do |env|
    "<h1>Hi, #{h title} #{h env[:name]}.</h1>"
  end

  # +env+ is available in instance methods too
  def title
    env[:title] || "Queen"
  end
end

FormalGreeter.call(name: 'Franklin') #=> "<h1>Hi, Queen Franklin.</h1>"
FormalGreeter.call(
  title: 'Captain',
  name: 'Kirk <[email protected]>'
) #=> <h1>Hi, Captain Kirk &lt;[email protected]&gt;.</h1>

Components if you hate inheritance

Instead of inheriting from Rack::Component, you can extend its methods:

class SoloComponent
  extend Rack::Component::Methods
  render { "Family is complicated" }
end

Recipes

Render one component inside another

You can nest Rack::Components as if they were React Children by calling them with a block.

Layout.call(title: 'Home') do
  Content.call
end

Here's a more fully fleshed example:

require 'rack/component'

# 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
class PostPage < Rack::Component
  render do |env|
    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>
            I am a footer.
          </footer>
        </main>
      HTML
    end
  end
end

class Layout < Rack::Component
  # 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>#{h title}</title>
        </head>
        <body>
        #{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

Render a template that escapes output by default via Tilt

If you add Tilt and erubi to your Gemfile, you can use the render macro with an automatically-escaped template instead of a block.

# Gemfile
gem 'tilt'
gem 'erubi'
gem 'rack-component'

# my_component.rb
class TemplateComponent < Rack::Component
  render erb: <<~ERB
    <h1>Hello, <%= name %></h1>
  ERB

  def name
    env[:name] || 'Someone'
  end
end

TemplateComponent.call #=> <h1>Hello, Someone</h1>
TemplateComponent.call(name: 'Spock<>') #=> <h1>Hello, Spock&lt;&gt;</h1>

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:

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.

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

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:

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

  render erb: <<~ERB
    <article>
      <h1><%= @post.title %></h1>
      <%= @post.body %>
    </article>
  ERB
end

Render an HTML list from an array

JSX Lists use JavaScript's map function. Rack::Component does likewise, only you need to call join on the array:

require 'rack/component'
class PostsList < Rack::Component
  render do
    <<~HTML
      <h1>This is a list of posts</h1>
      <ul>
        #{render_items}
      </ul>
    HTML
  end

  def render_items
    env[:posts].map { |post|
      <<~HTML
        <li class="item">
          <a href="/posts/#{post[:id]}">
            #{post[:name]}
          </a>
        </li>
      HTML
    }.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

Render a Rack::Component from a Rails controller

# app/controllers/posts_controller.rb
class PostsController < ApplicationController
  def index
    render json: PostsList.call(params)
  end
end

# app/components/posts_list.rb
class PostsList < Rack::Component
  def render
    Post.magically_filter_via_params(env).to_json
  end
end

Mount a Rack::Component as a Rack app

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.

# config.ru
require 'rack/component'

class Posts < Rack::Component
  def render
    [status, headers, [body]]
  end

  def status
    200
  end

  def headers
    { 'Content-Type' => 'application/json' }
  end

  def body
    Post.all.to_json
  end
end

run Posts

Build an entire App out of Rack::Components

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.

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.

# 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

Performance

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 --------------------------------------
          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 -------------------------------------
          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

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

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:

gem 'tilt'
gem 'erubi' # or gem 'haml', etc
gem 'rack-component'

Anybody using this in production?

Aye:

Ruby reference

Where React uses JSX to make components more ergonomic, Rack::Component leans heavily on some features built into the Ruby language, specifically:

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 prompt that will allow you to experiment.

To install this gem onto your local machine, run bundle exec rake install. To release a new version, update the version number in version.rb, and then run bundle exec rake release, which will create a git tag for the version, push git commits and tags, and push the .gem file to rubygems.org.

Contributing

Bug reports and pull requests are welcome on GitHub at https://github.com/chrisfrank/rack-component.

License

MIT

rack-component's People

Contributors

chrisfrank avatar kevin-wei avatar olleolleolle avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar

rack-component's Issues

Components should escape strings by default to prevent XSS attacks

Because Rack::Component uses raw Ruby string interpolation -- Hello, #{env[:world]} -- there's not an obvious way to escape output by default. This leaves components open to XSS attacks like this:

evil_user = { name: 'Mirror Spock <script src="https://evil.script.js" />' }

class UserProfile < Rack::Component
  render do |env|
    "<h1>#{env[:name]}</h1>"
  end
end
UserProfile.call(evil_user) #=> <h1>Mirror Spock <script src="https://evil.script.js" /></h1>

Anyone visiting Spock's profile would unknowingly load the evil script.

Possible Solutions

Add an escape helper to Rack::Component

Adding an instance method that escapes untrusted input, let's call it #h, would be trivial:

class UserProfile < Rack::Component
  render do |env|
    "<h1>#{h env[:name]}</h1>"
  end
end
UserProfile.call(evil_user) #=> <h1>Mirror Spock &lt;script src=&quot;https://evil.script.js&quot; /&gt;</h1>
# no problem

This approach would be an improvement, but still not very secure. We'd have to remember to call h every time we render untrusted input.

Escape all strings from env automatically

In addition to adding an h method, we could change how the #env accessor works and escape its values by default. This would make anything in env safe by default, which is good, but it wouldn't apply to variables outside of env.

Under this approach, this component would safely print a user's name:

class UserProfile < Rack::Component
  render do |env|
    "<h1>#{env[:name]}</h1>"
  end
end
UserProfile.call(evil_user) #=> <h1>Mirror Spock &lt;script src=&quot;https://evil.script.js&quot; /&gt;</h1>

But this component would print the malicious script:

class EvilUserProfile < Rack::Component
  render do |env|
    user = FakeUserModel.find(id_of_evil_user)
    "<h1>#{user[:name]}</h1>"
  end
end
UserProfile.call(evil_user) #=> <h1>Mirror Spock <script src="https://evil.script.js" /></h1>

Use template references instead of string interpolation

Instead of interpolating with #{}, we could interpolate with %{}, passing the output to Kernel#format under the hood. See this Idiosyncratic Ruby article for reference, and the code in the the "escape" branch for an implementation-in-progress.

This approach seems promising to me, but there are some decisions to make about the scope in which to evaluate template tokens. Here are a few possibilities:

# We could interpret tokens in the context of the component instance:
class UserProfile < Rack::Component
  render do
    "<h1>%{env[:name]}</h1>"
  end
end

# We could interpret tokens as keys in `env`:
class UserProfile < Rack::Component
  render do
    "<h1>%{name}</h1>"
  end
end

Either way, under the hood, we would escape all strings by default. To render non-escaped strings, you'd just use #{} instead of %{}.

Under this approach, I worry that mixing #{} and %{} is confusing, and there would definitely be times when #{} is required, like to render a list:

class List < Rack::Component
  render do
    <<~HTML
      <ul>
        #{env[:posts].map { |post| ListItem.call(post) }.join}
      </ul>
    HTML
  end

  class ListItem < Rack::Component
    render { "<li>%{env[:title]}</li>" }
  end
end

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.