How to Convert CSV Response into an Excel Sheet with Download by Using LWC
Reporting screens often need two things at once: a readable table on screen, and a downloadable file the user can open in Excel. Rather than building two separate data pipelines, you can fetch a single CSV string from Apex, render it as a table in your Lightning Web Component, and let the same string be downloaded as a .csv file with one click.
This post walks through a working pattern for exactly that — fetching report data, parsing raw CSV text into a JavaScript object array, displaying it in the UI, and exporting it back out as a downloadable spreadsheet.
Step 1: Fetch the Report Data from Apex
The component calls an imperative Apex method, passing along whatever filters the user has selected (in this case, a searchWrapper object holding search parameters):
getReportData() {
this.isLoading = true;
getReportData({
searchParams: this.searchWrapper
})
.then(result => {
this.csvdata = result;
this.formatReportData(result);
this.isLoading = false;
})
.catch(error => {
console.error('getReportData error', error);
this.isLoading = false;
});
}
The Apex method is expected to return a raw CSV string (header row + data rows). That string is stored as-is in this.csvdata — this is important, because it's what gets handed off later for the file download, untouched by any formatting done for display purposes.
Step 2: Turn the CSV String into an Array of Objects
To show the data in a table (or any other UI component), the raw CSV text needs to become structured JavaScript objects. The formatReportData method handles this:
formatReportData(info) {
var resultInfo = info.trim().split(/\r?\n|\r/);
var headerColumns = resultInfo[0].replaceAll(' ', '_').trim().split(',');
var dataObj = [];
for (let index = 0; index < resultInfo.length; ++index) {
const element = resultInfo[index];
if (index !== 0) {
var rowData = this.CSVtoArray(element);
var obj = {};
for (let colIndex = 0; colIndex < rowData.length; ++colIndex) {
obj[headerColumns[colIndex]] = rowData[colIndex];
}
dataObj.push(obj);
}
}
this.reportData = dataObj;
this.showDataResult = dataObj.length > 0;
}
A few things worth noting here:
- Line splitting is resilient.
split(/\r?\n|\r/)handles CSV files coming from Windows (\r\n), Mac (\r), or Unix (\n) line endings, which matters since exported reports can originate from different systems. - Header names are sanitized. Spaces in column headers are replaced with underscores, making them safe to use as JavaScript object keys.
- The first row is skipped when building data rows, since it only contains column headers.
showDataResultacts as a simple flag to conditionally show or hide the results table in the template, depending on whether any rows came back.
Step 3: Parse Each Row Safely with a Regex-Based CSV Splitter
A simple .split(',') breaks as soon as a field contains a comma inside quotes (for example, "Doe, John"). To avoid that, the component uses a small, well-tested regex-based parser:
CSVtoArray(text) {
var re_valid = /^\s*(?:'[^'\\]*(?:\\[\S\s][^'\\]*)*'|"[^"\\]*(?:\\[\S\s][^"\\]*)*"|[^,'"\s\\]*(?:\s+[^,'"\s\\]+)*)\s*(?:,\s*(?:'[^'\\]*(?:\\[\S\s][^'\\]*)*'|"[^"\\]*(?:\\[\S\s][^"\\]*)*"|[^,'"\s\\]*(?:\s+[^,'"\s\\]+)*)\s*)*$/;
var re_value = /(?!\s*$)\s*(?:'([^'\\]*(?:\\[\S\s][^'\\]*)*)'|"([^"\\]*(?:\\[\S\s][^"\\]*)*)"|([^,'"\s\\]*(?:\s+[^,'"\s\\]+)*))\s*(?:,|$)/g;
if (!re_valid.test(text)) return null;
var a = [];
text.replace(re_value, function(m0, m1, m2, m3) {
if (m1 !== undefined) a.push(m1.replace(/\\'/g, "'"));
else if (m2 !== undefined) a.push(m2.replace(/\\"/g, '"'));
else if (m3 !== undefined) a.push(m3);
return '';
});
if (/,\s*$/.test(text)) a.push('');
return a;
}
This function:
- First validates the line against
re_validto confirm it's a well-formed CSV row — returningnullimmediately if not. - Then walks through the line with
re_value, correctly extracting values that are single-quoted, double-quoted, or unquoted. - Unescapes any backslash-escaped quotes inside quoted values.
- Handles the edge case of a trailing empty value (a line ending in a comma).
Because this parser respects quotes, it can safely handle addresses, names, or notes fields that contain embedded commas — something a naive split would corrupt.
Step 4: Let the User Download the Same Data as a CSV File
Since the raw CSV string was preserved in this.csvdata, generating a downloadable file just means creating a temporary link element and triggering a click on it:
getDatasheet() {
let csv = this.csvdata;
if (csv == null) { return; }
var hiddenElement = document.createElement('a');
hiddenElement.href = 'data:text/csv;charset=utf-8,' + encodeURI(csv);
hiddenElement.target = '_self';
hiddenElement.download = 'Export.csv';
document.body.appendChild(hiddenElement);
hiddenElement.click();
}
Because the file is saved with a .csv extension and a text/csv MIME type, double-clicking it on most machines opens it directly in Excel or Google Sheets — no server round trip or extra Apex logic required for the export itself.
Step 5: Wiring Up Filter Selections
The component also supports multi-select filters for material and plant, which get converted into comma-separated strings before being sent to Apex:
handleMaterial(event) {
let selectedMaterial = event.detail
.map(x => x.selected === true ? x.value : null)
.filter(v => v !== null);
this.searchWrapper.material = selectedMaterial.toString();
}
handlePlant(event) {
let selectedPlant = event.detail
.map(x => x.selected === true ? x.value : null)
.filter(v => v !== null);
this.searchWrapper.plant = selectedPlant.toString();
}
These selections feed directly into searchWrapper, which is passed to getReportData() on the next fetch.
Key Takeaways
- Keep the raw CSV string intact for downloading, and only transform a copy of it for on-screen display — this avoids double work and keeps the exported file identical to the source data.
- Use a regex-based CSV parser instead of a plain
split(',')whenever fields might contain commas or quoted values. - Normalize line endings with
split(/\r?\n|\r/)so the parser works regardless of which OS generated the file. - Generating a downloadable file client-side with a
data:text/csvURI avoids extra server calls — the browser handles the file creation. - Toggle a boolean flag (like
showDataResult) based on whether parsed data exists, so your template can cleanly show empty states versus populated tables.
With this approach, a single Apex call and a lightweight JavaScript parser give you both an interactive report table and a one-click Excel-ready export — all inside one LWC.