In a previous tutorial we saw how to create Rails views with Markdown. Another common need in web applications is to render CSV views. We’ll do that using a handler. The code for this tutorial can be found on github.

Test

As usual, let’s start with an integration test. We’ll use a HandlersController for the integration test with only one action, index. This allows us to test the functionality from front to end.

ruby
# app/controllers/handlers_controller.rb

class HandlersController < ApplicationController
  def index
    @books = []
    @books << Book.new(title: 'Da Vinci Code', author: 'Dan Brown')
    @books << Book.new(title: 'Name of the Rose', author: 'Umberto Eco')
  end
end

Here we have a list of books that we want to download later in a CSV file. Our handler will need to respond to CSV format, and return three comma separated rows of the book details. The integration test for this is described below:

ruby
# test/integration/csv_handler_test.rb

class CsvHandlerTest < ActionDispatch::IntegrationTest
  test ".csv renders CSV" do
    get '/handlers.csv'
    assert_equal "Title,Author\nDa Vinci Code,Dan Brown\nName of the Rose,Umberto Eco\n", response.body
  end
end

Handler

We’ll add the handler to the lib folder, so let’s make sure that the folder is included in the autoload path:

ruby
# config/application.rb

config.autoload_paths += %W(#{config.root}/lib)

The benefits of using a handler lie on encapsulating the interaction with the csv library, and using a csv object instead, like the json object in Jbuilder.

ruby
# lib/csv_handler.rb

require 'csv'

module CsvHandler
  class CsvGenerator
    def self.generate
      file = CSV.generate do |csv|
        yield csv
      end
      file.html_safe
    end
  end

  class Handler
    def self.call (template)
      %{
        CsvHandler::CsvGenerator.generate do |csv|
          #{template.source}
        end
      }
    end
  end
end

The CsvGenerator class generates the CSV file by yielding the csv object. We can then use that object to add columns and rows.

The Handler class has only one call method, necessary to be considered a template handler. It calls the generator class on the view template, and returns the string to be rendered.

In order to hook the handler to the app, we need to add the line below to the initializer:

ruby
# config/initializers/handlers.rb

ActionView::Template.register_template_handler :am, CsvHandler::Handler

We have given our template viewer a .am extension, but anything that is not used by other plugins, would do. This means that our handler view would be named index.csv.am.

ruby
csv << ['Title', 'Author']

@books.each do |book|
  csv << [
    book.title,
    book.author
  ]
end

As the CSV library expects, we add rows of arrays to the csv object. If we run the test now, it should pass:

sh
bin/rake test test/integration/csv_handler_test.rb

Controller

To make the example complete, let’s add our new handler to a real page. For this, we can use our old BooksController:

ruby
# app/controllers/books_controller.rb

class BooksController < ApplicationController
  def index
    @books = Book.order(:title)
  end
end

Our book model has a few properties like title, author, etc. We display them in an index.html.erb as usual, and we also display a link to download the list in CSV format.

html
<%= link_to 'Download (CSV)', books_path(:format => :csv) %>

View

The CSV view is similar to the handler view we used in the integration test.

ruby
# app/views/books/index.csv.am

csv << ['Title', 'Author', 'Price', 'Publication date', 'Image url']

@books.each do |book|
  csv << [
    book.title,
    book.author,
    number_to_currency(book.price),
    book.published_at.strftime('%B %Y'),
    book.image_url
  ]
end

Please note that all the Rails helpers are available on our new views, just like in erb views.

Conclusion

That’s it. Only a few lines of code, allowed us to write CSV as just another Rails view.

Another way to create CSV files in Rails is via a renderer, as explained in Rails documentation. Please check it out and choose the best way that works for you.

Happy coding!