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