Overview
- Create a React App
- Generate a single PDF document containing multiple tables on the front-end
- Generate a single Excel file containing multiple tables on the front-end
- Generate a PDF invoice on the back-end
- Generate a DOCX invoice on the back-end
Create React App
The first step is to create a new React app using the command npx create-react-app my-app
and then navigate to the app directory using cd my-app
and start the app using npm start
.
Generate PDF tables
To generate PDF tables on the front-end, the following packages must be installed: jspdf
and jspdf-autotable
. These can be installed by running the command npm i jspdf jspdf-autotable
.
Dependencies
:
"dependencies": {
"jspdf": "^2.5.1",
"jspdf-autotable": "^3.5.28",
"@material-ui/core": "^4.10.2",
"@material-ui/icons": "^4.11.2",
"material-table": "^1.69.2"
......
}
To copy sample data you can visit Link.
To create the data for the tables, you can use the sample data provided in the src/utils.js
file. This file contains data for two tables, firstTableColumns
and firstTableData
, and secondTableColumns
and secondTableData
.
To trigger the download of the PDF, add a onClick
event on a Download PDF button. In the function defined in the event, the following code will generate the PDF:
const handleDownloadPDF = () => {
const pdfDoc = new jsPDF();
pdfDoc.text("Report", 15, 10);
autoTable(pdfDoc, {
theme: "grid",
headStyles: { fontSize: 10 },
bodyStyles: { fontSize: 8, fontStyle: "italic" },
columns: firstTableColumns.map((col) => ({ ...col, dataKey: col.field })),
body: firstTableData,
});
autoTable(pdfDoc, {
theme: "grid",
columns: secondTableColumns.map((col) => ({ ...col, dataKey: col.field })),
body: secondTableData,
});
pdfDoc.save("ReportTable.pdf");
};
In the above function we have pdfDoc
instance and we have pdfDoc.text(string, x, y)
. String to be added to the page. string on position x
, y
. x
is Coordinate (in units declared at inception of PDF document) against left edge of the page. y
Coordinate (in units declared at inception of PDF document) against upper edge of the page.
Then we have autotable
which will import autoTable from "jspdf-autotable";
Styling Options
* theme: 'striped'|'grid'|'plain' = 'striped'
* styles: StyleDef
* headStyles: StyleDef
* bodyStyles: StyleDef
* footStyles: StyleDef
* alternateRowStyles: StyleDef
* columnStyles: {&columnDataKey: StyleDef}
Note that the columnDataKey is normally the index of the column, but could also be the dataKey of a column if content initialized with the columns property.
StyleDef:
*font: 'helvetica'|'times'|'courier' = 'helvetica'
*fontStyle: 'normal'|'bold'|'italic'|'bolditalic' = 'normal'
*overflow: 'linebreak'|'ellipsize'|'visible'|'hidden' = 'linebreak'
*fillColor: Color? = null
*textColor: Color? = 20
*cellWidth: 'auto'|'wrap'|number = 'auto'
*minCellWidth: number? = 10
*minCellHeight: number = 0
*halign: 'left'|'center'|'right' = 'left'
*valign: 'top'|'middle'|'bottom' = 'top'
*fontSize: number = 10
*cellPadding: Padding = 10
*lineColor: Color = 10
*lineWidth: border = 0
// If 0, no border is drawn
Color: Either false for transparent, hex string, gray level 0-255 or rbg array e.g. [255, 0, 0] false|string|number|[number, number, number]
Padding: Either a number or object {top: number, right: number, bottom: number, left: number}
border: Either a number or object {top: number, right: number, bottom: number, left: number}
We can add page number on pages. And here is full function body to generate pdf file with page number.
const handleDownloadPDF = () => {
const pdfDoc = new jsPDF();
const totalPages = "{total_pages_count_string}";
pdfDoc.page = 1;
pdfDoc.text("Report", 15, 10);
autoTable(pdfDoc, {
theme: "grid",
headStyles: { fontSize: 10 },
bodyStyles: { fontSize: 8, fontStyle: "italic" },
columns: firstTableColumns.map((col) => ({ ...col, dataKey: col.field })),
body: firstTableData,
});
autoTable(pdfDoc, {
theme: "grid",
columns: secondTableColumns.map((col) => ({ ...col, dataKey: col.field })),
body: secondTableData,
addPageContent: (data) => {
let footerStr = "Page " + pdfDoc.internal.getNumberOfPages();
if (typeof pdfDoc.putTotalPages === "function") {
footerStr = footerStr + " of " + totalPages;
}
pdfDoc.setFontSize(10);
pdfDoc.text(
footerStr,
data.settings.margin.left,
pdfDoc.internal.pageSize.height - 10
);
},
});
if (typeof pdfDoc.putTotalPages === "function") {
pdfDoc.putTotalPages(totalPages);
}
pdfDoc.save("ReportTable.pdf");
};
For more options we have jspdf-autotable documentation here: Link
Generate Excel tables
To generate excel files on the front-end, you can use the npm packages xlsx
and xlsx-populate
. The packages can be installed by running the command npm i xlsx xlsx-populate
.
Dependencies
:
"dependencies": {
"xlsx": "^0.17.0",
"xlsx-populate": "^1.21.0",
......
}
To create the data for the tables, you can use the sample data provided in the src/utils.js
file. This file contains data for two tables, firstTableColumns
and firstTableData
, and secondTableColumns
and secondTableData
.
To trigger the download of the Excel file, you can create a button or a link that calls the downloadExcel()
function when clicked.
const downloadExcel = () => {
handleExport().then((url) => {
const downloadAnchorNode = document.createElement("a");
downloadAnchorNode.setAttribute("href", url);
downloadAnchorNode.setAttribute("download", "report.xlsx");
downloadAnchorNode.click();
downloadAnchorNode.remove();
});
};
Handle Export handleExport()
const handleExport = () => {
let title = [{ A: "Excel Sheet 1" }].concat("");
let table1 = [
{
A: "Name",
B: "Email",
},
];
let table2 = [
{
A: "Name",
B: "Email",
C: "Year",
D: "Fee",
},
];
firstTableData.forEach((row) => {
table1.push({
A: row.name,
B: row.email,
});
});
secondTableData.forEach((row) => {
table2.push({
A: row.name,
B: row.email,
C: row.year,
D: row.fee,
});
});
let table = [{ A: "Table Data" }]
.concat(table1)
.concat([""])
.concat(table2);
let finalData = [...title, ...table];
const wb = XLSX.utils.book_new();
// we will add multiple sheets
// sheet 1
const sheet1 = XLSX.utils.json_to_sheet(finalData, {
skipHeader: true,
});
// sheet 2
title = [{ A: "Excel Sheet 2" }].concat("");
table = [{ A: "Table Data" }].concat(table2);
finalData = [...title, ...table];
const sheet2 = XLSX.utils.json_to_sheet(finalData, {
skipHeader: true,
});
XLSX.utils.book_append_sheet(wb, sheet1, "First Sheet");
XLSX.utils.book_append_sheet(wb, sheet2, "Second Sheet");
const workbookBlob = workbook2blob(wb);
let headerIndexes = [];
finalData.forEach((data, index) =>
data["A"] === "Name" ? headerIndexes.push(index) : null
);
const totalRecords = [...firstTableData, ...secondTableData].length;
const dataInfo = {
titleCell: "A2",
titleRange: "A1:D2",
tbodyRange: `A4:D${finalData.length}`,
theadRange:
headerIndexes?.length >= 1
? `A${headerIndexes[0]}:D${headerIndexes[0]}`
: null,
theadRange1:
headerIndexes?.length >= 1
? `A${headerIndexes[0] + 1}:D${headerIndexes[0] + 1}`
: null,
tFirstColumnRange:
headerIndexes?.length >= 1
? `A${headerIndexes[0] + 1}:A${totalRecords + headerIndexes[0] + 1}`
: null,
};
return addStyle(workbookBlob, dataInfo);
};
Work Book to Blob workbook2blob(wb)
const workbook2blob = (workbook) => {
const wopts = {
bookType: "xlsx",
bookSST: false,
type: "binary",
};
const wbout = XLSX.write(workbook, wopts);
const blob = new Blob([s2ab(wbout)], {
type: "application/octet-stream",
});
return blob;
};
s2ab s2ab(wbout)
const s2ab = (s) => {
const buf = new ArrayBuffer(s.length);
const view = new Uint8Array(buf);
for (let i = 0; i < s.length; ++i) {
view[i] = s.charCodeAt(i);
}
return buf;
};
Add Style addStyle(workbookBlob, dataInfo)
const addStyle = async (workbookBlob, dataInfo) => {
const workbook = await XlsxPopulate.fromDataAsync(workbookBlob);
workbook.sheets().forEach((sheet) => {
sheet.usedRange().style({
fontFamily: "Arial",
verticalAlignment: "center",
});
sheet.column("A").width(15);
sheet.column("B").width(25);
sheet.range(dataInfo.titleRange).merged(true).style({
bold: true,
horizontalAlignment: "center",
verticalAlignment: "center",
});
sheet.range(dataInfo.theadRange).merged(true).style({
bold: true,
horizontalAlignment: "center",
verticalAlignment: "center",
});
sheet.range(dataInfo.theadRange1).style({
bold: true,
horizontalAlignment: "center",
verticalAlignment: "center",
});
if (dataInfo.tbodyRange) {
sheet.range(dataInfo.tbodyRange).style({
horizontalAlignment: "center",
});
}
});
const workbookBlob_1 = await workbook.outputAsync();
return URL.createObjectURL(workbookBlob_1);
};
For more, you can visit xlsx
documentation xlsx. And explore this example Github line 60-219.
Generate PDF invoice (on back-end)
This code is for generating a PDF invoice on the back-end and downloading it on the front-end.
To trigger the download of the PDF file, you can create a button or a link that calls the downloadFromBackendPdf()
function when clicked.
The downloadFromBackendPdf
function is an async
function that makes a GET
request to a specified URL, in this case http://localhost:9000/pdf
, to retrieve a PDF invoice.
Front-end:
Dependencies
"dependencies": {
"axios": "^0.19.2",
.....
},
const downloadFromBackendPdf = async () => {
await axios
.get("http://localhost:9000/pdf", { responseType: "blob" })
.then((response) => {
const href = URL.createObjectURL(
new Blob([response.data], { type: "octet-stream" })
);
const a = Object.assign(document.createElement("a"), {
href,
style: "display: none",
download: "Report.pdf",
});
document.body.appendChild(a);
a.click();
URL.revokeObjectURL(href);
a.remove();
})
.catch((error) => {
console.log(error);
});
};
The function uses the axios
library to make the GET
request and specifies that the response should be of type blob
. The function then creates a URL object using the URL.createObjectURL()
method, passing in the blob data from the response.
A new <a>
element is then created, and its href
and download attributes are set to the URL object and the desired file name respectively. The element is then appended to the document's body, and the click()
method is called on it, which triggers the download.
The URL object is then revoked using URL.revokeObjectURL(href)
and the element is removed from the document's body.
It's important to note that the URL used in the GET request should be the correct URL of the back-end endpoint that serves the PDF invoice. Also, if the app is served in other than localhost, the domain should be changed accordingly.
Data for Invoice
const invoice = {
invoice_no: "47-606-0116",
created_date: "08/12/2022",
due_date: "09/05/2022",
company_name: "Oyoba",
address: "9th Floor",
payment_methods: [
{
type: "Check",
value: 500,
},
{
type: "Card",
value: 400,
},
],
items_description: [
{
name: "Website Design",
price: 400,
},
{
name: "Hosting(3 months)",
price: 100,
},
{
name: "Domain Name",
price: 70,
},
],
};
module.exports = invoice;
Invoice PDF content from
const invoice = require("./utils")
let total = 0;
let content = `
<div class="invoice-box" style="max-width: 800px;margin: auto;padding: 30px;border: 1px solid #eee;box-shadow: 0 0 10px rgba(0, 0, 0, .15);font-size: 16px;line-height: 24px;font-family: 'Helvetica Neue', 'Helvetica', Helvetica, Arial, sans-serif;color: #555;">
<table cellpadding="0" cellspacing="0" style="width: 100%;line-height: inherit;text-align: left;">
<tr class="top">
<td colspan="2" style="padding: 5px;vertical-align: top;">
<table style="width: 100%;line-height: inherit;text-align: left;">
<tr>
<td class="title" style="padding: 5px;vertical-align: top;padding-bottom: 20px;font-size: 45px;line-height: 45px;color: #333;">
<img src="https://banner2.cleanpng.com/20190205/gqc/kisspng-computer-icons-invoice-clip-art-illustration-scala-close-the-books-faster-terminalmanager-emerson-5c595ed26cb4d9.7140607415493608504453.jpg" style="width:100%; max-width:300px;">
</td>
<td style="padding: 5px;vertical-align: top;text-align: right;padding-bottom: 20px;">
Invoice #: ${invoice.invoice_no}<br>
Created: ${invoice.created_date}<br>
Due: ${invoice.due_date}
</td>
</tr>
</table>
</td>
</tr>
<tr class="information">
<td colspan="2" style="padding: 5px;vertical-align: top;">
<table style="width: 100%;line-height: inherit;text-align: left;">
<tr>
<td style="padding: 5px;vertical-align: top;padding-bottom: 40px;">
${invoice.company_name}.<br>
${invoice.address}
</td>
</tr>
</table>
</td>
</tr>
<tr class="heading">
<td style="padding: 5px;vertical-align: top;background: #eee;border-bottom: 1px solid #ddd;font-weight: bold;">
Payment Method
</td>
<td style="padding: 5px;vertical-align: top;text-align: right;background: #eee;border-bottom: 1px solid #ddd;font-weight: bold;">
Amount
</td>
</tr>`
for (const payment of invoice.payment_methods) {
content += `<tr class="details">
<td style="padding: 5px;vertical-align: top;padding-bottom: 20px;">
${payment.type}
</td>
<td style="padding: 5px;vertical-align: top;text-align: right;padding-bottom: 20px;">
$${payment.value}
</td>
</tr>`
}
content += `<tr class="heading">
<td style="padding: 5px;vertical-align: top;background: #eee;border-bottom: 1px solid #ddd;font-weight: bold;">
Item
</td>
<td style="padding: 5px;vertical-align: top;text-align: right;background: #eee;border-bottom: 1px solid #ddd;font-weight: bold;">
Price
</td>
</tr>`
for (const item of invoice.items_description) {
total += item.price;
content += `
<tr class="item">
<td style="padding: 5px;vertical-align: top;border-bottom: 1px solid #eee;">
${item.name}
</td>
<td style="padding: 5px;vertical-align: top;text-align: right;border-bottom: 1px solid #eee;">
$${item.price}
</td>
</tr>`
}
content += `
<tr class="total">
<td style="padding: 5px;vertical-align: top;"></td>
<td style="padding: 5px;vertical-align: top;text-align: right;border-top: 2px solid #eee;font-weight: bold;">
Total: $${total}
</td>
</tr>
</table>
</div>
`
module.exports = content;
Back-end:
First of all we need to install some packages by using npm i puppeteer cors express
Dependencies
:
"dependencies": {
"cors": "^2.8.5",
"puppeteer": "^19.5.0",
"react": "^18.2.0",
"react-dom": "^18.2.0"
},
app.js
const puppeteer = require("puppeteer");
const cors = require("cors");
const content = require('./pdfHtml');
const express = require("express");
const app = express();
app.use(
cors({
origin: "http://localhost:3000",// may vary depends on your app setup
})
);
app.get("/pdf", async (req, res) => {
const browser = await puppeteer.launch();
const page = await browser.newPage();
await page.setContent(content);
const pdf = await page.pdf({ format: "A4" });
res.send(Buffer.from(pdf));
browser.close();
});
app.listen(9000, () => {
console.log("Server is running");
});
To start the server run node app.js
.
The above code is for generating a PDF invoice on the back-end using the puppeteer
library. The app.get("/pdf", async (req, res) => {}
function is a GET
endpoint that listens to requests at the route /pdf
and uses the puppeteer library to generate a PDF invoice.
The puppeteer.launch()
method is used to launch an instance of the browser. A new page is then created using browser.newPage()
.
The method await page.setContent(content)
is used to set the content of the page to the content variable. The content variable should contain the HTML code that should be rendered in the PDF invoice.
You can visit Link to see the sample invoice html code.
const pdf = await page.pdf({ format: "A4" });
generates the PDF using the format specified in the options object (A4 in this case).
Finally, the generated PDF is sent as a response to the client using res.send(Buffer.from(pdf))
, and the browser instance is closed using browser.close()
.
It's important to note that this code uses puppeteer library, which requires a running chrome browser in the machine where the back-end is running. Also, the Content variable should contain the correct HTML structure that needs to be rendered in the PDF.
Generate Docx CV (on back-end)
Docx template code is quite large for this documentation here is the Link to explore the code.
This code generates a docx file containing information about experiences, education, and skills.
Inputs
-
experiences
: an array of objects, each containing information about a past or current job experience, including job title, company, start and end dates, and summary. -
education
: an array of objects, each containing information about a past education, including degree, field of study, school name, start and end dates, and notes. -
skills
: an array of objects, each containing information about a skill, including name. -
PHONE_NUMBER
,PROFILE_URL
,EMAIL
: constants containing personal contact information.
Outputs
- A docx file containing the input information in a formatted manner.
Dependencies
- The "docx" package, which can be installed via npm.
Now look at the DocumentCreator Class and it's properties
class DocumentCreator {
create([experiences, educations, skills, achivements]) {
const document = new Document({
sections: [
{
children: [
new Paragraph({
text: "Full Name",
heading: HeadingLevel.TITLE,
}),
this.createContactInfo(PHONE_NUMBER, PROFILE_URL, EMAIL),
this.createHeading("Education"),
...educations
.map((education) => {
const arr = [];
arr.push(
this.createInstitutionHeader(
education.schoolName,
`${education.startDate.year} - ${education.endDate.year}`
)
);
arr.push(
this.createRoleText(
`${education.fieldOfStudy} - ${education.degree}`
)
);
const bulletPoints = this.splitParagraphIntoBullets(
education.notes
);
bulletPoints.forEach((bulletPoint) => {
arr.push(this.createBullet(bulletPoint));
});
return arr;
})
.reduce((prev, curr) => prev.concat(curr), []),
this.createHeading("Experience"),
...experiences
.map((position) => {
const arr = [];
arr.push(
this.createInstitutionHeader(
position.company.name,
this.createPositionDateText(
position.startDate,
position.endDate,
position.isCurrent
)
)
);
arr.push(this.createRoleText(position.title));
const bulletPoints = this.splitParagraphIntoBullets(
position.summary
);
bulletPoints.forEach((bulletPoint) => {
arr.push(this.createBullet(bulletPoint));
});
return arr;
})
.reduce((prev, curr) => prev.concat(curr), []),
this.createHeading("Skills, Achievements and Interests"),
this.createSubHeading("Skills"),
this.createSkillList(skills),
this.createSubHeading("Achievements"),
...this.createAchivementsList(achivements),
this.createSubHeading("Interests"),
this.createInterests(
"Programming, Technology, Music Production, Web Design, 3D Modelling, Dancing."
),
this.createHeading("References"),
new Paragraph(
"Dr. Dean Mohamedally Director of Postgraduate Studies Department of Computer Science, University College London Malet Place, Bloomsbury, London WC1E d.mohamedally@ucl.ac.uk"
),
new Paragraph("More references upon request"),
new Paragraph({
text: "This CV was generated in real-time based on my Linked-In profile from my personal website www.dolan.bio.",
alignment: AlignmentType.CENTER,
}),
],
},
],
});
return document;
}
createContactInfo(phoneNumber, profileUrl, email) {
return new Paragraph({
alignment: AlignmentType.CENTER,
children: [
new TextRun(
`Mobile: ${phoneNumber} | LinkedIn: ${profileUrl} | Email: ${email}`
),
new TextRun({
text: "Address: 58 Elm Avenue, Kent ME4 6ER, UK",
break: 1,
}),
],
});
}
createHeading(text) {
return new Paragraph({
text: text,
heading: HeadingLevel.HEADING_1,
thematicBreak: true,
});
}
createSubHeading(text) {
return new Paragraph({
text: text,
heading: HeadingLevel.HEADING_2,
});
}
createInstitutionHeader(institutionName, dateText) {
return new Paragraph({
tabStops: [
{
type: TabStopType.RIGHT,
position: TabStopPosition.MAX,
},
],
children: [
new TextRun({
text: institutionName,
bold: true,
}),
new TextRun({
text: `\t${dateText}`,
bold: true,
}),
],
});
}
createRoleText(roleText) {
return new Paragraph({
children: [
new TextRun({
text: roleText,
italics: true,
}),
],
});
}
createBullet(text) {
return new Paragraph({
text: text,
bullet: {
level: 0,
},
});
}
createSkillList(skills) {
return new Paragraph({
children: [
new TextRun(skills.map((skill) => skill.name).join(", ") + "."),
],
});
}
createAchivementsList(achivements) {
return achivements.map(
(achievement) =>
new Paragraph({
text: achievement.name,
bullet: {
level: 0,
},
})
);
}
createInterests(interests) {
return new Paragraph({
children: [new TextRun(interests)],
});
}
splitParagraphIntoBullets(text) {
return text.split("\n\n");
}
createPositionDateText(startDate, endDate, isCurrent) {
const startDateText =
this.getMonthFromInt(startDate.month) + ". " + startDate.year;
const endDateText = isCurrent
? "Present"
: `${this.getMonthFromInt(endDate.month)}. ${endDate.year}`;
return `${startDateText} - ${endDateText}`;
}
getMonthFromInt(value) {
switch (value) {
case 1:
return "Jan";
case 2:
return "Feb";
case 3:
return "Mar";
case 4:
return "Apr";
case 5:
return "May";
case 6:
return "Jun";
case 7:
return "Jul";
case 8:
return "Aug";
case 9:
return "Sept";
case 10:
return "Oct";
case 11:
return "Nov";
case 12:
return "Dec";
default:
return "N/A";
}
}
}
Properties
-
create([experiences, educations, skills, achivements])
: a function that generates a document object containing the input information in a formatted manner. -
createContactInfo(phoneNumber, profileUrl, email)
: a function that creates a paragraph containing contact information. -
createHeading(text)
: a function that creates a heading with the specified text. -
createSubHeading(text)
: a function that creates a subheading with the specified text. -
createInstitutionHeader(institutionName, dateText)
: a function that creates a header for an institution with the specified name and date text. -
createPositionDateText(startDate, endDate, isCurrent)
: a function that creates a text indicating the start and end date of a job position, and whether it's current or not. -
createRoleText(role)
: a function that creates a text indicating a role. -
splitParagraphIntoBullets(paragraph)
: a function that split a paragraph into bullet points. -
createBullet(text)
: a function that creates a bullet with the specified text. -
createSkillList(skills)
: a function that creates a list of skills. -
createAchivementsList(achievements)
: a function that creates a list of achievements. -
createInterests(interests)
: a function that creates a list of interests.
Now create an instance and generate a doc.
const documentCreator = new DocumentCreator();
const doc = documentCreator.create([
experiences,
education,
skills,
achievements,
]);
const b64string = Packer.toBase64String(doc);
module.exports = b64string;
Purposes
- The code creates a new instance of the
DocumentCreator
class and uses itscreate
method to generate a document object. - The
create
method accepts an array of arrays containing the different sections of information, such as experiences, education, skills, and achievements. - The format and structure of the document object will depend on the implementation of the
DocumentCreator
class. - The code then uses the
Packer.toBase64String
function from thedocx
package to convert the document object to a base64 encoded string. - The base64 encoded string can be used to transmit the document to other systems or to be decoded and saved as a docx file.
Here is the back-end code
const cors = require("cors");
const express = require("express");
const app = express();
const b64string = require('./docxHtml');
app.use(
cors({
origin: "http://localhost:3000",
})
);
app.get("/doc", async (req, res) => {
res.setHeader("Content-Disposition", "attachment; filename=My Document.docx");
res.send(Buffer.from(await b64string, "base64"));
});
This code exports a base64 encoded string of a document object, and sets up a route on an express.js server to handle requests to download the document in the form of a docx file.
Purposes
- The code exports the base64 encoded string representation of the document object.
- When the route "/doc" is hit on the express.js server, the server sets the header of the response to indicate that the body is a downloadable attachment with the file name "My Document.docx".
- The server then sends the body of the response as a buffer containing the decoded base64 encoded string of the document.
- The route "/doc" can be accessed by making a GET request to the server at the specified endpoint.
- The code uses the
Buffer.from
method to convert the base64 encoded string to a buffer so it can be sent as a response.
Here is the full project. You can clone the project
-
git clone https://github.com/golammostafa13/generate-files
. cd generate-files
npm i
npm start
To start server:
cd server
npm i
npm start
Happy Coding :)
Top comments (0)