Sheets in Angular Sites
Angular is a JS library for building user interfaces.1
SheetJS is a JavaScript library for reading and writing data from spreadsheets.
This demo uses Angular and SheetJS to process and generate spreadsheets. We'll explore how to load SheetJS in Angular projects and compare common state models and data flow strategies.
This demo focuses on Angular concepts. Other demos cover general deployments:
Angular tooling uses native NodeJS modules. There are a number of issues when trying to run Angular projects with different NodeJS versions. These issues should be directed to the Angular project.
Angular CLI enables telemetry by default. When using a recent version, disable analytics globally through the CLI tool before creating a new project:
npx @angular/cli analytics disable -g
(If prompted to share data, type N
and press Enter)
Installation​
The "Frameworks" section covers
installation with pnpm
and other package managers.
The library can be imported directly from JS or TS code with:
import { read, utils, writeFile } from 'xlsx';
Internal State​
The various SheetJS APIs work with various data shapes. The preferred state depends on the application.
Angular 17 broke backwards compatibility with projects using Angular 2 - 16.
Despite the Angular turmoil, SheetJS plays nice with each version of Angular.
When relevant, code snippets for Angular 17 and Angular 2 - 16 are included. The "Angular 2-16" and "Angular 17+" tabs change the displayed code blocks
Array of Objects​
Typically, some users will create a spreadsheet with source data that should be loaded into the site. This sheet will have known columns.
State​
The example presidents sheet has one header row with "Name" and "Index" columns. The natural JS representation is an object for each row, using the values in the first rows as keys:
Spreadsheet | State |
---|---|
|
This data is typically stored as an array of objects in the component class:
import { Component } from '@angular/core';
@Component({ /* ... component configuration options ... */ })
export class AppComponent {
/* the component state is an array of objects */
rows: any[] = [ { Name: "SheetJS", Index: 0 }];
}
When the spreadsheet header row is known ahead of time, row typing is possible:
import { Component } from '@angular/core';
interface President {
Name: string;
Index: number;
}
@Component({ /* ... component configuration options ... */ })
export class AppComponent {
/* the component state is an array of presidents */
rows: President[] = [ { Name: "SheetJS", Index: 0 }];
}
The types are informative. They do not enforce that worksheets include the named columns. A runtime data validation library should be used to verify the dataset.
When the file header is not known in advance, any
should be used.
Updating State​
The SheetJS read
and sheet_to_json
functions simplify state updates. They are best used in the function bodies of
ngOnInit
2 and event handlers.
A ngOnInit
method can download and update state when a person loads the site:
flowchart LR
url[(Remote\nFile)]
ab[(Data\nArrayBuffer)]
wb(SheetJS\nWorkbook)
ws(SheetJS\nWorksheet)
aoo(array of\nobjects)
state((component\nstate))
url --> |fetch\n\n| ab
ab --> |read\n\n| wb
wb --> |wb.Sheets\nselect sheet| ws
ws --> |sheet_to_json\n\n| aoo
aoo --> |setPres\nfrom `setState`| state
import { Component } from '@angular/core';
import { read, utils } from 'xlsx';
interface President { Name: string; Index: number };
@Component({ /* ... component configuration options ... */ })
export class AppComponent {
rows: President[] = [ { Name: "SheetJS", Index: 0 }];
ngOnInit(): void { (async() => {
/* Download from https://docs.sheetjs.com/pres.numbers */
const f = await fetch("https://docs.sheetjs.com/pres.numbers");
const ab = await f.arrayBuffer();
/* parse workbook */
const wb = read(ab);
/* generate array of objects from first worksheet */
const ws = wb.Sheets[wb.SheetNames[0]]; // get the first worksheet
const data = utils.sheet_to_json<President>(ws); // generate objects
/* update data */
this.rows = data;
})(); }
}
Rendering Data​
Components typically render HTML tables from arrays of objects. The <tr>
table
row elements are typically generated by mapping over the state array, as shown
in the example template.
- Angular 2-16
- Angular 17+
<div class="content" role="main"><table>
<thead><tr><th>Name</th><th>Index</th></tr></thead>
<tbody>
<tr *ngFor="let row of rows">
<td>{{row.Name}}</td>
<td>{{row.Index}}</td>
</tr>
</tbody>
</table></div>
<div class="content" role="main"><table>
<thead><tr><th>Name</th><th>Index</th></tr></thead>
<tbody>
@for(row of rows; track $index) { <tr>
<td>{{row.Name}}</td>
<td>{{row.Index}}</td>
</tr> }
</tbody>
</table></div>
Exporting Data​
The writeFile
and json_to_sheet
functions simplify exporting data. They are best used in the function bodies of
event handlers attached to button or other elements.
A callback can generate a local file when a user clicks a button:
flowchart LR
state((component\nstate))
ws(SheetJS\nWorksheet)
wb(SheetJS\nWorkbook)
file[(XLSX\nexport)]
state --> |json_to_sheet\n\n| ws
ws --> |book_new\nbook_append_sheet| wb
wb --> |writeFile\n\n| file
import { Component } from '@angular/core';
import { utils, writeFileXLSX } from 'xlsx';
interface President { Name: string; Index: number };
@Component({ /* ... component configuration options ... */ })
export class AppComponent {
rows: President[] = [ { Name: "SheetJS", Index: 0 }];
/* get state data and export to XLSX */
onSave(): void {
/* generate worksheet from state */
const ws = utils.json_to_sheet(this.rows);
/* create workbook and append worksheet */
const wb = utils.book_new();
utils.book_append_sheet(wb, ws, "Data");
/* export to XLSX */
writeFileXLSX(wb, "SheetJSAngularAoO.xlsx");
}
}
Complete Component​
This complete component example fetches a test file and displays the contents in a HTML table. When the export button is clicked, a callback will export a file:
- Angular 2-16
- Angular 17+
import { Component } from '@angular/core';
import { read, utils, writeFileXLSX } from 'xlsx';
interface President { Name: string; Index: number };
@Component({
selector: 'app-root',
template: `
<div class="content" role="main"><table>
<thead><tr><th>Name</th><th>Index</th></tr></thead>
<tbody>
<tr *ngFor="let row of rows">
<td>{{row.Name}}</td>
<td>{{row.Index}}</td>
</tr>
</tbody><tfoot>
<button (click)="onSave()">Export XLSX</button>
</tfoot>
</table></div>
`
})
export class AppComponent {
rows: President[] = [ { Name: "SheetJS", Index: 0 }];
ngOnInit(): void { (async() => {
/* Download from https://docs.sheetjs.com/pres.numbers */
const f = await fetch("https://docs.sheetjs.com/pres.numbers");
const ab = await f.arrayBuffer();
/* parse workbook */
const wb = read(ab);
/* update data */
this.rows = utils.sheet_to_json<President>(wb.Sheets[wb.SheetNames[0]]);
})(); }
/* get state data and export to XLSX */
onSave(): void {
const ws = utils.json_to_sheet(this.rows);
const wb = utils.book_new();
utils.book_append_sheet(wb, ws, "Data");
writeFileXLSX(wb, "SheetJSAngularAoO.xlsx");
}
}
import { Component } from '@angular/core';
import { read, utils, writeFileXLSX } from 'xlsx';
interface President { Name: string; Index: number };
@Component({
selector: 'app-root',
standalone: true,
template: `
<div class="content" role="main"><table>
<thead><tr><th>Name</th><th>Index</th></tr></thead>
<tbody>
@for(row of rows; track $index) {
<tr>
<td>{{row.Name}}</td>
<td>{{row.Index}}</td>
</tr>
}
</tbody><tfoot>
<button (click)="onSave()">Export XLSX</button>
</tfoot>
</table></div>
`
})
export class AppComponent {
rows: President[] = [ { Name: "SheetJS", Index: 0 }];
ngOnInit(): void { (async() => {
/* Download from https://docs.sheetjs.com/pres.numbers */
const f = await fetch("https://docs.sheetjs.com/pres.numbers");
const ab = await f.arrayBuffer();
/* parse workbook */
const wb = read(ab);
/* update data */
this.rows = utils.sheet_to_json<President>(wb.Sheets[wb.SheetNames[0]]);
})(); }
/* get state data and export to XLSX */
onSave(): void {
const ws = utils.json_to_sheet(this.rows);
const wb = utils.book_new();
utils.book_append_sheet(wb, ws, "Data");
writeFileXLSX(wb, "SheetJSAngularAoO.xlsx");
}
}
How to run the example (click to hide)
This demo was tested in the following environments:
Angular | Date |
---|---|
17.3.0 | 2024-03-13 |
16.2.12 | 2024-03-13 |
- Disable telemetry:
npx @angular/cli analytics disable -g
- Create a new project:
npx @angular/cli@17.3.0 new --minimal --defaults --no-interactive sheetjs-angular
The @angular/cli
version controls the project version of Angular. For example,
the following command uses Angular 16.2.12:
npx @angular/cli@16.2.12 new --minimal --defaults --no-interactive sheetjs-angular
- Install the SheetJS dependency and start the dev server:
cd sheetjs-angular
npm i
npm i --save https://cdn.sheetjs.com/xlsx-0.20.3/xlsx-0.20.3.tgz
npx @angular/cli analytics disable
npm start
-
Open a web browser and access the displayed URL (
http://localhost:4200
) -
In the previous
src/app/app.component.ts
code snippet, select the tab for the appropriate version of Angular ("Angular 2-16" or "Angular 17+"), copy the code contents and replacesrc/app/app.component.ts
in the project.
The page will refresh and show a table with an Export button. Click the button
and the page will attempt to download SheetJSAngularAoO.xlsx
. Open the file
with a spreadsheet editor.
- Stop the dev server and build the site:
npm run build
To test the generated site, start a web server:
- Angular 2-16
- Angular 17+
npx -y http-server dist/sheetjs-angular/
npx -y http-server dist/sheetjs-angular/browser/
Access the displayed URL (typically http://localhost:8080
) with a web browser
to test the bundled site.
HTML​
The main disadvantage of the Array of Objects approach is the specific nature of the columns. For more general use, passing around an Array of Arrays works. However, this does not handle merge cells5 well!
The sheet_to_html
function generates HTML that is aware of merges and other
worksheet features. The generated HTML does not contain any <script>
tags,
and should therefore be safe to pass to an innerHTML
-bound variable, but the
DomSanitizer
approach6 is strongly recommended:
- Angular 2-16
- Angular 17+
import { Component, ElementRef, ViewChild } from '@angular/core';
import { DomSanitizer, SafeHtml } from '@angular/platform-browser';
import { read, utils, writeFileXLSX } from 'xlsx';
@Component({
selector: 'app-root',
template: `<div class="content" role="main" [innerHTML]="html" #tableau></div>
<button (click)="onSave()">Export XLSX</button>`
})
export class AppComponent {
constructor(private sanitizer: DomSanitizer) {}
html: SafeHtml = "";
@ViewChild('tableau') tabeller!: ElementRef<HTMLDivElement>;
ngOnInit(): void { (async() => {
/* Download from https://docs.sheetjs.com/pres.numbers */
const f = await fetch("https://docs.sheetjs.com/pres.numbers");
const ab = await f.arrayBuffer();
/* parse workbook */
const wb = read(ab);
/* update data */
const h = utils.sheet_to_html(wb.Sheets[wb.SheetNames[0]]);
this.html = this.sanitizer.bypassSecurityTrustHtml(h);
})(); }
/* get live table and export to XLSX */
onSave(): void {
const elt = this.tabeller.nativeElement.getElementsByTagName("TABLE")[0];
const wb = utils.table_to_book(elt);
writeFileXLSX(wb, "SheetJSAngularHTML.xlsx");
}
}
import { Component, ElementRef, ViewChild } from '@angular/core';
import { DomSanitizer, SafeHtml } from '@angular/platform-browser';
import { read, utils, writeFileXLSX } from 'xlsx';
@Component({
selector: 'app-root',
standalone: true,
template: `<div class="content" role="main" [innerHTML]="html" #tableau></div>
<button (click)="onSave()">Export XLSX</button>`
})
export class AppComponent {
constructor(private sanitizer: DomSanitizer) {}
html: SafeHtml = "";
@ViewChild('tableau') tabeller!: ElementRef<HTMLDivElement>;
ngOnInit(): void { (async() => {
/* Download from https://docs.sheetjs.com/pres.numbers */
const f = await fetch("https://docs.sheetjs.com/pres.numbers");
const ab = await f.arrayBuffer();
/* parse workbook */
const wb = read(ab);
/* update data */
const h = utils.sheet_to_html(wb.Sheets[wb.SheetNames[0]]);
this.html = this.sanitizer.bypassSecurityTrustHtml(h);
})(); }
/* get live table and export to XLSX */
onSave(): void {
const elt = this.tabeller.nativeElement.getElementsByTagName("TABLE")[0];
const wb = utils.table_to_book(elt);
writeFileXLSX(wb, "SheetJSAngularHTML.xlsx");
}
}
How to run the example (click to hide)
This demo was tested in the following environments:
Angular | Date |
---|---|
17.3.0 | 2024-03-13 |
16.2.12 | 2024-03-13 |
- Disable telemetry:
npx @angular/cli analytics disable -g
- Create a new project:
npx @angular/cli@17.3.0 new --minimal --defaults --no-interactive sheetjs-angular
The @angular/cli
version controls the project version of Angular. For example,
the following command uses Angular 16.2.12:
npx @angular/cli@16.2.12 new --minimal --defaults --no-interactive sheetjs-angular
- Install the SheetJS dependency and start the dev server:
cd sheetjs-angular
npm i
npm i --save https://cdn.sheetjs.com/xlsx-0.20.3/xlsx-0.20.3.tgz
npx @angular/cli analytics disable
npm start
-
Open a web browser and access the displayed URL (
http://localhost:4200
) -
In the previous
src/app/app.component.ts
code snippet, select the tab for the appropriate version of Angular ("Angular 2-16" or "Angular 17+"), copy the code contents and replacesrc/app/app.component.ts
in the project.
The page will refresh and show a table with an Export button. Click the button
and the page will attempt to download SheetJSAngularHTML.xlsx
. Open the file
with a spreadsheet editor.
- Stop the dev server and build the site:
npm run build
To test the generated site, start a web server:
- Angular 2-16
- Angular 17+
npx -y http-server dist/sheetjs-angular/
npx -y http-server dist/sheetjs-angular/browser/
Access http://localhost:8080
with a web browser to test the bundled site.
Rows and Columns​
Some data grids and UI components split worksheet state in two parts: an array of column attribute objects and an array of row objects. The former is used to generate column headings and for indexing into the row objects.
The safest approach is to use an array of arrays for state and to generate column objects that map to A1-Style column headers.
ngx-datatable
uses prop
as the key and name
for the column label:
/* rows are generated with a simple array of arrays */
this.rows = utils.sheet_to_json(worksheet, { header: 1 });
/* column objects are generated based on the worksheet range */
const range = utils.decode_range(ws["!ref"]||"A1");
this.columns = Array.from({ length: range.e.c + 1 }, (_, i) => ({
/* for an array of arrays, the keys are "0", "1", "2", ... */
prop: String(i),
/* column labels: encode_col translates 0 -> "A", 1 -> "B", 2 -> "C", ... */
name: XLSX.utils.encode_col(i)
}));
Older Versions​
This demo is included for legacy deployments. There are incompatibilities with different NodeJS and other ecosystem versions. Issues should be raised with Google and the Angular team.
The newest versions of NodeJS will not work with older Angular projects!
The Angular tooling does not provide a command to switch between versions!
This is a known Angular problem.
To work around this, SheetJSAngular.zip
is a skeleton project designed to play nice with each Angular version.
Strategies​
Internal State​
This demo uses an array of arrays as the internal state:
export class SheetJSComponent {
data: any[][] = [ [1, 2], [3, 4] ];
// ...
}
Nested ngFor
in a template can loop across the rows and cells:
<table class="sjs-table">
<tr *ngFor="let row of data">
<td *ngFor="let val of row">{{val}}</td>
</tr>
</table>
Reading Data​
For legacy deployments, the best ingress is a standard HTML INPUT file element:
<input type="file" (change)="onFileChange($event)" multiple="false" />
In the component, the event is a standard file event. Using a FileReader
has
broad support compared to the modern Blob#arrayBuffer
approach:
onFileChange(evt: any) {
/* wire up file reader */
const target: DataTransfer = <DataTransfer>(evt.target);
if (target.files.length !== 1) throw new Error('Cannot use multiple files');
const reader: FileReader = new FileReader();
reader.onload = (e: any) => {
/* read workbook */
const ab: ArrayBuffer = e.target.result;
const wb: WorkBook = read(ab);
/* grab first sheet */
const wsname: string = wb.SheetNames[0];
const ws: WorkSheet = wb.Sheets[wsname];
/* save data */
this.data = <AOA>(utils.sheet_to_json(ws, {header: 1}));
};
reader.readAsArrayBuffer(target.files[0]);
}
Writing Data​
The demo uses an HTML5 button in the template:
<button (click)="export()">Export!</button>
In the component, aoa_to_sheet
is used to generate the worksheet:
export(): void {
/* generate worksheet */
const ws: WorkSheet = utils.aoa_to_sheet(this.data);
/* generate workbook and add the worksheet */
const wb: WorkBook = utils.book_new();
utils.book_append_sheet(wb, ws, 'Sheet1');
/* save to file */
writeFile(wb, "SheetJS.xlsx");
}
SystemJS​
The default angular-cli
configuration requires no additional configuration.
Some deployments use the SystemJS loader, which does require configuration. The SystemJS demo includes the required settings.
Legacy Demo​
- Download and unzip
SheetJSAngular.zip
:
curl -LO https://docs.sheetjs.com/angular/SheetJSAngular.zip
unzip SheetJSAngular.zip
cd SheetJSAngular
- Download the files for the desired Angular version:
- 2
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
package.json-ng2
save topackage.json
polyfills.ts-ng2
save tosrc/polyfills.ts
curl -o package.json -L https://docs.sheetjs.com/angular/versions/package.json-ng2
curl -o src/polyfills.ts -L https://docs.sheetjs.com/angular/versions/polyfills.ts-ng2
package.json-ng4
save topackage.json
polyfills.ts-ng4
save tosrc/polyfills.ts
curl -o package.json -L https://docs.sheetjs.com/angular/versions/package.json-ng4
curl -o src/polyfills.ts -L https://docs.sheetjs.com/angular/versions/polyfills.ts-ng4
package.json-ng5
save topackage.json
polyfills.ts-ng5
save tosrc/polyfills.ts
curl -o package.json -L https://docs.sheetjs.com/angular/versions/package.json-ng5
curl -o src/polyfills.ts -L https://docs.sheetjs.com/angular/versions/polyfills.ts-ng5
package.json-ng6
save topackage.json
polyfills.ts-ng6
save tosrc/polyfills.ts
angular.json-ng6
save toangular.json
curl -o package.json -L https://docs.sheetjs.com/angular/versions/package.json-ng6
curl -o src/polyfills.ts -L https://docs.sheetjs.com/angular/versions/polyfills.ts-ng6
curl -o angular.json -L https://docs.sheetjs.com/angular/versions/angular.json-ng6
package.json-ng7
save topackage.json
polyfills.ts-ng7
save tosrc/polyfills.ts
angular.json-ng7
save toangular.json
curl -o package.json -L https://docs.sheetjs.com/angular/versions/package.json-ng7
curl -o src/polyfills.ts -L https://docs.sheetjs.com/angular/versions/polyfills.ts-ng7
curl -o angular.json -L https://docs.sheetjs.com/angular/versions/angular.json-ng7
package.json-ng8
save topackage.json
polyfills.ts-ng8
save tosrc/polyfills.ts
angular.json-ng8
save toangular.json
tsconfig.app.json-ng8
save totsconfig.app.json
curl -o package.json -L https://docs.sheetjs.com/angular/versions/package.json-ng8
curl -o src/polyfills.ts -L https://docs.sheetjs.com/angular/versions/polyfills.ts-ng8
curl -o angular.json -L https://docs.sheetjs.com/angular/versions/angular.json-ng8
curl -o tsconfig.app.json -L https://docs.sheetjs.com/angular/versions/tsconfig.app.json-ng8
package.json-ng9
save topackage.json
polyfills.ts-ng9
save tosrc/polyfills.ts
angular.json-ng9
save toangular.json
tsconfig.app.json-ng9
save totsconfig.app.json
curl -o package.json -L https://docs.sheetjs.com/angular/versions/package.json-ng9
curl -o src/polyfills.ts -L https://docs.sheetjs.com/angular/versions/polyfills.ts-ng9
curl -o angular.json -L https://docs.sheetjs.com/angular/versions/angular.json-ng9
curl -o tsconfig.app.json -L https://docs.sheetjs.com/angular/versions/tsconfig.app.json-ng9
package.json-ng10
save topackage.json
polyfills.ts-ng10
save tosrc/polyfills.ts
angular.json-ng10
save toangular.json
tsconfig.app.json-ng10
save totsconfig.app.json
curl -o package.json -L https://docs.sheetjs.com/angular/versions/package.json-ng10
curl -o src/polyfills.ts -L https://docs.sheetjs.com/angular/versions/polyfills.ts-ng10
curl -o angular.json -L https://docs.sheetjs.com/angular/versions/angular.json-ng10
curl -o tsconfig.app.json -L https://docs.sheetjs.com/angular/versions/tsconfig.app.json-ng10
package.json-ng11
save topackage.json
polyfills.ts-ng11
save tosrc/polyfills.ts
angular.json-ng11
save toangular.json
tsconfig.app.json-ng11
save totsconfig.app.json
curl -o package.json -L https://docs.sheetjs.com/angular/versions/package.json-ng11
curl -o src/polyfills.ts -L https://docs.sheetjs.com/angular/versions/polyfills.ts-ng11
curl -o angular.json -L https://docs.sheetjs.com/angular/versions/angular.json-ng11
curl -o tsconfig.app.json -L https://docs.sheetjs.com/angular/versions/tsconfig.app.json-ng11
package.json-ng12
save topackage.json
polyfills.ts-ng12
save tosrc/polyfills.ts
angular.json-ng12
save toangular.json
tsconfig.app.json-ng12
save totsconfig.app.json
curl -o package.json -L https://docs.sheetjs.com/angular/versions/package.json-ng12
curl -o src/polyfills.ts -L https://docs.sheetjs.com/angular/versions/polyfills.ts-ng12
curl -o angular.json -L https://docs.sheetjs.com/angular/versions/angular.json-ng12
curl -o tsconfig.app.json -L https://docs.sheetjs.com/angular/versions/tsconfig.app.json-ng12
package.json-ng13
save topackage.json
polyfills.ts-ng13
save tosrc/polyfills.ts
angular.json-ng13
save toangular.json
tsconfig.app.json-ng13
save totsconfig.app.json
curl -o package.json -L https://docs.sheetjs.com/angular/versions/package.json-ng13
curl -o src/polyfills.ts -L https://docs.sheetjs.com/angular/versions/polyfills.ts-ng13
curl -o angular.json -L https://docs.sheetjs.com/angular/versions/angular.json-ng13
curl -o tsconfig.app.json -L https://docs.sheetjs.com/angular/versions/tsconfig.app.json-ng13
package.json-ng14
save topackage.json
polyfills.ts-ng14
save tosrc/polyfills.ts
angular.json-ng14
save toangular.json
tsconfig.app.json-ng14
save totsconfig.app.json
curl -o package.json -L https://docs.sheetjs.com/angular/versions/package.json-ng14
curl -o src/polyfills.ts -L https://docs.sheetjs.com/angular/versions/polyfills.ts-ng14
curl -o angular.json -L https://docs.sheetjs.com/angular/versions/angular.json-ng14
curl -o tsconfig.app.json -L https://docs.sheetjs.com/angular/versions/tsconfig.app.json-ng14
- Install project and dependencies:
npm i
npm i -S https://cdn.sheetjs.com/xlsx-0.20.3/xlsx-0.20.3.tgz
- Start a local server with
npm start
The traditional site URL is http://localhost:4200/
. Open the page with a web
browser and open the console. In the "Elements" tab, the app-root
element
will have an ng-version
attribute.
- Build the app with
npm run build
Footnotes​
-
The main website for Angular versions 2-16 is https://angular.io/ . The project moved to a new domain https://angular.dev/ during the Angular 17 launch. ↩
-
See
OnInit
in the Angular 2-16 docs or Angular 17 docs ↩ -
See "Merged Cells" in "SheetJS Data Model" for more details. ↩
-
See
DomSanitizer
in the Angular 2-16 docs or Angular 17 docs ↩