Add multiple worksheet to a stream workbook with ExcelJS - node.js

Context
NodeJS 10
ExcelJS 0.8.5
LibreOffice 5.1.6.2 on Ubuntu
Issue
I am trying to create a multi-sheet Excel file with ExcelJS.
I am following the official documentation from the ExcelJS github page.
The first step is the creation of the workbook. In my case, i want a stream because i will append lot of datas.
// Create Excel Workbook Stream
const workbookStream = new Excel.stream.xlsx.WorkbookWriter({
filename: path,
useStyles: true,
useSharedStrings: true,
});
Then i add sheet to the created workbook's stream as said into the documentation Worksheet Properties.
const sheet = workbookStream.addSheet('sheet1'); // Throw here
But in this way, i got the following error :
'Type error: workbookStream.addSheet is not a function
I have also found a code that do not throw but do not work and do not create many sheets.
const header = ['A', 'B', 'C'];
const sheet1 = Excel.addSheetOnWorkbook({
workbook: workbookStream,
name: 'sheet1',
});
const sheet2 = Excel.addSheetOnWorkbook({
workbook: workbookStream,
name: 'sheet2',
});
sheet.addRow(header).commit();
sheet.addRow(header).commit();
await workbookStream.commit();
In this case, only the sheet1 is created (opening with LibreOffice 5.1.6.2).
Any way to resolve this case with ExcelJS ?

I don't know how much help this is to you, but on my environment:
Windows 10 (10.0.17134.0)
Node 10.15.0
exceljs 1.6.0
Once I set the worksheet.state to visible, I can see it in LibreOffice:
const Excel = require('exceljs');
const workbook = new Excel.Workbook();
const fs = require('fs');
const filename = "test.xlsx";
const sheetNames = ["A", "B", "C", "D"];
sheetNames.forEach(sheetName => {
let worksheet = workbook.addWorksheet(sheetName);
// I believe this needs to be set to show in LibreOffice.
worksheet.state = 'visible';
});
const stream = fs.createWriteStream(filename);
workbook.xlsx.write(stream)
.then(function() {
console.log(`File: ${filename} saved!`);
stream.end();
}).catch(error => {
console.err(`File: ${filename} save failed: `, error);
});
Using the Streaming XLSX WorkbookWriter:
const Excel = require('exceljs');
const sheetNames = ["A", "B", "C", "D"];
const workbook = new Excel.stream.xlsx.WorkbookWriter( { filename: './streamed-workbook.xlsx' } );
sheetNames.forEach(sheetName => {
let worksheet = workbook.addWorksheet(sheetName);
worksheet.state = 'visible';
worksheet.commit();
});
// Finished the workbook.
workbook.commit()
.then(function() {
console.log(`Worksheet committed!`);
});
I'll test on an Ubuntu machine as well.
XSLX files are simply .zip files containing multiple .xml files, so you can inspect the XML data yourself.
To show the raw Xml (worksheet.xml) produced by LibreOffice versus exceljs:
LibreOffice
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<workbook xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main" xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships">
<fileVersion appName="Calc"/>
<workbookPr backupFile="false" showObjects="all" date1904="false"/>
<workbookProtection/>
<bookViews>
<workbookView showHorizontalScroll="true" showVerticalScroll="true" showSheetTabs="true" xWindow="0" yWindow="0" windowWidth="16384" windowHeight="8192" tabRatio="500" firstSheet="0" activeTab="1"/>
</bookViews>
<sheets>
<sheet name="A" sheetId="1" state="visible" r:id="rId2"/>
<sheet name="B" sheetId="2" state="visible" r:id="rId3"/>
</sheets>
<calcPr iterateCount="100" refMode="A1" iterate="false" iterateDelta="0.001"/>
<extLst>
<ext xmlns:loext="http://schemas.libreoffice.org/" uri="{7626C862-2A13-11E5-B345-FEFF819CDC9F}">
<loext:extCalcPr stringRefSyntax="CalcA1"/>
</ext>
</extLst>
</workbook>
exceljs
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<workbook xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main" xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" mc:Ignorable="x15" xmlns:x15="http://schemas.microsoft.com/office/spreadsheetml/2010/11/main">
<fileVersion appName="xl" lastEdited="5" lowestEdited="5" rupBuild="9303"/>
<workbookPr defaultThemeVersion="164011" filterPrivacy="1"/>
<sheets>
<sheet sheetId="1" name="A" state="show" r:id="rId3"/>
<sheet sheetId="2" name="B" state="show" r:id="rId4"/>
<sheet sheetId="3" name="C" state="show" r:id="rId5"/>
<sheet sheetId="4" name="D" state="show" r:id="rId6"/>
</sheets>
<calcPr calcId="171027"/>
</workbook>
One more thing
The sheet title can corrupt the XML file structure when some non alphanum characters are set.
Be careful about sheet title !

Related

SuiteScript 2.0 xmlToPdf - add image to PDF

I have an image in File Cabinet that I want to add to my PDF. I have a script that creates a PDF and adds that image to it.
I tested the link https://system.na2.netsuite.com${imgURL} on my browser and the image loads. However I get a strange error when I try to add it to my PDF below:
var myImageFromFileCabinet = file.load({id:10202});
imgURL = myImageFromFileCabinet.url;
xmlStr = `<body><img src="https://system.na2.netsuite.com${imgURL}"></body>`;
let pdfFile = render.xmlToPdf({ xmlString: xmlStr });
context.response.writeFile({
file: pdfFile,
isInline: true
});
"type":"error.SuiteScriptError","name":"USER_ERROR","message":"Error Parsing XML: The reference to entity "c" must end with the ';' delimiter.
How can I add an image to a PDF?
TLDR: Escape the URL string for use in XML
The root cause of your error is that you are not escaping the URL for use in XML. The & characters in the URL must be escaped as XML/HTML entities. You can do this with the N/xml.escape() function:
const imgURL = xml.escape({xmlText: myImageFromFileCabinet.url});
That said, there were several other issues I had to resolve with this code along the way:
Outer tag must be pdf
The initial error I got when running this code was:
Error Parsing XML: Outer tag is body, should be pdf or pdfset
I fixed this by wrapping the <body> in a <pdf>.
img tag must be closed
Next I needed to close the <img> with </img> (or /> whichever you prefer).
Summary
My full working onRequest looks like:
const onRequest = (context) => {
const myImageFromFileCabinet = file.load({id:1820});
const imgURL = xml.escape({xmlText: myImageFromFileCabinet.url});
const xmlString = `<pdf><body><img src="https://system.na2.netsuite.com${imgURL}"/></body></pdf>`;
const pdfFile = render.xmlToPdf({ xmlString });
context.response.writeFile({
file: pdfFile,
isInline: true
});
};
Note that I've also made some minor changes like renaming variables and adding some const keywords, as well as of course changing the image's internal ID for my own account.

NodeJS checking if XML element exists and add or delete

I'm working with XML in NodeJS. I've been using the xmlbuilder to create my XML. The problem is that now I need to check if a element already exists and delete or update it.
For example, I have the following XML
<?xml version="1.0"?>
<listings>
<listing>
<id>1</id>
<name>TEST</name>
<description>TEST</description>
</listing>
<listing>
<id>2</id>
<name>TEST</name>
<description>TEST</description>
</listing>
</listings>
Then, I call my updateXML controller to add data to it.
const builder = require('xmlbuilder');
const fs = require('fs');
const path = require("path");
exports.updateXML = async (req, res, next) => {
const data = req.body.data;
/*
For example data is
{
id: 2,
name: "Test2",
description: "Desc2"
}
*/
const xmlFile = fs.readFileSync(path.resolve(__dirname, "./oodle.xml"), 'utf8');
if(/*How do I check if the xmlFile has a <id> === data.id?*/) {
// If id matches. How can I delete the whole <listing> node for that id?
}
const newListing = builder.create('listing');
newListing.ele("id", data.id);
newListing.ele("name", data.name);
newListing.ele("description", data.description);
// How can I add the newListing node to the xmlFile?
}
Thanks
I don't believe it's really necessary to remove the node with the duplicate <id>, create a new <listing> node and then insert it into the xml. At least as far as the sample xml in your question is concerned, you can just modify the text child nodes of the relevant <listing> node.
Something along the lines of:
const { select } = require('xpath');
let query = `//listing[./id[./text()="${data.id}"]]`;
const nodes = select(query, doc.node);
nodes.forEach(function (node) {
nam = select('.//name/text()',node)
desc = select('.//description/text()',node)
nam[0].data = data.name;
desc[0].data = data.description;
});
const serializedXML = doc.end({ format: 'xml', prettyPrint: true });
console.log(serializedXML)
Output:
<?xml version="1.0"?>
<listings>
<listing>
<id>1</id>
<name>TEST</name>
<description>TEST</description>
</listing>
<listing>
<id>2</id>
<name>Test2</name>
<description>Desc2</description>
</listing>
</listings>

Export TypeScript to Excel with style

I use xlsx to write data to xlsx file.
My code :
const worksheet: XLSX.WorkSheet = XLSX.utils.json_to_sheet(myData);
const workbook: XLSX.WorkBook = { Sheets: { 'mySheet': worksheet }, SheetNames: ['mySheet'] };
XLSX.writeFile(workbook, 'MyData.xlsx');.
I want to style the cell ,how and whether it can be done?
I tried to use s but it did not work:
worksheet['B1'].s = {font: {bold : true}};
EDIT
"The docs claim that's only available in the pro version"- This comment is true, I did not notice.
So my question is different:
Is there any other way to export to excel file with style?
you can use exceljs package instead of xlsx in angular.

Extracting Extended Data from KML using Javascript

I have a placemark in a KML that looks something like this
<Placemark>
<id>test345</id>
<name>Images from KML file</name>
<ExtendedData>
<Data name="type">
<value>images</value>
</Data>
</ExtendedData>
<Point>
<coordinates>-122.448425,37.802907,0</coordinates>
</Point>
I'm attempting to extract the ExtendedData information out of this placemarker on a click event:
google.earth.addEventListener(kmlObject, 'click', function(event) {
event.preventDefault();
var kmlPlacemark = event.getTarget();
});
An alternative solution would be to get the kmlObject from the kmlPlacemarker, any ideas?
Given the placemark the Google Earth API provides two methods to access the ExtendedData element.
getBalloonHtml()
getBalloonHtmlUnsafe()
API Reference:
https://developers.google.com/earth/documentation/reference/interface_kml_feature
You can find a working example in the Google Code Playground here:
https://code.google.com/apis/ajax/playground/?exp=earth#extended_data_in_balloons
If you wanted to get the raw KML for extended data then you could fetch the KML representation and parse it as an XML document.
var output = placemark.getKml();
Just to say I posted about just this issue on the support forum for the plug-in: https://code.google.com/p/earth-api-samples/issues/detail?id=16
Here is a method I cobbled together to provide support for getExtendedData. It takes a string of Kml as the argument via 'feature.getKml();` It returns any extended data elements that have values in a key[value] object. It expects the extended data to be in the format:
<Data name="Foo">
<value>bar</value>
</Data>
Tested in XP - FF3.0, IE7, Chrome
function getExtendedData(kmlString) {
var xmlDoc = null;
var keyValue = [];
//Parse the kml
try {
//Internet Explorer
xmlDoc = new ActiveXObject("Microsoft.XMLDOM");
xmlDoc.async="false";
xmlDoc.loadXML(kmlString);
} catch(e) {
try {
//Firefox, etc.
var parser = new DOMParser();
xmlDoc = parser.parseFromString(kmlString,"text/xml");
}
catch(e) {
//Failed to parse
alert(e.message);
return;
}
}
// Get all the named elements
var data = xmlDoc.getElementsByTagName("Data");
// Iterate through the data elements
for(var i=0; i<data.length; i++) {
if(data[i].getAttribute("name") &&
data[i].getElementsByTagName("value").length > 0) {
// Get the name and value
var name = data[i].getAttribute("name");
var value = data[i].getElementsByTagName("value")[0].firstChild.data;
// Assign them to the keyValue object
keyValue[name] = value;
}
}
return keyValue;
}
Usage
// where 'feature' is the object with the extended data
var data = getExtendedData(feature.getKml());
for (var name in data) {
var value = data[name];
alert(name + '=' + value); // e.g. type=images
}
It is actually possible to access the ExtendedData elements via the DOM APIs, although they're not particularly well-documented anywhere.
I found them while grepping around inside some of the resource (.rcc) files packaged with the Plugin.
Assuming a simple Placemark sample similar to yours:
<Placemark id="testmark">
<!-- other stuff... -->
<ExtendedData>
<Data name="someDataUrl">
<displayName>URL Representing some Data</displayName>
<value>http://example.com/#hello</value>
</Data>
</ExtendedData>
</Placemark>
Then (once it's fetched/parsed/loaded into Earth, you can access it something like:
var mark = ge.getElementById('testmark');
var extDataObj = mark.getExtendedData();
var extDataOut = Array(extDataObj.getDataCount());
for (var i = 0; i < extDataObj.getDataCount(); i++) {
var item = extDataObj.getData(i);
var details = { name: item.getName(),
displayName: item.getDisplayName(),
value: item.getValue()
};
extDataOut[i] = details;
}
console.dir(extDataOut);
Haven't tested it for performance vs the .getKml() and feed to an external parser approach, and the lack of official documentation might mean it's not fully functional or supported, but in all testing so far it seems to do ok. I haven't yet found a way to access any of the more complicated SchemaData type structures, only the simple <data name=''><value>... form.

How to create an Excel File with Nodejs?

I am a nodejs programmer . Now I have a table of data that I want to save in Excel File format . How do I go about doing this ?
I found a few Node libraries . But most of them are Excel Parsers rather than Excel Writers .I am using a Linux Server . Hence need something that can run on Linux . Please let me know if there are any helpful libraries that you know of .
Or is there a way I can convert a CSV file to an xls file ( programmatically ) ?
excel4node is a maintained, native Excel file creator built from the official specification. It's similar to, but more maintained than msexcel-builder mentioned in the other answer.
// Require library
var excel = require('excel4node');
// Create a new instance of a Workbook class
var workbook = new excel.Workbook();
// Add Worksheets to the workbook
var worksheet = workbook.addWorksheet('Sheet 1');
var worksheet2 = workbook.addWorksheet('Sheet 2');
// Create a reusable style
var style = workbook.createStyle({
font: {
color: '#FF0800',
size: 12
},
numberFormat: '$#,##0.00; ($#,##0.00); -'
});
// Set value of cell A1 to 100 as a number type styled with paramaters of style
worksheet.cell(1,1).number(100).style(style);
// Set value of cell B1 to 300 as a number type styled with paramaters of style
worksheet.cell(1,2).number(200).style(style);
// Set value of cell C1 to a formula styled with paramaters of style
worksheet.cell(1,3).formula('A1 + B1').style(style);
// Set value of cell A2 to 'string' styled with paramaters of style
worksheet.cell(2,1).string('string').style(style);
// Set value of cell A3 to true as a boolean type styled with paramaters of style but with an adjustment to the font size.
worksheet.cell(3,1).bool(true).style(style).style({font: {size: 14}});
workbook.write('Excel.xlsx');
I just figured a simple way out . This works -
Just create a file with Tabs as delimiters ( similar to CSV but replace comma with Tab ). Save it with extension .XLS . The file can be opened in Excel .
Some code to help --
var fs = require('fs');
var writeStream = fs.createWriteStream("file.xls");
var header="Sl No"+"\t"+" Age"+"\t"+"Name"+"\n";
var row1 = "0"+"\t"+" 21"+"\t"+"Rob"+"\n";
var row2 = "1"+"\t"+" 22"+"\t"+"bob"+"\n";
writeStream.write(header);
writeStream.write(row1);
writeStream.write(row2);
writeStream.close();
This creates the file in XLS file format . It doesnt work if you try XLSX instead of XLS .
Use msexcel-builder. Install it with:
npm install msexcel-builder
Then:
// Create a new workbook file in current working-path
var workbook = excelbuilder.createWorkbook('./', 'sample.xlsx')
// Create a new worksheet with 10 columns and 12 rows
var sheet1 = workbook.createSheet('sheet1', 10, 12);
// Fill some data
sheet1.set(1, 1, 'I am title');
for (var i = 2; i < 5; i++)
sheet1.set(i, 1, 'test'+i);
// Save it
workbook.save(function(ok){
if (!ok)
workbook.cancel();
else
console.log('congratulations, your workbook created');
});
Although this question has several answers, they may now be a little dated.
New readers may prefer to consider the xlsx or "sheetsJS" package, which now seems to now be by far the most popular node package for this use case.
The current top answer recommends excel4node , which does look quite good - but the latter package seems less maintained (and far less popular) than the former.
Answering the question directly, using xlsx:
const XLSX = require('xlsx');
/* create a new blank workbook */
const wb = XLSX.utils.book_new();
// Do stuff, write data
//
//
// write the workbook object to a file
XLSX.writeFile(workbook, 'out.xlsx');
You should check ExcelJS
Works with CSV and XLSX formats.
Great for reading/writing XLSX streams. I've used it to stream an XLSX download to an Express response object, basically like this:
app.get('/some/route', function(req, res) {
res.writeHead(200, {
'Content-Disposition': 'attachment; filename="file.xlsx"',
'Transfer-Encoding': 'chunked',
'Content-Type': 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'
})
var workbook = new Excel.stream.xlsx.WorkbookWriter({ stream: res })
var worksheet = workbook.addWorksheet('some-worksheet')
worksheet.addRow(['foo', 'bar']).commit()
worksheet.commit()
workbook.commit()
}
Works great for large files, performs much better than excel4node (got huge memory usage & Node process "out of memory" crash after nearly 5 minutes for a file containing 4 million cells in 20 sheets) since its streaming capabilities are much more limited (does not allows to "commit()" data to retrieve chunks as soon as they can be generated)
See also this SO answer.
Using fs package we can create excel/CSV file from JSON data.
Step 1: Store JSON data in a variable (here it is in jsn variable).
Step 2: Create empty string variable(here it is data).
Step 3: Append every property of jsn to string variable data, while appending put '\t' in-between 2 cells and '\n' after completing the row.
Code:
var fs = require('fs');
var jsn = [{
"name": "Nilesh",
"school": "RDTC",
"marks": "77"
},{
"name": "Sagar",
"school": "RC",
"marks": "99.99"
},{
"name": "Prashant",
"school": "Solapur",
"marks": "100"
}];
var data='';
for (var i = 0; i < jsn.length; i++) {
data=data+jsn[i].name+'\t'+jsn[i].school+'\t'+jsn[i].marks+'\n';
}
fs.appendFile('Filename.xls', data, (err) => {
if (err) throw err;
console.log('File created');
});
XLSx in the new Office is just a zipped collection of XML and other files. So you could generate that and zip it accordingly.
Bonus: you can create a very nice template with styles and so on:
Create a template in 'your favorite spreadsheet program'
Save it as ODS or XLSx
Unzip the contents
Use it as base and fill content.xml (or xl/worksheets/sheet1.xml) with your data
Zip it all before serving
However I found ODS (openoffice) much more approachable (excel can still open it), here is what I found in content.xml
<table:table-row table:style-name="ro1">
<table:table-cell office:value-type="string" table:style-name="ce1">
<text:p>here be a1</text:p>
</table:table-cell>
<table:table-cell office:value-type="string" table:style-name="ce1">
<text:p>here is b1</text:p>
</table:table-cell>
<table:table-cell table:number-columns-repeated="16382"/>
</table:table-row>
Or - build on #Jamaica Geek's answer, using Express - to avoid saving and reading a file:
res.attachment('file.xls');
var header="Sl No"+"\t"+" Age"+"\t"+"Name"+"\n";
var row1 = [0,21,'BOB'].join('\t')
var row2 = [0,22,'bob'].join('\t');
var c = header + row1 + row2;
return res.send(c);
install exceljs
npm i exceljs --save
import exceljs
var Excel = require('exceljs');
var workbook = new Excel.Workbook();
create workbook
var options = {
filename: __dirname+'/Reports/'+reportName,
useStyles: true,
useSharedStrings: true
};
var workbook = new Excel.stream.xlsx.WorkbookWriter(options);
after create worksheet
var worksheet = workbook.addWorksheet('Rate Sheet',{properties:{tabColor:{argb:'FFC0000'}}});
in worksheet.column array you pass column name in header and array key
pass in key
worksheet.columns = [
{ header: 'column name', key: 'array key', width: 35},
{ header: 'column name', key: 'array key', width: 35},
{ header: 'column name', key: 'array key', width: 20},
];
after using forEach loop append row one by one in exel file
array.forEach(function(row){ worksheet.addRow(row); })
you can also perfome loop on each exel row and cell
worksheet.eachRow(function(row, rowNumber) {
console.log('Row ' + rowNumber + ' = ' + JSON.stringify(row.values));
});
row.eachCell(function(cell, colNumber) {
console.log('Cell ' + colNumber + ' = ' + cell.value);
});
Use exceljs library for creating and writing into existing excel sheets.
You can check this tutorial for detailed explanation.
link
First parameter is the source file
Second parameter is the separator
Third parameter is the resulting file (*.xlsx)
Attention: to increase node heap use: node --max-old-space-size=4096 index.js
const fs = require('fs');
var xl = require('excel4node');
const data = fs.readFileSync(process.argv[2], 'utf-8');
const lines = data.split(/\r?\n/);
const linesFromOne = [null].concat(lines);
var wb = new xl.Workbook();
var ws = wb.addWorksheet('Planilha 1');
for (let j=1;j<=linesFromOne.length-1;j++){
// Create a reusable style
var style = wb.createStyle({
font: {
color: '#050000',
size: 12,
},
});
pieces = linesFromOne[j].split(process.argv[3])
pieces.forEach((element, index) =>{
ws.cell(j, index+1).string(element)
.style(style);
});
}
wb.write(process.argv[4]);

Resources