Railsで既存のエクセルファイルをテンプレートにできる魔法のヘルパー


はじめに

今回のコードは、以下の環境で動作確認しています。


  • Rails 5.2.0

  • rubyXL 3.3.29


1. rubyXL Gem をプロジェクトに追加

Ruby で エクセルファイルを扱うのに rubyXL を利用します。


Gemfile

gem 'rubyXL'



2. 次に魔法のヘルパーを追加します

中身はとりあえずこれだけ❤


app/helpers/excel_template_helper.rb

module ExcelTemplateHelper

def excel_render(template_file)
RubyXL::Parser.parse(template_file).tap do |workbook|
workbook.worksheets.each do |worksheet|
@worksheet = worksheet
@worksheet.each_with_index do |row, row_num|
row&.cells&.each do |cell|
next if cell.nil?
cell_render(cell)
end
row_height_auto(row_num)
end
end
end
end

private

def content_eval(content)
view_context.instance_eval(%("#{content}")).gsub(/\R/, "\n") # エクセルの改行は LF
end

def cell_render(cell)
cell.change_contents(content_eval(cell.value))
cell.change_text_wrap(true) if (cell&.value&.lines("\n")&.count) > 1
rescue
cell.change_contents('error!')
cell.change_font_color('FF0000')
cell.change_fill('FFFF00')
end

def row_height_auto(row_num)
max_lines = @worksheet[row_num]&.cells&.map { |cell| cell&.value&.lines("\n")&.count || 0 }&.max
origin_height = [@worksheet.get_row_height(row_num), 20].max # 最小値が RubyXL::Row::DEFAULT_HEIGHT (= 13) では合わなかったので手動調整
@worksheet.change_row_height(row_num, origin_height * max_lines) if max_lines&.positive?
end
end


ポイントは content_eval メソッド内の view_context.instance_eval() です。これのおかげで、通常の view と同じように、エクセルファイル内でインスタンス変数を扱えるようになります。


3. 呼び出す controller 側のサンプルを書きます

こんな感じ。


app/controllers/excel_controller.rb

class ExcelController < ApplicationController

include ExcelTemplateHelper

def sample1
create_data
send_data(
excel_render('lib/excel_templates/sample1.xlsx').stream.string,
type: 'application/vnd.ms-excel',
filename: "#{@datetime.strftime("%Y%m%d_%H%M%S")}_sample1(#{@name}).xlsx"
)
end

private

def create_data
@datetime = Time.now
@name = 'm-kubo'
@multi_lines_text = "Remember kids,\nthe magic is with in you.\nI'm princess m-kubo."
end
end



4. テンプレートになるエクセルを用意しよう

例えば、 lib/excel_templates/sample1.xlsx にこんな感じのエクセルファイルを配置します。

image.png


5. 出力してみよう

routing を追加して、


config/routes.rb

get 'sample1', to: 'excel#sample1'


/sample1 にアクセスすると、レンダリングされたエクセルファイルがダウンロードされます 🧙‍♀️

image.png

帳票の種類が多かったり、帳票レイアウトの変更が多い案件の場合、エクセルファイルで直接レイアウトなどを修正できるため、開発も運用もすごく楽になると思います :smile:


6. おまけ

excel_template_helpercell_render メソッドを以下のように変更して、


app/helpers/excel_template_helper.rb

  def spread_render(cell, array_name)

original_style_index = cell.style_index
original_row_height = @worksheet.get_row_height(cell.row)
view_context.instance_eval(array_name).each_with_index do |_array, index|
target_row = cell.row + index
target_column = cell.column
target_value = cell.value.gsub(array_name) { "#{array_name}[#{index}]" } # 配列変数部に添え字を付与する
@worksheet.add_cell(target_row, target_column, content_eval(target_value))
@worksheet[target_row][target_column].style_index = original_style_index
@worksheet.change_row_height(target_row, original_row_height)
end
end

def normal_render(cell)
cell.change_contents(content_eval(cell.value))
cell.change_text_wrap(true) if (cell&.value&.lines("\n")&.count) > 1
end

def cell_render(cell)
if /(?<array_name>@[\w-]+)\[:[\w-]*\]/ =~ cell.value
spread_render(cell, array_name) # 配列変数を展開しながらのレンダリング
else
normal_render(cell) # 通常のレンダリング
end
rescue StandardError
cell.change_contents('error!')
cell.change_font_color('FF0000')
cell.change_fill('FFFF00')
end


こんなテンプレートを用意すると、

image.png

配列変数を自動展開させて、任意の一覧表を出力させることもできます。

image.png

工夫次第ですね❤


終わりに

以上、どこかの案件で書いたコードの紹介でした。

誰かの役に立ちますように!