README.md in rack-component-0.2.0 vs README.md in rack-component-0.3.0
- old
+ new
@@ -1,10 +1,12 @@
# Rack::Component
-Like a React.js component, a `Rack::Component` implements a `render` method that takes input data and returns what to display.
+Like a React.js component, a `Rack::Component` implements a `render` method that
+takes input data and returns what to display.
-You can combine Components to build complex features out of simple, easily testable units.
+You can combine Components to build complex features out of simple, easily
+testable units.
## Installation
Add this line to your application's Gemfile:
@@ -19,103 +21,107 @@
Or install it yourself as:
$ gem install rack-component
## API Reference
-Please see the [YARD docs on rubydoc.info](https://www.rubydoc.info/gems/rack-component)
+Please see the
+[YARD docs on rubydoc.info](https://www.rubydoc.info/gems/rack-component)
+
## Usage
-You could build an entire app out of Components, but Ruby already has great HTTP routers like [Roda][roda] and [Sinatra][sinatra]. Here's an example that uses Sinatra for routing, and Components instead of views, controllers, and templates.
+Subclass `Rack::Component` and `#call` it:
-### With Sinatra
-
```ruby
-get '/posts/:id' do
- PostFetcher.call(id: params[:id]) do |post|
- Layout.call(title: post[:title]) do
- PostView.call(post)
- end
- end
+require 'rack/component'
+class Useless < Rack::Component
end
-```
-_Why_, you may be thinking, _would I write something so ugly when I could write this instead?_
-
-```ruby
-get '/posts/:id' do
- @post = Post.find(params[:id])
- @title = @post[:title]
- erb :post
-end
+Useless.call #=> the output Useless.new.render
```
-You'd be right that the traditional version is shorter and pretter. But the Component version’s API is more declarative -- you are describing what you want, and leaving the details of _how to get it_ up to each Component, instead of writing implementation-specific details right in your route block.
+The default implementation of `#render` is to yield the component instance to
+whatever block you pass to `Component.call`, like this:
-The Component version is easier to reuse, refactor, and test. And because Components are meant to be combined via composition, it's actually trivial to make a Component version that's even more concise:
-
```ruby
-get('/posts/:id') do
- PostPageView.call(id: params[:id])
-end
+Useless.call { |instance| "Hello from #{instance}" }
+#=> "Hello from #<Useless:0x00007fcaba87d138>"
-# Compose a few Components to save on typing
-class PostPageView < Rack::Component
- def render
- PostFetcher.call(id: props[:id]) do |post|
- Layout.call(title: post[:title]) { PostView.call(post) }
- end
+Useless.call do |instance|
+ Useless.call do |second_instance|
+ <<~HTML
+ <h1>Hello from #{instance}</h1>
+ <p>And also from #{second_instance}"</p>
+ HTML
end
end
+# =>
+# <h1>Hello from #<Useless:0x00007fcaba87d138></h1>
+# <p>And also from #<Useless:0x00007f8482802498></p>
```
-PostFetcher, Layout, and PostView are all simple Rack::Components. Their implementation looks like this:
+### Implement `#render` or add instance methods to make Components do work
+Peruse the [specs][specs] for examples of component chains that handle
+data fetching, views, and error handling in Sinatra and raw Rack.
+
+Here's a component chain that prints headlines from Daring Fireball’s JSON feed:
+
```ruby
require 'rack/component'
-# render an HTML page
-class Layout < Rack::Component
+# Make a network request and return the response
+class Fetcher < Rack::Component
+ require 'net/http'
+ def initialize(uri:)
+ @response = Net::HTTP.get(URI(uri))
+ end
+
def render
- %(
- <html>
- <head>
- <title>#{props[:title]}</title>
- </head>
- <body>
- #{yield}
- </body>
- </html>
- )
+ yield @response
end
end
-# Fetch a post, pass it to the next component
-class PostFetcher < Rack::Component
- def render
- yield fetch
+# Parse items from a JSON Feed document
+class JSONFeedParser < Rack::Component
+ require 'json'
+ def initialize(data)
+ @items = JSON.parse(data).fetch('items')
end
- def fetch
- DB[:posts].fetch(props[:id].to_i)
+ def render
+ yield @items
end
end
-# A fake database with a fake 'posts' table
-DB = { posts: { 1 => { title: 'Example Title', body: 'Example body' } } }
+# Render an HTML list of posts
+class PostsList < Rack::Component
+ def initialize(posts:, style: '')
+ @posts = posts
+ @style = style
+ end
-# View a single post
-class PostView < Rack::Component
def render
- %(
- <article>
- <h1>#{props[:title]}</h1>
- <p>#{props[:body]}</h1>
- </article>
- )
+ <<~HTML
+ <ul style="#{@style}">
+ #{@posts.map(&ListItem).join}"
+ </ul>
+ HTML
end
+
+ ListItem = ->(post) { "<li>#{post['title']}</li>" }
end
+
+# Fetch JSON Feed data from daring fireball, parse it, render a list
+Fetcher.call(uri: 'https://daringfireball.net/feeds/json') do |data|
+ JSONFeedParser.call(data) do |items|
+ PostsList.call(posts: items, style: 'background-color: red')
+ end
+end
+end
+#=> A <ul> full of headlines from Daring Fireball
+
```
## Development
After checking out the repo, run `bin/setup` to install dependencies. Then, run
@@ -135,7 +141,6 @@
## License
MIT
-[roda]: https://github.com/jeremyevans/roda
-[sinatra]: https://github.com/sinatra/sinatra
+[specs]: https://github.com/chrisfrank/rack-component/tree/master/spec