0.0
No release in over 3 years
XlsxComposer provides cursor-based navigation and reusable templates for generating dynamic Excel spreadsheets using WriteXLSX.
2005
2006
2007
2008
2009
2010
2011
2012
2013
2014
2015
2016
2017
2018
2019
2020
2021
2022
2023
2024
2025
2026
 Dependencies

Development

>= 0

Runtime

>= 0
~> 1.12
 Project Readme

XlsxComposer

XlsxComposer is a small Ruby library built on top of WriteXLSX that makes the creation of complex and dynamic Excel spreadsheets easier, more readable, and more maintainable through cursor-based navigation and composition.

It does not try to replace WriteXLSX.
Instead, it provides a higher-level abstraction focused on navigation, composition, and formatting, which are usually the hardest parts when spreadsheets stop being static.

Motivation

WriteXLSX does an excellent job at generating .xlsx files.
However, when spreadsheets become dynamic, code tends to become harder to read and maintain:

  • manual row and column index management
  • lots of hard-coded coordinates
  • duplicated formatting logic
  • fragile templates that only work for fixed layouts

XlsxComposer introduces a cursor-based approach to spreadsheet writing:

  • move through the sheet with next_row, next_col, go_to, etc.
  • reuse the same template for variable-length data
  • define and reuse formats easily
  • compose spreadsheets incrementally instead of calculating coordinates upfront

Installation

Add this line to your application's Gemfile:

gem "xlsx_composer"

And then execute:

bundle install

Basic usage

require "xlsx_composer"

workbook = XlsxComposer.workbook("example.xlsx")

sheet = XlsxComposer::Worksheet.new(workbook, "Demo")

sheet.go_to("1", "A")
sheet.write_row(["Year", "Revenue"], sheet.bold)

sheet.next_row
sheet.write_row([2024, 1000], sheet.thousands_mark)

sheet.next_row
sheet.write_row([2025, 1250], sheet.thousands_mark)

workbook.close

Cursor-based navigation

Instead of calculating coordinates manually, you move a cursor across the sheet.

Available navigation methods

go_to(row, col)      # go_to("3", "B")
go_to_row(row)
go_to_col(col)

next_row
previous_row
next_col
previous_col

next_rows(n)
previous_rows(n)
next_cols(n)
previous_cols(n)
  • Rows are 1-based
  • Columns use Excel notation (A, Z, AA, BA, etc.)

Dynamic templates

The main idea behind XlsxComposer is that the same spreadsheet template can generate different shapes of data.

Example: contracts with variable durations

Imagine a Contract entity that produces yearly results:

  • some contracts last 3 years
  • others last 5, 10, or more
  • the layout is the same, only the number of rows changes
class ContractWorksheet < XlsxComposer::Base
  def worksheet_name
    "Contract Summary"
  end

  def run(contract)
    write_row(["Year", "Value"], bold)

    contract.years.each do |year|
      next_row
      write_row([year.number, year.value], thousands_mark)
    end
  end
end

The same template works regardless of how many years the contract has.

Composing a worksheet using multiple templates

For more complex spreadsheets, it is common to split a single worksheet into multiple logical sections, each one handled by a dedicated service or template.

XlsxComposer supports this by allowing multiple composers to write into the same worksheet instance.

Orchestrating a worksheet

The main composer is responsible for creating the worksheet and coordinating the order in which sections are written.

class ReportWorksheet < XlsxComposer::Base
  def initialize(workbook, report)
    @report = report
    super(workbook: workbook)
  end

  def worksheet_name
    "Report"
  end

  def run
    write_header
    write_summary
    write_details
  end

  private

  attr_reader :report

  def write_header
    HeaderSection.new(composer_worksheet, report).run
  end

  def write_summary
    SummarySection.new(composer_worksheet, report).run
  end

  def write_details
    DetailsSection.new(composer_worksheet, report).run
  end
end

Writing a section using an existing worksheet

Each section receives the same XlsxComposer::Worksheet instance and writes to it using the same cursor-based API.

class HeaderSection < XlsxComposer::Base
  def initialize(worksheet, report)
    @report = report
    super(worksheet: worksheet)
  end

  def run
    go_to("1", "A")
    write_row(["Report title"], bold)

    next_row
    write_row([report.title])
  end

  private

  attr_reader :report
end

Formatting

Built-in formats

XlsxComposer ships with a set of common, reusable formats:

bold
bold_center
bold_title
bold_percent

thousands_mark
currency_brl

font_size_ten
font_size_ten_thousands_mark
font_size_ten_currency_brl
font_size_ten_percent

Formats are cached per worksheet and reused internally by WriteXLSX.

Usage example:

write_row(["Total"], bold)
write_row([1000], currency_brl)

Adding custom formats

You can define additional formats per worksheet class using add_format.

class ReportWorksheet < XlsxComposer::Base
  add_format :header, bold: 1, size: 14, align: "center"
  add_format :highlight, bold: 1, align: "center", bg_color: "yellow"

  def worksheet_name
    "Report"
  end

  def run
    write_row(["Report"], header)
    next_row
    write_row(["Important"], highlight)
  end
end

Dynamic formats

Formats can also be defined using a block:

add_format(:conditional) do
  if some_condition?
    { bold: 1, color: "red" }
  else
    { bold: 1 }
  end
end

Global vs class-specific formats

  • Global formats live in XlsxComposer::Formatter and are available everywhere.
  • Class-specific formats are defined via add_format and scoped to that worksheet composer.

Both are cached on the underlying worksheet and backed by WriteXLSX Format objects.

Why not just WriteXLSX?

You absolutely can — and should — use WriteXLSX directly for simple spreadsheets.

XlsxComposer becomes valuable when:

  • spreadsheets are generated from templates
  • layouts are dynamic
  • rows and columns are added conditionally or iteratively
  • formatting logic starts spreading across the codebase
  • readability and maintainability matter

Think of XlsxComposer as a composition layer, not a replacement.

Contributing

Bug reports and pull requests are welcome on GitHub at https://github.com/hugopassos/xlsx_composer. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the code of conduct.

License

The gem is available as open source under the terms of the MIT License.

Code of Conduct

Everyone interacting in the XlsxComposer project's codebases, issue trackers, chat rooms and mailing lists is expected to follow the code of conduct.