- Notifications
You must be signed in to change notification settings - Fork4
ƛ fxl.js is a data-oriented JavaScript spreadsheet library. It provides a way to build spreadsheets using modular, lego-like blocks.
License
zero-one-group/fxl.js
Folders and files
Name | Name | Last commit message | Last commit date | |
---|---|---|---|---|
Repository files navigation
fxl.js (/ˈfɪk.səl/ or "pixel" with an f) is adata-oriented JavaScript spreadsheet library built on top ofExcelJS. The library focuses oncomposability, 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 libraryfxl.
- Installation
- Whyfxl.js?
- Examples
- Cells as Plain Data
- Creating a Spreadsheet (The Wrong Way)
- Loading a Spreadsheet
- Coordinate Shortcuts
- Style Shortcuts
- Putting Things Together
- Known Issues
- Further Resources
- License
npm install @01group/fxl
The TypeDoc generated documentation can be foundhere.
There are three things thatfxl.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. Withfxl.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 - seeFunctional 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 integratefxl.js with functional utility libraries such asLodash andRamda - seeAlan 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 - seeRich 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.
Afxl.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 thefxl.Cell
interface, you are very close to being very productive withfxl.js! The rest of the library is composed of IO functions (such asfxl.readXlsx
andfxl.writeXlsx
) and shortcut functions that make life very easy when massaging the cell objects.
To find out more aboutfxl.js' cell interface, seethe interface declaration andExcelJS' 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:
constcosts=[{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 usingfxl.js), but it would work:
constheaderCells=[{value:'Item',coord:{row:0,col:0}},{value:'Cost',coord:{row:0,col:1}},];constbodyCells=costs.flatMap((record,index)=>{return[{value:record.item,coord:{row:index+1,col:0}},{value:record.cost,coord:{row:index+1,col:1}},];});consttotalCells=[{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 askfxl.js to write the cells into an XLSX file:
import*asfxlfrom'@01group/fxl';constallCells=headerCells.concat(bodyCells).concat(totalCells);awaitfxl.writeXlsx(allCells,'costs.xlsx')
The above summarises the essence of spreadsheet building withfxl.js. It is about taking a piece of data, transform it into the cell objects before finally calling an IO function.
constcells=awaitfxl.readXlsx('costs.xlsx')
An important part offxl.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*asfxlfrom'@01group/fxl';constcosts=[{item:"Rent",cost:1000},{item:"Gas",cost:100},{item:"Food",cost:300},{item:"Gym",cost:50},];consttotalCost=costs.map((x)=>x.cost).reduce((x,y)=>x+y);constheaderCells=fxl.rowToCells(["Item","Cost"]);constbodyCells=fxl.recordsToCells(["item","cost"],costs);consttotalCells=fxl.rowToCells(["Total",totalCost]);constallCells=fxl.concatBelow(headerCells,bodyCells,totalCells);awaitfxl.writeXlsx(allCells,'costs.xlsx')
fxl.js provides shortcuts for creating rows, cells and tables from plain values (such asfxl.rowToCells
,fxl.colToCells
,fxl.tableToCells
andfxl.recordToCells
), as well as shortcuts for combining groups of cells together (such asfxl.concatRight
andfxl.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:
- 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:
functionsetHeaderStyle(cell:fxl.Cell):fxl.Cell{returnfxl.pipe(cell,fxl.setBold(true),fxl.setSolidFg('light_gray'));}functionsetTotalStyle(cell:fxl.Cell):fxl.Cell{returnfxl.pipe(cell,setHeaderStyle,fxl.setFontColor('dark_red'));}functionsetBodyStyle(cell:fxl.Cell):fxl.Cell{if(cell.coord.col==0){returnfxl.setItalic(true)(cell);}else{returncell;}}constallCells=fxl.concatBelow(headerCells.map(setHeaderStyle),bodyCells.map(setBodyStyle),totalCells.map(setTotalStyle)).map(fxl.setHorizontalAlignement('center'));
Notice thatfxl.js comes with a few handy higher-order functions in order to facilitate function compositions, such asfxl.pipe
andfxl.compose
.
When we put our running example together, we actually see a relatively common pattern when building spreadsheets withfxl.js. In general, when building a spreadsheet usingfxl.js, we follow a number of high-level steps:
- prepare thedata to be used as cell values;
- build smallspreadsheet components with those values;
- prepare thestyles of each component;
- put together the styled components in theirrelative coordinates; and
- finally executing theIO operation.
import*asfxlfrom'@01group/fxl';// ------------------------------------------------------------------// data// ------------------------------------------------------------------constcosts=[{item:"Rent",cost:1000},{item:"Gas",cost:100},{item:"Food",cost:300},{item:"Gym",cost:50},];consttotalCost=costs.map((x)=>x.cost).reduce((x,y)=>x+y);// ------------------------------------------------------------------// spreadsheet components// ------------------------------------------------------------------constheaderCells=fxl.rowToCells(["Item","Cost"]);constbodyCells=fxl.recordsToCells(["item","cost"],costs);consttotalCells=fxl.rowToCells(["Total",totalCost]);constallCells=fxl.concatBelow(headerCells,bodyCells,totalCells);// ------------------------------------------------------------------// styles// ------------------------------------------------------------------functionsetHeaderStyle(cell:fxl.Cell):fxl.Cell{returnfxl.pipe(cell,fxl.setBold(true),fxl.setSolidFg('light_gray'));}functionsetTotalStyle(cell:fxl.Cell):fxl.Cell{returnfxl.pipe(cell,setHeaderStyle,fxl.setFontColor('dark_red'));}functionsetBodyStyle(cell:fxl.Cell):fxl.Cell{if(cell.coord.col==0){returnfxl.setItalic(true)(cell);}else{returncell;}}// ------------------------------------------------------------------// relative coordinates// ------------------------------------------------------------------constallCells=fxl.concatBelow(headerCells.map(setHeaderStyle),bodyCells.map(setBodyStyle),totalCells.map(setTotalStyle)).map(fxl.setHorizontalAlignement('center'));// ------------------------------------------------------------------// IO// ------------------------------------------------------------------awaitfxl.writeXlsx(allCells,'costs.xlsx')
See also theinventory-spreadsheet walkthrough and itsaccompanying script for a more detailed example based on a real use case.
- 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 alsoExcelJS' known issues.
fxl.js is very much a work-in-progress. Whilst it is being used in production atZero 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 thecontributing document.
- fxl - composable data-oriented spreadsheet library for Clojure is a talk by one of the authors of
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". Seehere for more information.
About
ƛ fxl.js is a data-oriented JavaScript spreadsheet library. It provides a way to build spreadsheets using modular, lego-like blocks.