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.
# 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:
# 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:
# 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.
# 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:
# 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
.
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:
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:
# 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.
<%= link_to 'Download (CSV)', books_path(:format => :csv) %>
View
The CSV view is similar to the handler view we used in the integration test.
# 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!