├── .gitignore ├── README.md ├── index.js ├── lib ├── excel.js └── node-excel-export.d.ts └── package.json /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | npm-debug.log 3 | nbproject 4 | .idea 5 | .vscode 6 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ### Node.JS Excel-Export 2 | 3 | Nice little module that is assisting when creating excel exports from datasets. It takes normal array-of-objects dataset plus a json report specification and builds excel(.xlsx) file. It supports styling and re-formating of the data on the fly. Check the example usage for more information. 4 | 5 | ### Installation 6 | ```bash 7 | npm install node-excel-export 8 | ``` 9 | 10 | ### Usage 11 | 12 | * Check [here](https://github.com/protobi/js-xlsx#cell-styles), for more styling 13 | ```javascript 14 | const excel = require('node-excel-export'); 15 | 16 | // You can define styles as json object 17 | const styles = { 18 | headerDark: { 19 | fill: { 20 | fgColor: { 21 | rgb: 'FF000000' 22 | } 23 | }, 24 | font: { 25 | color: { 26 | rgb: 'FFFFFFFF' 27 | }, 28 | sz: 14, 29 | bold: true, 30 | underline: true 31 | } 32 | }, 33 | cellPink: { 34 | fill: { 35 | fgColor: { 36 | rgb: 'FFFFCCFF' 37 | } 38 | } 39 | }, 40 | cellGreen: { 41 | fill: { 42 | fgColor: { 43 | rgb: 'FF00FF00' 44 | } 45 | } 46 | } 47 | }; 48 | 49 | //Array of objects representing heading rows (very top) 50 | const heading = [ 51 | [{value: 'a1', style: styles.headerDark}, {value: 'b1', style: styles.headerDark}, {value: 'c1', style: styles.headerDark}], 52 | ['a2', 'b2', 'c2'] // <-- It can be only values 53 | ]; 54 | 55 | //Here you specify the export structure 56 | const specification = { 57 | customer_name: { // <- the key should match the actual data key 58 | displayName: 'Customer', // <- Here you specify the column header 59 | headerStyle: styles.headerDark, // <- Header style 60 | cellStyle: function(value, row) { // <- style renderer function 61 | // if the status is 1 then color in green else color in red 62 | // Notice how we use another cell value to style the current one 63 | return (row.status_id == 1) ? styles.cellGreen : {fill: {fgColor: {rgb: 'FFFF0000'}}}; // <- Inline cell style is possible 64 | }, 65 | width: 120 // <- width in pixels 66 | }, 67 | status_id: { 68 | displayName: 'Status', 69 | headerStyle: styles.headerDark, 70 | cellFormat: function(value, row) { // <- Renderer function, you can access also any row.property 71 | return (value == 1) ? 'Active' : 'Inactive'; 72 | }, 73 | width: '10' // <- width in chars (when the number is passed as string) 74 | }, 75 | note: { 76 | displayName: 'Description', 77 | headerStyle: styles.headerDark, 78 | cellStyle: styles.cellPink, // <- Cell style 79 | width: 220 // <- width in pixels 80 | } 81 | } 82 | 83 | // The data set should have the following shape (Array of Objects) 84 | // The order of the keys is irrelevant, it is also irrelevant if the 85 | // dataset contains more fields as the report is build based on the 86 | // specification provided above. But you should have all the fields 87 | // that are listed in the report specification 88 | const dataset = [ 89 | {customer_name: 'IBM', status_id: 1, note: 'some note', misc: 'not shown'}, 90 | {customer_name: 'HP', status_id: 0, note: 'some note'}, 91 | {customer_name: 'MS', status_id: 0, note: 'some note', misc: 'not shown'} 92 | ] 93 | 94 | // Define an array of merges. 1-1 = A:1 95 | // The merges are independent of the data. 96 | // A merge will overwrite all data _not_ in the top-left cell. 97 | const merges = [ 98 | { start: { row: 1, column: 1 }, end: { row: 1, column: 10 } }, 99 | { start: { row: 2, column: 1 }, end: { row: 2, column: 5 } }, 100 | { start: { row: 2, column: 6 }, end: { row: 2, column: 10 } } 101 | ] 102 | 103 | // Create the excel report. 104 | // This function will return Buffer 105 | const report = excel.buildExport( 106 | [ // <- Notice that this is an array. Pass multiple sheets to create multi sheet report 107 | { 108 | name: 'Report', // <- Specify sheet name (optional) 109 | heading: heading, // <- Raw heading array (optional) 110 | merges: merges, // <- Merge cell ranges 111 | specification: specification, // <- Report specification 112 | data: dataset // <-- Report data 113 | } 114 | ] 115 | ); 116 | 117 | // You can then return this straight 118 | res.attachment('report.xlsx'); // This is sails.js specific (in general you need to set headers) 119 | return res.send(report); 120 | 121 | // OR you can save this buffer to the disk by creating a file. 122 | ``` 123 | 124 | #### Contributors 125 | | Contributor | Contribution | 126 | | --- | --- | 127 | | @jbogatay | Allow null values | 128 | | @frenchbread | Example update | 129 | | @fhemberger | Undefined header style | 130 | | @zeg-io Tony Archer | Cell Merging | 131 | | @martin-podlubny | Number and Dates custom formatting | 132 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const excel = require('./lib/excel') 4 | 5 | let buildExport = (params, options) => { 6 | if (!(params instanceof Array)) throw 'buildExport expects an array' 7 | 8 | let sheets = [] 9 | params.forEach(function (sheet, index) { 10 | let specification = sheet.specification 11 | let dataset = sheet.data 12 | let sheet_name = sheet.name || 'Sheet' + (index + 1) 13 | let data = [] 14 | let merges = sheet.merges 15 | let config = { 16 | cols: [] 17 | } 18 | 19 | if (!specification || !dataset) throw 'missing specification or dataset.' 20 | 21 | if (sheet.heading) { 22 | sheet.heading.forEach(function (row) { 23 | data.push(row) 24 | }) 25 | } 26 | 27 | //build the header row 28 | let header = [] 29 | for (let col in specification) { 30 | header.push({ 31 | value: specification[col].displayName, 32 | style: specification[col].headerStyle || '' 33 | }) 34 | 35 | if (specification[col].width) { 36 | if (Number.isInteger(specification[col].width)) { 37 | config.cols.push({ wpx: specification[col].width }) 38 | } else if (Number.isInteger(parseInt(specification[col].width))) { 39 | config.cols.push({ wch: specification[col].width }) 40 | } else { 41 | throw 'Provide column width as a number' 42 | } 43 | } else { 44 | config.cols.push({}) 45 | } 46 | 47 | } 48 | data.push(header) //Inject the header at 0 49 | 50 | dataset.forEach(record => { 51 | let row = [] 52 | for (let col in specification) { 53 | let cell_value = record[col] 54 | 55 | if (specification[col].cellFormat && typeof specification[col].cellFormat == 'function') { 56 | cell_value = specification[col].cellFormat(record[col], record) 57 | } 58 | 59 | if (specification[col].cellStyle && typeof specification[col].cellStyle == 'function') { 60 | cell_value = { 61 | value: cell_value, 62 | style: specification[col].cellStyle(record[col], record) 63 | } 64 | } else if (specification[col].cellStyle) { 65 | cell_value = { 66 | value: cell_value, 67 | style: specification[col].cellStyle 68 | } 69 | } 70 | row.push(cell_value) // Push new cell to the row 71 | } 72 | data.push(row) // Push new row to the sheet 73 | }) 74 | 75 | sheets.push({ 76 | name: sheet_name, 77 | data: data, 78 | merge: merges, 79 | config: config 80 | }) 81 | 82 | }) 83 | 84 | return excel.build(sheets, options) 85 | 86 | } 87 | 88 | module.exports = { 89 | buildExport 90 | } 91 | -------------------------------------------------------------------------------- /lib/excel.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const XLSX = require('xlsx-style'); 4 | 5 | function datenum(v, date1904) { 6 | if(date1904) v += 1462; 7 | let epoch = Date.parse(v); 8 | return (epoch - new Date(Date.UTC(1899, 11, 30))) / (24 * 60 * 60 * 1000); 9 | } 10 | 11 | function sheet_from_array_of_arrays(data, merges) { 12 | let ws = {}; 13 | let range = {s: {c:10000000, r:10000000}, e: {c:0, r:0}}; 14 | for(let R = 0; R !== data.length; ++R) { 15 | for(let C = 0; C !== data[R].length; ++C) { 16 | if(range.s.r > R) range.s.r = R; 17 | if(range.s.c > C) range.s.c = C; 18 | if(range.e.r < R) range.e.r = R; 19 | if(range.e.c < C) range.e.c = C; 20 | 21 | let cell; 22 | if(data[R][C] && typeof data[R][C] === 'object' && data[R][C].style && !(data[R][C] instanceof Date)) { 23 | cell = { 24 | v: data[R][C].value, 25 | s: data[R][C].style 26 | }; 27 | } else { 28 | cell = {v: data[R][C] }; 29 | } 30 | 31 | if(cell.v === null) continue; 32 | let cell_ref = XLSX.utils.encode_cell({c:C,r:R}); 33 | 34 | if(typeof cell.v === 'number') { 35 | cell.t = 'n'; 36 | if(data[R][C] && typeof data[R][C] === 'object' 37 | && data[R][C].style && typeof data[R][C].style === 'object' 38 | && data[R][C].style.numFmt) { 39 | cell.z = data[R][C].style.numFmt; 40 | } 41 | } 42 | else if(typeof cell.v === 'boolean') cell.t = 'b'; 43 | else if(cell.v instanceof Date) { 44 | cell.t = 'n'; 45 | if(data[R][C] && typeof data[R][C] === 'object' 46 | && data[R][C].style && typeof data[R][C].style === 'object' 47 | && data[R][C].style.numFmt) { 48 | cell.z = data[R][C].style.numFmt; 49 | } else { 50 | cell.z = XLSX.SSF._table[14]; 51 | } 52 | cell.v = datenum(cell.v); 53 | } 54 | else cell.t = 's'; 55 | 56 | ws[cell_ref] = cell; 57 | } 58 | } 59 | 60 | if (merges) { 61 | if (!ws['!merges']) ws['!merges'] = []; 62 | merges.forEach(function (merge) { 63 | ws['!merges'].push({ 64 | s: { 65 | r: merge.start.row - 1, 66 | c: merge.start.column - 1 67 | }, 68 | e: { 69 | r: merge.end.row - 1, 70 | c: merge.end.column - 1 71 | } 72 | }); 73 | }); 74 | } 75 | 76 | if(range.s.c < 10000000) ws['!ref'] = XLSX.utils.encode_range(range); 77 | return ws; 78 | } 79 | 80 | function Workbook() { 81 | if(!(this instanceof Workbook)) return new Workbook(); 82 | this.SheetNames = []; 83 | this.Sheets = {}; 84 | } 85 | 86 | module.exports = { 87 | parse: function(mixed, options) { 88 | let ws; 89 | if(typeof mixed === 'string') ws = XLSX.readFile(mixed, options); 90 | else ws = XLSX.read(mixed, options); 91 | return _.map(ws.Sheets, function(sheet, name) { 92 | return {name: name, data: XLSX.utils.sheet_to_json(sheet, {header: 1, raw: true})}; 93 | }); 94 | }, 95 | build: function(array, options) { 96 | let writeOptions = Object.assign({ 97 | bookType:'xlsx', 98 | bookSST: false, 99 | type:'binary' 100 | }, options); 101 | let wb = new Workbook(); 102 | array.forEach(function(worksheet) { 103 | let name = worksheet.name || 'Sheet'; 104 | let data = sheet_from_array_of_arrays(worksheet.data || [], worksheet.merge); 105 | wb.SheetNames.push(name); 106 | wb.Sheets[name] = data; 107 | 108 | if(worksheet.config.cols) { 109 | wb.Sheets[name]['!cols'] = worksheet.config.cols 110 | } 111 | 112 | }); 113 | 114 | let data = XLSX.write(wb, writeOptions); 115 | if(!data) return false; 116 | let buffer = new Buffer(data, 'binary'); 117 | return buffer; 118 | 119 | } 120 | }; 121 | -------------------------------------------------------------------------------- /lib/node-excel-export.d.ts: -------------------------------------------------------------------------------- 1 | declare module 'node-excel-export' { 2 | 3 | type CellStyle = { 4 | fill?: { 5 | fgColor: { 6 | rgb: string; 7 | } 8 | }, 9 | font?: { 10 | color?: { 11 | rgb: string; 12 | }, 13 | sz?: number; 14 | bold?: boolean; 15 | underline?: boolean; 16 | }, 17 | alignment?: { 18 | horizontal?: 'left' | 'center' | 'right', 19 | vertical?: 'top' | 'center' | 'bottom', 20 | }, 21 | }; 22 | 23 | type Heading = [{ 24 | value: string; 25 | style: CellStyle 26 | }| string[]][] 27 | 28 | type Merges = { 29 | start: { 30 | row: number; 31 | column: number; 32 | }, 33 | end: { 34 | row: number; 35 | column: number; 36 | }; 37 | }[] 38 | 39 | type Specification = { 40 | [CellName in keyof TRowData]: { 41 | displayName: string; 42 | headerStyle?: ((value: TRowData[CellName], row: TRowData) => CellStyle) | CellStyle; 43 | cellStyle?: ((value: TRowData[CellName], row: TRowData) => CellStyle) | CellStyle; 44 | cellFormat?: (value: TRowData[CellName], row: TRowData) => string; 45 | width: string | number; 46 | } 47 | } 48 | 49 | function buildExport(sheets: { 50 | name?: string; 51 | heading?: Heading; 52 | merges?: Merges; 53 | specification: Specification; 54 | data: TRowData[]; 55 | }[]): Buffer; 56 | 57 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "node-excel-export", 3 | "version": "1.4.4", 4 | "description": "Node-Excel-Export", 5 | "main": "index.js", 6 | "types": "./lib/node-excel-export.d.ts", 7 | "scripts": { 8 | "test": "echo \"Error: no test specified\" && exit 1" 9 | }, 10 | "repository": { 11 | "type": "git", 12 | "url": "git+https://github.com/andreyan-andreev/node-excel-export.git" 13 | }, 14 | "keywords": [ 15 | "excel" 16 | ], 17 | "author": "Andreyan Andreev ", 18 | "license": "MIT", 19 | "bugs": { 20 | "url": "https://github.com/andreyan-andreev/node-excel-export/issues" 21 | }, 22 | "homepage": "https://github.com/andreyan-andreev/node-excel-export#readme", 23 | "dependencies": { 24 | "xlsx-style": "^0.8.13" 25 | } 26 | } 27 | --------------------------------------------------------------------------------