So I needed to create a button that generates a custom PDF for my weweb app from components already displayed on the screen. I looked into a few existing threads on how to do so, and most seemed like they had to utilize a backend(Xano) and/or third party plugins (paid or free) to achieve this. That was too complicated for my case so I found a way to do it all on the frontend side. Its not the best way but gets the job done, so I felt like I should share it since many people were interested in such functionality.
My approach:
I used html2pdf.js and html2canvas to capture components as images and add to the pdf.
I chatgpted my way through this code so I don’t expect it to be best practice or “proper” way to get this done.
Step 1: Create a custom weweb component.
Step 2: Add desired fonts and styling in the template section of the vue component to style the button appearance. (this was all I needed), colors and such are added later in css.
<template>
<link href="https://fonts.googleapis.com/css2?family=Suez+One&display=swap" rel="stylesheet">
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400&display=swap" rel="stylesheet">
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@500&display=swap" rel="stylesheet">
<button @click="generatePDF" class="pdf-button">
<img src="./pdff.png"
alt="PDF Icon" style="width: 18px; height: 18px; margin-right: 5px; margin-bottom: 2px;">
PDF
</button>
</template>
For the script part:
<script>
import html2pdf from "html2pdf.js";
import html2canvas from "html2canvas";
export default {
methods: {
generatePDF() {
const element1 = document.querySelector('[uid="10sft964b-262eer9-414ertertgbfa9"]');
const element2 = document.querySelector('[uid="4e708eregergb3e1d59erggerg0ae"]');
if (!element1 || !element2 ) {
console.error("Target element not found");
return;
//Here is where this gets tricky, you would need to grab the "**Section UID**"
and not the elements(divs and such). The querySelector() function was able to find/recognize
sections only and not individual components or containers. So my approach was if I had 4
different parts in a page where I want to add to the PDF, I would create a new section for each
part, so I have the freedom to move around these components in the PDF once captured.
//Another important part, make sure the section created for each component has a specified max
or fixed width, usually one that matches the component ur tryna capture. If you set it to auto or
100%, the components captured dimensions would differ based on the user's screen size.
// Function to capture an element and return a promise with the image
const captureElementAsImage = (el) => {
return html2canvas(el, { scale: 2 }).then(canvas => {
const imgData = canvas.toDataURL("image/png");
const imgElement = document.createElement('img');
imgElement.src = imgData;
imgElement.style.width = '100%'; // Scale to fit the PDF
imgElement.style.height = 'auto'; // Maintain aspect ratio
return imgElement;
});
};
// Create a temporary container for the PDF content
const container = document.createElement('div');
// Capture each element and append to container
captureElementAsImage(element1).then(imgElement => {
container.appendChild(imgElement);
// Here below I added empty divs to space elements from each other because Chatgpt couldnt
figure how to specifically assign each element to a separate page and provide a 1 inch margin around the page borders so I did this approach to manually achieve that.
If there is an easy way to apply 1 inch margin to the document and have element automatically get added to next page if it falls on that margin lmk in the comments!
//Create a new paragraph div with "space" and a 200px bottom margin
const spaceParagraph1 = document.createElement('div');
spaceParagraph1.innerText = 'space';
spaceParagraph1.style.marginBottom = '200px'; // Add bottom margin
spaceParagraph1.style.color = 'white';
container.appendChild(spaceParagraph1);
// Capture the second element and append to container
captureElementAsImage(element2).then(imgElement => {
container.appendChild(imgElement);
// PDF options
const options = {
filename: "Demographics_data.pdf",
image: { type: "jpeg", quality: 0.98 },
html2canvas: { scale: 2 },
jsPDF: { unit: "in", format: "letter", orientation: "portrait", margins: { top: 1, bottom: 1, left: 1, right: 1 } },
};
// this will add page numbering at the bottom before saving/downloading the pdf
html2pdf()
.from(container)
.set(options)
.toPdf()
.get('pdf')
.then((pdf) => {
const totalPages = pdf.internal.getNumberOfPages();
for (let i = 1; i <= totalPages; i++) {
pdf.setPage(i);
pdf.setFontSize(10);
pdf.setTextColor(100);
pdf.text(`Page ${i} of ${totalPages}`, (pdf.internal.pageSize.getWidth() / 2), (pdf.internal.pageSize.getHeight() - 0.75), { align: "center" });
}
})
.save()
.then(() => {
console.log('PDF generated'); // Log when PDF is generated
}).catch(error => {
console.error('Error generating PDF:', error);
});
});
});
});
}
},
};
</script>
//some styling for the button
<style scoped>
.pdf-button {
padding: 10px 12px;
font-family: 'Inter', sans-serif;
font-weight: 700;
font-size: 14px;
background-color: #d91c1f;
color: rgb(255, 255, 255);
border: none;
border-radius: 6px;
cursor: pointer;
display: flex; /* Use flex to align the icon and text */
align-items: center;
}
.pdf-button:hover {
background-color: #b51818;
}
.pdf-button span {
padding-bottom: 10px; /* Adjust this value to lift the text up */
}
</style>
After clicking this button, the pdf will generate and download right away, 100% client side rendering. Specifying the section width will have the PDF look consistent no matter the screen size of the end user.
Hope this helps someone, lmk in comments if theres any questions. Heres an example of an output using this approach: ( I coded extra stuff for title and date generated at the top)