fxl.js (/ˈfɪk.səl/ or "pixel" with an f) is a data-oriented JavaScript spreadsheet library built on top of ExcelJS. The library focuses on composability, and aims to provide a way to build spreadsheets using modular, lego-like blocks. Its primary use case is for building spreadsheets based on human-designed templates that are not tabular.
fxl.js is a JavaScript adaptation of the original Clojure library fxl.
Contents
Installation
npm install @01group/fxl
The TypeDoc generated documentation can be found here.
Why fxl.js?
There are three things that fxl.js tries to do differently compared to other JavaScript spreadsheet libraries, namely:
- immutability: the entire API requires no side effects or mutations except for the IO operations at the very start or end for reading and writing the spreadsheets respectively. With fxl.js, it is more ergonomic to work with data and pure functions until near the end of the application, where all the side effects are isolated and happen in one go - see Functional Core, Imperative Shell.
- data orientation: the data model is represented as plain, nested JavaScript objects with literal child nodes. This allows us to reuse common JavaScript functions/methods to manipulate objects, and easily integrate fxl.js with functional utility libraries such as Lodash and Ramda - see Alan Perlis' Epigram on common functions.
- cells as unordered collections of objects: by expressing value, coordinate and style as three separate, orthogonal properties, we can work on the three components that make up spreadsheet separately. We can deal with interactions of the components only when we put them together. Expressing columns and rows as ordered sequences introduces complexity - see Rich Hickey's take on the list-and-order problem.
fxl.js is not built with performance in mind. It is built on top of ExcelJS, which thus sets the performance limit for the library. fxl.js shines at building spreadsheets based on human-designed templates, which typically do not translate well to tabular formats such as CSVs, records or nested lists.
Examples
Cells as Plain Data
A fxl.js cell is an object with three properties, namely value, coordinate and optionally style. The following are valid cells:
{ value: 'abc', coord: { row: 0, col: 0 } }
{
value: 1.23,
coord: { row: 2, col: 3, sheet: 'Sheet 1' },
style: {
numFmt: '0.00%',
border: {
right: { style: 'medium', color: { argb: 'FF00FF00' } },
left: { style: 'medium', color: { argb: 'FF00FF00' } },
},
font: { name: 'Roboto', size: 16, bold: true },
},
}
By understanding the fxl.Cell
interface, you are very close to being very productive with fxl.js! The rest of the library is composed of IO functions (such as fxl.readXlsx
and fxl.writeXlsx
) and shortcut functions that make life very easy when massaging the cell objects.
To find out more about fxl.js' cell interface, see the interface declaration and ExcelJS' cell value and style.
Creating a Spreadsheet (The Wrong Way)
Let's suppose that we would like to create a plain spreadsheet such as the following:
| Item | Cost |
| -------- | -------- |
| Rent | 1000 |
| Gas | 100 |
| Food | 300 |
| Gym | 50 |
| Total | 1450 |
from an existing JavaScript array of objects such as the following:
const costs = [
{ item: "Rent", cost: 1000 },
{ item: "Gas", cost: 100 },
{ item: "Food", cost: 300 },
{ item: "Gym", cost: 50 },
];
We would break the spreadsheet down into three components, namely the header, the body and the total. The following is not the prettiest piece of code (and not the recommended way of using fxl.js), but it would work:
const headerCells = [
{ value: 'Item', coord: { row: 0, col: 0 } },
{ value: 'Cost', coord: { row: 0, col: 1 } },
];
const bodyCells = costs.flatMap((record, index) => {
return [
{ value: record.item, coord: { row: index + 1, col: 0 } },
{ value: record.cost, coord: { row: index + 1, col: 1 } },
];
});
const totalCells = [
{ value: 'Total', coord: { row: costs.length + 2, col: 0 } },
{
value: costs.map((x) => x.cost).reduce((x, y) => x + y),
coord: { row: costs.length + 2, col: 1 },
},
];
We then concatenate them, and ask fxl.js to write the cells into an XLSX file:
import * as fxl from '@01group/fxl';
const allCells = headerCells.concat(bodyCells).concat(totalCells);
await fxl.writeXlsx(allCells, 'costs.xlsx')
The above summarises the essence of spreadsheet building with fxl.js. It is about taking a piece of data, transform it into the cell objects before finally calling an IO function.
Loading a Spreadsheet
const cells = await fxl.readXlsx('costs.xlsx')
Coordinate Shortcuts
An important part of fxl.js is the collection of shortcut functions that makes it easy to create the cell objects. We can boil down the above example to the following:
import * as fxl from '@01group/fxl';
const costs = [
{ item: "Rent", cost: 1000 },
{ item: "Gas", cost: 100 },
{ item: "Food", cost: 300 },
{ item: "Gym", cost: 50 },
];
const totalCost = costs.map((x) => x.cost).reduce((x, y) => x + y);
const headerCells = fxl.rowToCells(["Item", "Cost"]);
const bodyCells = fxl.recordsToCells(["item", "cost"], costs);
const totalCells = fxl.rowToCells(["Total", totalCost]);
const allCells = fxl.concatBelow(headerCells, bodyCells, totalCells);
await fxl.writeXlsx(allCells, 'costs.xlsx')
fxl.js provides shortcuts for creating rows, cells and tables from plain values (such as fxl.rowToCells
, fxl.colToCells
, fxl.tableToCells
and fxl.recordToCells
), as well as shortcuts for combining groups of cells together (such as fxl.concatRight
and fxl.concatBelow
). This allows us to break down a big spreadsheet into very small components, and only to put them together later at a higher level of abstraction.
Style Shortcuts
Let's suppose that we would like to style our simple spreadsheet as follows:
- The header row's font should be bold with a light gray background.
- The footer row should be the same as the header row, but with a dark red font colour.
- The item column of the body should be in italic.
- All cells should be horizontally aligned center.
In this case, we would take each bullet point into its own function, and apply it to the right cell components:
function setHeaderStyle(cell: fxl.Cell): fxl.Cell {
return fxl.pipe(cell, fxl.setBold(true), fxl.setSolidFg('light_gray'));
}
function setTotalStyle(cell: fxl.Cell): fxl.Cell {
return fxl.pipe(cell, setHeaderStyle, fxl.setFontColor('dark_red'));
}
function setBodyStyle(cell: fxl.Cell): fxl.Cell {
if (cell.coord.col == 0) {
return fxl.setItalic(true)(cell);
} else {
return cell;
}
}
const allCells = fxl
.concatBelow(
headerCells.map(setHeaderStyle),
bodyCells.map(setBodyStyle),
totalCells.map(setTotalStyle)
)
.map(fxl.setHorizontalAlignement('center'));
Notice that fxl.js comes with a few handy higher-order functions in order to facilitate function compositions, such as fxl.pipe
and fxl.compose
.
Putting Things Together
When we put our running example together, we actually see a relatively common pattern when building spreadsheets with fxl.js. In general, when building a spreadsheet using fxl.js, we follow a number of high-level steps:
- prepare the data to be used as cell values;
- build small spreadsheet components with those values;
- prepare the styles of each component;
- put together the styled components in their relative coordinates; and
- finally executing the IO operation.
import * as fxl from '@01group/fxl';
const costs = [
{ item: "Rent", cost: 1000 },
{ item: "Gas", cost: 100 },
{ item: "Food", cost: 300 },
{ item: "Gym", cost: 50 },
];
const totalCost = costs.map((x) => x.cost).reduce((x, y) => x + y);
const headerCells = fxl.rowToCells(["Item", "Cost"]);
const bodyCells = fxl.recordsToCells(["item", "cost"], costs);
const totalCells = fxl.rowToCells(["Total", totalCost]);
const allCells = fxl.concatBelow(headerCells, bodyCells, totalCells);
function setHeaderStyle(cell: fxl.Cell): fxl.Cell {
return fxl.pipe(cell, fxl.setBold(true), fxl.setSolidFg('light_gray'));
}
function setTotalStyle(cell: fxl.Cell): fxl.Cell {
return fxl.pipe(cell, setHeaderStyle, fxl.setFontColor('dark_red'));
}
function setBodyStyle(cell: fxl.Cell): fxl.Cell {
if (cell.coord.col == 0) {
return fxl.setItalic(true)(cell);
} else {
return cell;
}
}
const allCells = fxl
.concatBelow(
headerCells.map(setHeaderStyle),
bodyCells.map(setBodyStyle),
totalCells.map(setTotalStyle)
)
.map(fxl.setHorizontalAlignement('center'));
await fxl.writeXlsx(allCells, 'costs.xlsx')
See also the inventory-spreadsheet walkthrough and its accompanying script for a more detailed example based on a real use case.
Known Issues
- Column widths and row heights are not persisted after writing the spreadsheet. Loading an existing spreadsheet will contain no information regarding column widths and row heights.
See also ExcelJS' known issues.
Contributing
fxl.js is very much a work-in-progress. Whilst it is being used in production at Zero One Group, it may not be stable just yet. We would love your help to make it production ready! Any sort of contributions (issues, pull requests or general feedback) are all very welcomed!
See the contributing document.
Further Resources
License
Copyright 2021 Zero One Group.
fxl.js is licensed under Apache License v2.0. It means that "users can do (nearly) anything they want with the code, with very few exceptions". See here for more information.