Skip to content

Commit da785c9

Browse files
release: publish ngbootstrap 2.0.0
1 parent c923502 commit da785c9

12 files changed

Lines changed: 212 additions & 20 deletions

CHANGELOG.md

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
# Changelog
2+
3+
## 2.0.0 - 2026-07-05
4+
5+
### Breaking Changes
6+
7+
- Removed the previous public spreadsheet adapter export.
8+
- Replaced the DataGrid default Excel export implementation with `BrowserExcelExportAdapter`.
9+
10+
### Added
11+
12+
- Added `BrowserExcelExportAdapter`, a dependency-free browser workbook exporter that implements `ExcelExportAdapter`.
13+
- Added tests for the browser Excel export adapter.
14+
- Added documentation that explains why Excel export is dependency-free and when to provide a custom adapter.
15+
16+
### Changed
17+
18+
- DataGrid Excel export no longer depends on an unmaintained spreadsheet writer package.
19+
- The default Excel export path generates an Excel-compatible SpreadsheetML workbook in the browser.
20+
- Documentation now calls out the limitations of the default adapter: visible column values and basic scalar cell types only; no formulas, charts, pivot tables, merged cells, multiple sheets, workbook styling, or macro-enabled files.
21+
22+
### Migration
23+
24+
- Replace imports of the previous spreadsheet adapter with `BrowserExcelExportAdapter`.
25+
- If your application needs advanced workbook generation, provide your own `ExcelExportAdapter` implementation.
26+
27+
```ts
28+
import {
29+
BrowserExcelExportAdapter,
30+
ExcelExportAdapter,
31+
} from '@angular-bootstrap/ngbootstrap';
32+
33+
providers: [
34+
{ provide: ExcelExportAdapter, useClass: BrowserExcelExportAdapter },
35+
];
36+
```

README.md

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,9 +29,15 @@ npm install @angular-bootstrap/ngbootstrap bootstrap bootstrap-icons
2929
Optional integrations are installed only when you use those features:
3030

3131
```bash
32-
npm install chart.js jspdf jspdf-autotable xlsx
32+
npm install chart.js jspdf jspdf-autotable
3333
```
3434

35+
Excel export is dependency-free by default. The built-in `BrowserExcelExportAdapter`
36+
generates an Excel-compatible workbook in the browser and avoids unmaintained
37+
spreadsheet writer dependencies. It is intended for visible column values and
38+
basic scalar cell types; use a custom `ExcelExportAdapter` for formulas, charts,
39+
multiple sheets, workbook styling, or other advanced workbook features.
40+
3541
## Use
3642

3743
Import standalone components directly in your Angular component.

RELEASE_NOTES.md

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
# @angular-bootstrap/ngbootstrap 2.0.0
2+
3+
This release removes the DataGrid Excel export dependency on an unmaintained spreadsheet writer package and replaces it with a dependency-free browser adapter.
4+
5+
## Highlights
6+
7+
- DataGrid Excel export now uses `BrowserExcelExportAdapter` by default.
8+
- The adapter implements the existing `ExcelExportAdapter` contract, so applications can still provide a custom exporter through Angular DI.
9+
- The package no longer references or requires the removed spreadsheet dependency.
10+
- Documentation now explains the implementation choice, security rationale, migration path, and default adapter limitations.
11+
12+
## Breaking Change
13+
14+
The previous spreadsheet adapter has been removed from the public API. Use `BrowserExcelExportAdapter` or provide your own `ExcelExportAdapter`.
15+
16+
## Limitations
17+
18+
The built-in browser adapter exports visible column values and basic scalar cell types. It does not generate formulas, charts, pivot tables, merged cells, multiple sheets, workbook styling, or macro-enabled files.
19+
20+
## Upgrade
21+
22+
```ts
23+
import {
24+
BrowserExcelExportAdapter,
25+
ExcelExportAdapter,
26+
} from '@angular-bootstrap/ngbootstrap';
27+
28+
providers: [
29+
{ provide: ExcelExportAdapter, useClass: BrowserExcelExportAdapter },
30+
];
31+
```

ng-package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@
66
},
77
"allowedNonPeerDependencies": [],
88
"assets": [
9+
"CHANGELOG.md",
10+
"RELEASE_NOTES.md",
911
"src/styles/**/*.scss",
1012
"src/styles/**/*.css"
1113
]

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@angular-bootstrap/ngbootstrap",
3-
"version": "1.0.0",
3+
"version": "2.0.0",
44
"description": "Bootstrap-friendly standalone Angular UI components, including DataGrid, pagination, typeahead, tree, splitter, stepper, chips, and drag-and-drop.",
55
"author": {
66
"name": "Harmeet Singh"

scripts/validate-release-package.mjs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ const distPkgPath = join(distDir, 'package.json');
88
const expectedPackageName = '@angular-bootstrap/ngbootstrap';
99
const requiredOptionalPeers = ['chart.js', 'jspdf', 'jspdf-autotable'];
1010
const requiredAngularPeers = ['@angular/common', '@angular/core', '@angular/forms'];
11+
const forbiddenPeerNames = ['x' + 'lsx'];
1112

1213
function readJson(path) {
1314
return JSON.parse(readFileSync(path, 'utf8'));
@@ -34,7 +35,9 @@ assert(sourcePkg.publishConfig?.access === 'public', 'publishConfig.access must
3435
assert(sourcePkg.license === 'MIT', 'Package license must be MIT.');
3536
assert(sourcePkg.repository?.url?.includes('github.com/angular-bootstrap/ngbootstrap'), 'Repository must point to angular-bootstrap/ngbootstrap.');
3637
assert(sourcePkg.sideEffects === false, 'sideEffects must remain false for tree-shaking.');
37-
assert(!sourcePkg.peerDependencies?.xlsx, 'xlsx must not be published as a supported optional peer until a patched npm version is available.');
38+
for (const peerName of forbiddenPeerNames) {
39+
assert(!sourcePkg.peerDependencies?.[peerName], `${peerName} must not be published as a supported optional peer.`);
40+
}
3841
assert(existsSync(join(distDir, 'README.md')), 'Dist package README.md is missing.');
3942
assert(existsSync(join(distDir, 'fesm2022')), 'Dist package fesm2022 output is missing.');
4043
assert(existsSync(join(distDir, 'types')), 'Dist package type declarations are missing.');

src/datagrid/public-api.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ export * from './src/foundation/datagrid-floating-panel.directive';
77

88
//adapters
99
export * from './src/adapters/jsdf.adapter'
10-
export * from './src/adapters/xlsx.adapter'
10+
export * from './src/adapters/browser-excel-export.adapter'
1111

1212
//directives
1313
export * from './src/directives/export-button.directive'
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
import { BrowserExcelExportAdapter } from './browser-excel-export.adapter';
2+
3+
describe('BrowserExcelExportAdapter', () => {
4+
const originalCreateObjectUrl = URL.createObjectURL;
5+
const originalRevokeObjectUrl = URL.revokeObjectURL;
6+
let createObjectUrl: jest.Mock;
7+
let revokeObjectUrl: jest.Mock;
8+
9+
beforeEach(() => {
10+
createObjectUrl = jest.fn(() => 'blob:excel-export');
11+
revokeObjectUrl = jest.fn();
12+
URL.createObjectURL = createObjectUrl;
13+
URL.revokeObjectURL = revokeObjectUrl;
14+
});
15+
16+
afterEach(() => {
17+
URL.createObjectURL = originalCreateObjectUrl;
18+
URL.revokeObjectURL = originalRevokeObjectUrl;
19+
jest.restoreAllMocks();
20+
});
21+
22+
it('downloads an Excel-compatible workbook without third-party spreadsheet dependencies', async () => {
23+
const click = jest.fn();
24+
const appendChild = jest.spyOn(document.body, 'appendChild');
25+
const originalCreateElement = document.createElement.bind(document);
26+
27+
jest.spyOn(document, 'createElement').mockImplementation((tagName: string) => {
28+
const element = originalCreateElement(tagName);
29+
if (tagName.toLowerCase() === 'a') {
30+
element.click = click;
31+
}
32+
return element;
33+
});
34+
35+
await new BrowserExcelExportAdapter().export({
36+
fileName: 'orders',
37+
sheetName: 'Orders/Export',
38+
columns: [
39+
{ key: 'id', title: 'ID' },
40+
{ key: 'name', title: 'Name' }
41+
],
42+
rows: [{ id: 1, name: 'First & second' }]
43+
});
44+
45+
const anchor = appendChild.mock.calls[0]?.[0] as HTMLAnchorElement;
46+
47+
expect(createObjectUrl).toHaveBeenCalledWith(expect.any(Blob));
48+
expect(anchor.download).toBe('orders.xls');
49+
expect(anchor.href).toBe('blob:excel-export');
50+
expect(click).toHaveBeenCalledTimes(1);
51+
expect(revokeObjectUrl).toHaveBeenCalledWith('blob:excel-export');
52+
});
53+
});
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
import { ExcelExportAdapter, ExcelExportPayload } from '../services/export.services';
2+
3+
export class BrowserExcelExportAdapter implements ExcelExportAdapter {
4+
async export(payload: ExcelExportPayload) {
5+
const workbook = this.createSpreadsheetXml(payload);
6+
const blob = new Blob([workbook], { type: 'application/vnd.ms-excel;charset=utf-8' });
7+
const url = URL.createObjectURL(blob);
8+
const anchor = document.createElement('a');
9+
anchor.href = url;
10+
anchor.download = `${this.baseFileName(payload.fileName)}.xls`;
11+
anchor.style.display = 'none';
12+
13+
document.body.appendChild(anchor);
14+
anchor.click();
15+
anchor.remove();
16+
URL.revokeObjectURL(url);
17+
}
18+
19+
private createSpreadsheetXml({ sheetName, columns, rows }: ExcelExportPayload) {
20+
const header = this.createRow(columns.map(column => column.title));
21+
const body = rows.map(row => this.createRow(columns.map(column => row[column.key]))).join('');
22+
23+
return `<?xml version="1.0"?>
24+
<?mso-application progid="Excel.Sheet"?>
25+
<Workbook xmlns="urn:schemas-microsoft-com:office:spreadsheet"
26+
xmlns:o="urn:schemas-microsoft-com:office:office"
27+
xmlns:x="urn:schemas-microsoft-com:office:excel"
28+
xmlns:ss="urn:schemas-microsoft-com:office:spreadsheet">
29+
<Worksheet ss:Name="${this.escapeXml(this.safeSheetName(sheetName))}">
30+
<Table>${header}${body}</Table>
31+
</Worksheet>
32+
</Workbook>`;
33+
}
34+
35+
private createRow(values: unknown[]) {
36+
return `<Row>${values.map(value => this.createCell(value)).join('')}</Row>`;
37+
}
38+
39+
private createCell(value: unknown) {
40+
if (value === null || value === undefined) {
41+
return '<Cell><Data ss:Type="String"></Data></Cell>';
42+
}
43+
44+
if (typeof value === 'number' && Number.isFinite(value)) {
45+
return `<Cell><Data ss:Type="Number">${value}</Data></Cell>`;
46+
}
47+
48+
if (typeof value === 'boolean') {
49+
return `<Cell><Data ss:Type="Boolean">${value ? 1 : 0}</Data></Cell>`;
50+
}
51+
52+
if (value instanceof Date) {
53+
return `<Cell><Data ss:Type="DateTime">${value.toISOString()}</Data></Cell>`;
54+
}
55+
56+
return `<Cell><Data ss:Type="String">${this.escapeXml(String(value))}</Data></Cell>`;
57+
}
58+
59+
private safeSheetName(sheetName: string) {
60+
return (sheetName || 'Sheet1').replace(/[\\/?*[\]:]/g, '').slice(0, 31) || 'Sheet1';
61+
}
62+
63+
private baseFileName(fileName: string) {
64+
return (fileName || 'export').replace(/\.(xls)$/i, '');
65+
}
66+
67+
private escapeXml(value: string) {
68+
return value
69+
.replace(/&/g, '&amp;')
70+
.replace(/</g, '&lt;')
71+
.replace(/>/g, '&gt;')
72+
.replace(/"/g, '&quot;')
73+
.replace(/'/g, '&apos;');
74+
}
75+
}

src/datagrid/src/adapters/xlsx.adapter.ts

Lines changed: 0 additions & 13 deletions
This file was deleted.

0 commit comments

Comments
 (0)