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.
npm install @01group/fxl
The TypeDoc generated documentation can be found here.
There are three things that fxl.js tries to do differently compared to other JavaScript spreadsheet libraries, namely:
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.
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.
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.
const cells = await fxl.readXlsx('costs.xlsx')
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.
Let's suppose that we would like to style our simple spreadsheet as follows:
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
.
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:
import * as fxl from '@01group/fxl';
// ------------------------------------------------------------------
// data
// ------------------------------------------------------------------
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);
// ------------------------------------------------------------------
// spreadsheet components
// ------------------------------------------------------------------
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);
// ------------------------------------------------------------------
// styles
// ------------------------------------------------------------------
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;
}
}
// ------------------------------------------------------------------
// relative coordinates
// ------------------------------------------------------------------
const allCells = fxl
.concatBelow(
headerCells.map(setHeaderStyle),
bodyCells.map(setBodyStyle),
totalCells.map(setTotalStyle)
)
.map(fxl.setHorizontalAlignement('center'));
// ------------------------------------------------------------------
// IO
// ------------------------------------------------------------------
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.
See also ExcelJS' known issues.
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.
fxl.js
gave, which includes the inspiration and thinking behind the original Clojure library.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.
Generated using TypeDoc