Print PDF with jsPDF

Hi all,
Following Use PDF Exporter to print content of table - #13 by PatrickMast
I'm using jsPDF to print tables for FREE with out API and it's work great, attached example of the PDF document and the code

I have an issue with tables that are more than 3 pages, I don't understand where is the limitation !!!


this is the JS code:

async function generatePDF() {
console.log("Starting PDF generation process...");
const { jsPDF } = window.jspdf;
if (!jsPDF) {
console.error("jsPDF not loaded");
return;
}

const doc = new jsPDF({
orientation: "landscape",
unit: "mm",
format: "a4",
putOnlyUsedFonts: true,
floatPrecision: 16
});

const pageHeight = doc.internal.pageSize.getHeight();
const pageWidth = doc.internal.pageSize.getWidth();

const formatDate = (dateString) => {
const d = new Date(dateString);
return ${String(d.getDate()).padStart(2, '0')}.${String(d.getMonth() + 1).padStart(2, '0')}.${String(d.getFullYear()).slice(-2)} ${String(d.getHours()).padStart(2, '0')}:${String(d.getMinutes()).padStart(2, '0')};
};

// Logo, Header, and Title
const addPageHeader = (pageNum) => {
doc.addImage(QSA_Logo.value, 'PNG', 8, 8, 20, 9);
const currentDate = formatDate(new Date());
doc.setFontSize(8);
doc.text(currentDate, pageWidth - 15, 15, null, null, "right");
doc.setFontSize(11);
doc.text('Document List', pageWidth / 2, 18, null, null, "center");
};

const addPageFooter = (pageNum) => {
doc.setFontSize(8);
doc.text(Page ${pageNum}, pageWidth - 10, pageHeight - 10, null, null, "right");
doc.text("Generated by QSA-Soft https://qsa-soft.online", pageWidth / 2, pageHeight - 10, null, null, "center");
};

let currentPage = 1;

// Table Content
const visibleFields = [
{ field: "doc_category", display: "Category" },
{ field: "doc_no", display: "Number" },
{ field: "doc_ver", display: "Version" },
{ field: "doc_status", display: "Status" },
{ field: "doc_title", display: "Title" },
{ field: "open_date", display: "Open Date" },
{ field: "last_update", display: "Last update" },
{ field: "update_by", display: "Update By" },
{ field: "doc_link", display: "Word Link" },
{ field: "pd_flink", display: "PDF Link" },
{ field: "doc_rem", display: "Remarks" }
];

const tableHeaders = visibleFields.map(field => field.display);
const fieldKeys = visibleFields.map(field => field.field);

const sortedData = DOC_table.data.sort((a, b) => a.doc_category - b.doc_category);
const tableData = sortedData.map(row => {
return fieldKeys.map((field) => {
if ((field === "doc_link" && row.doc_link) || (field === "pd_flink" && row.pd_flink)) {
return "link"; // Handle links later
}
if (field === "open_date" || field === "last_update") {
return formatDate(row[field]);
}
return row[field] || "";
});
});

const columnStyles = {
doc_category: { cellWidth: 10 },
doc_no: { cellWidth: 10 },
doc_ver: { cellWidth: 25 },
doc_status: { cellWidth: 25 },
doc_title: { cellWidth: 10 },
open_date: { cellWidth: 10 },
last_update: { cellWidth: 10 },
update_by: { cellWidth: 10 },
doc_link: { cellWidth: 10 },
pd_flink: { cellWidth: 10 },
doc_rem: { cellWidth: 10 }
};

const rowStyles = {
fillColor: (rowIndex) => (rowIndex % 2 === 0 ? [220, 220, 220] : [255, 255, 255])
};

// Increase page break handling for large datasets
const maxHeight = pageHeight - 40; // Adjust this to fit content more efficiently

// Wrap the table generation inside a setTimeout to allow async handling
setTimeout(() => {
doc.autoTable({
startY: 30,
head: [tableHeaders],
body: tableData,
styles: { overflow: 'linebreak', cellPadding: 2 },
headStyles: { fontSize: 9, fillColor: [0, 102, 204] },
bodyStyles: { fontSize: 8, textColor: [10, 10, 10] },
columnStyles: columnStyles,
rowStyles: {
fillColor: (rowIndex) => rowStyles.fillColor(rowIndex)
},
margin: { top: 30, bottom: 10, left: 10, right: 10 },
maxHeight: maxHeight, // Limit the height of the table content per page
didDrawCell: function(data) {
const rowIndex = data.row.index;
const columnIndex = data.column.index;

    if (fieldKeys[columnIndex] === "doc_link") {
      const linkUrl = sortedData[rowIndex].doc_link;
      if (linkUrl) {
        doc.link(data.cell.x, data.cell.y, data.cell.width, data.cell.height, { url: linkUrl });
      }
    }
    if (fieldKeys[columnIndex] === "pd_flink") {
      const linkUrl = sortedData[rowIndex].pd_flink;
      if (linkUrl) {
        doc.link(data.cell.x, data.cell.y, data.cell.width, data.cell.height, { url: linkUrl });
      }
    }
  },
  didDrawPage: function(data) {
    addPageHeader(currentPage);
    addPageFooter(currentPage);
    currentPage++;
  }
});

const mainImage = SignatureBackground.value;
if (mainImage) {
  const imgX = 20;
  const imgY = pageHeight - 40;
  const imgWidth = 40;
  const imgHeight = 15;
  doc.addImage(mainImage, 'PNG', imgX, imgY, imgWidth, imgHeight);

  const name = G_GetUser_info.data.full_name[0];
  doc.setFontSize(9);
  doc.setFont("times", "italic");
  doc.setTextColor(50, 50, 50);
  doc.text(name, imgX + 8, imgY + 8);

  doc.setFontSize(9);
  doc.setTextColor(69, 69, 69);
  doc.text(formatDate(new Date()), imgX + 18, imgY + 14, null, null, "center");
}

doc.save('Document List.pdf');
console.log("PDF generated successfully!");

}, 500); // Delay of 100ms, adjust as needed
}

// Call the function to generate the PDF
generatePDF();

1 Like

Hello @Avner1!

I am not familiar with jsPDF :sweat_smile:

What error are you getting with tables that are more than three pages?

Are the other pages from the table just not showing up? Are you able to increase the table size to reduce the number of pages?

Hi Jay, already solved it. Thanks.

Avner Avrahami
+972 54 228 1158

1 Like

@Avner1 Great to hear!

Feel free to share your solution in case there are other users that run into similar issues :sweat_smile:

Handling PDFs has been somewhat tricky at times with Retool so this is great you have a solution working with jsPDF :tada:

SURE
async function generatePDF() {
console.log("Starting PDF generation process...");

const { jsPDF } = window.jspdf;
if (!jsPDF) {
    console.error("jsPDF not loaded");
    return;
}

const doc = new jsPDF({
    orientation: "landscape",
    unit: "mm",
    format: "a4",
    putOnlyUsedFonts: true,
    floatPrecision: 16
});

const notoSansVariableFontBase64 = ArielFont.value; // Replace this with the actual Base64 encoded string

// Add the font to VFS (Virtual File System)
doc.addFileToVFS("Helvetica World.ttf", notoSansVariableFontBase64);

// Register the font
doc.addFont("Helvetica World.ttf", "NotoSansVariable", "normal");

// Set the font to the new one
doc.setFont("NotoSansVariable");

console.log(doc.getFontList()); // Check if "NotoSans" appears in the console output

function capitalizeFirstLetter(str) {
return str.charAt(0).toUpperCase() + str.slice(1);
}

const allTableColumns = Object.keys(DOC_tablePDF.data[0]).filter(key => !['id', 'updater', 'company'].includes(key));
console.log("allTableColumns :", allTableColumns);

const columnsToFilter = DOC_table_column_multiSelect.value;
console.log("columnsToFilter :", columnsToFilter);

const tableHeader = allTableColumns.filter(key => !columnsToFilter.includes(key))
  //  .map(key => capitalizeFirstLetter(key));
console.log("Table Header:", tableHeader);

const tableHeaderCapital = allTableColumns.filter(key => !columnsToFilter.includes(key)).map(key => capitalizeFirstLetter(key));
console.log("Table Header:", tableHeader);

// Function to format date to "DD.MM.YY HH:MM"
const formatDate = (dateString) => {
if (!dateString) return ""; // Return an empty string if the date is null or undefined

   try {
       const d = new Date(dateString);
       // Check if the date is valid
       if (isNaN(d.getTime())) return ""; // Return empty if date is invalid

       return `${String(d.getDate()).padStart(2, '0')}.${String(d.getMonth() + 1).padStart(2, '0')}.${String(d.getFullYear()).slice(-2)} ${String(d.getHours()).padStart(2, '0')}:${String(d.getMinutes()).padStart(2, '0')}`;
   } catch (error) {
       console.error("Error formatting date:", error);
       return "";
   }

};

// Create an object to store the link cells with their real URLs
const linkCells = [];

// Generate table data and apply date formatting to columns that contain "date"
const tableData = DOC_tablePDF.data.map((row, rowIndex) => 
    tableHeader.map((col, colIndex) => {
        let value = row[col];
        // If the value is a URL, store the real URL and replace the cell content with 'link'
        if (typeof value === 'string' && value.startsWith('http')) {
            
            // Store the real URL and the cell position in the linkCells array
            linkCells.push({
                row: rowIndex,
                col: colIndex,
                url: row[col] // This is the real URL
            });

          value = 'link'; // Replace with 'link' text
            row[col] = value; // Update the row data with 'link' text
          }

     if (col.toLowerCase().includes("updated") || col.toLowerCase().includes("initiated")) {
      return formatDate(value);  // If condition is met, format the date
      } else {
      return value;  // Otherwise, return the value as it is
  }}));



console.log("Final Filtered Table Data:", tableData);
console.log("Link Cells Array:", linkCells);

const pageHeight = doc.internal.pageSize.getHeight();
const pageWidth = doc.internal.pageSize.getWidth();

const addPageHeader = () => {
    doc.addImage(QSA_Logo.value, 'PNG', 8, 8, 20, 9);
    const currentDate = formatDate(new Date());
    doc.setFontSize(8);
    doc.text(currentDate, pageWidth - 15, 15, null, null, "right");
    doc.setFontSize(11);
    doc.text("Documents List", pageWidth / 2, 18, null, null, "center");
};

const addPageFooter = (doc, pageWidth, pageHeight) => {
    const pageNo = doc.internal.getCurrentPageInfo().pageNumber;
    doc.setFontSize(8);
    doc.text(`Page ${pageNo}`, pageWidth - 10, pageHeight - 10, null, null, "right");
    doc.text("Generated by QSA-Soft https://qsa-soft.online", pageWidth / 2, pageHeight - 10, null, null, "center");
};

doc.autoTable({
    startY: 25,
    head: [tableHeaderCapital],
    body: tableData,
    styles: { overflow: 'linebreak', cellPadding: 1 },
    headStyles: { fontSize: 8, fillColor: [0, 102, 204] },
    bodyStyles: { fontSize: 8, textColor: [10, 10, 10] },
    theme: 'striped',
    margin: { top: 20, bottom: 20, left: 10, right: 10 },
    
    didDrawCell: function(data) {
        const rowIndex = data.row.index;
        const columnIndex = data.column.index;
      

        // Check if this cell is a link and if we have a URL stored in linkCells
        linkCells.forEach(linkCell => {
            if (linkCell.row === rowIndex && linkCell.col === columnIndex) {
                const url = linkCell.url;
                const x = data.cell.x;
                const y = data.cell.y;
                const width = data.cell.width;
                const height = data.cell.height;
                doc.link(x, y, width, height, { url: url }); // Add the actual link

            }
        });
    },

    didDrawPage: function (data) {
        addPageHeader(doc, pageWidth, pageHeight);  
        addPageFooter(doc, pageWidth, pageHeight);
    }
});

// Add a signature image to the document, if available
const mainImage = SignatureBackground.value;
if (mainImage) {
    const imgX = 20;
    const imgY = pageHeight - 40;
    const imgWidth = 40;
    const imgHeight = 15;
    doc.addImage(mainImage, 'PNG', imgX, imgY, imgWidth, imgHeight); // Signature image

    const name = G_GetUser_info.data.full_name[0]; // User name from data
    doc.setFontSize(9);
    //  doc.setFont("times", "italic");
    doc.setTextColor(50, 50, 50);
    doc.text(name, imgX + 8, imgY + 8); // User name next to signature

    doc.setFontSize(9);
    doc.setTextColor(69, 69, 69);
    doc.text(formatDate(new Date()), imgX + 18, imgY + 14, null, null, "center"); // Date next to signature
}

doc.save('Document List.pdf');

}
generatePDF();

1 Like

Does Retool have a more comprehensive step by step guide on how to configure something like this?

Hi @hexx,

One of our partners, Bold Tech has a nice article about working with PDFs and Retool here.

Here are our official docs for working with PDFs here.

Another user from the forum post that @Avner1 linked above reported that they used https://craftmypdf.com/ and it worked well for them.

What exactly are you looking to do? Print a PDF from a Retool table? Did Avner1's solution not work for you?

I just want to generate PDF invoice based on data from selectedrow in the table. That's all. I looked at all the docs and a few forum posts. It all seem kind of dramatic. I will take a look at the other links you provided. Thanks!

1 Like

I can format what I want in a simple container but I can't find a way to even print contents from a container. There are no print components in Retool.

Yes unfortunately PDFs are tricky to work with from a coding standpoint but we have gotten a lot of feedback on this and our engineering team is working on rolling out some feature to make this easier and streamline the process :sweat_smile:

There are a couple util methods that might help, but you will need to download the file/data/pdf from the retool app to your local machine and then print this out separate from Retool, as we do not have any print events/options/integrations currently :sweat_smile:

Check out the docs here for utils.exportData, utils.downloadFile and utils.downloadPage as these are the best options we have for getting your data from a Retool app to there it needs to be to convert to a PDF to then print.

Hope that helps :crossed_fingers:

1 Like

@Jack_T Well I was able to format the data in my container and created a button for print view which pops up a modal. Within the modal I have Print button. I just can't get a way to reference the container data properly for printing because I keep getting containerid not found. There's gotta be a non-convoluted way to do just a simple print window. This is nuts.

Hi @hexx,

Apologies, Retool was not built with printing use cases in mind. But we do have an ticket for our engineering team to add this functionality, so I can add a +1 to this ticket for you and let you know if i hear any updates on this.

Containers are useful for displaying other components, but you can't key into them to access the internal components or their data.

I would say that if you have a modal, a component that displays a preview of the data and a print button, you can add a component with the data you want to print into this modal and set Appearance->Hidden->true so that it doesn't take up a bunch of space in the modal, but the data will be accessible in the modal.

If you can share a screenshot of the script/event to print on button click, the code where you are getting 'containerId not found' and the modal I might be able to help further.

You may need to directly reference the component/data inside the container or move it out of a container but not sure which makes more sense for your use case :sweat_smile:

Ok yeah I was thinking if I could figure out how to render to html or something flat. thanks for the follow up. I'll keep tinkering and post screenshot later.

1 Like

@Jack_T Here is a screenshot. Basically, I have a container with data and a button to click for print view. The print view is the modal which has a print button. I will provide the code as well for the print button which responds that the container is not found.

Code for print action

    const container = document.getElementById("cointainer2"); // Replace with your actual container ID
    if (container) {
        const printWindow = window.open("", "_blank");
        printWindow.document.write("<html><head><title>Print</title></head><body>");
        printWindow.document.write(container.outerHTML); // Copies only the container content
        printWindow.document.write("</body></html>");
        printWindow.document.close();
        printWindow.print();
    } else {
        console.error("Container not found for printing.");
    }
}

printContainer();

Hi @hexx,

Thank you for sharing this. Helps clear up a lot of questions on my end.

From scouring the documents, asking engineers internally and looking through the forums, unfortunately there is no way to print PDFs out of Retool. The reason for this is because for security purposes, all Retool code is executed in a sandboxed environment, which does not have access to your computer's operating system to trigger printing events.

I can make a feature request but at the moment it is likely not possible and there are better tools for a use case that needs to be printing table data :sweat:

The next best thing that can do done with Retool is to generate PDFs which can be downloaded to the local computer which is running a retool app. Then from there, a user can manually open the PDF and select the print options.

For your code, the error message seems to be coming from the document.getElementById() method. You may need to double check that you have the HTML elements ID. Just as a warning, using DOM methods in Retool is not fully supported as when we make updates and patches to frontend components there is a chance that these IDs can change which would be app breaking and need to be updated.

I wish printing from Retool was easier and will let you know if the ticket to add in this functionality has any updates.

ok thanks for the info. It doesn't even need to be pdf I just need to print was is on screen. I'm sure I'll figure something to rig this for my needs.

@Jack_T I ended up developing my solution outside of retool but integrated it a bit. I abandoned the whole PDF thing. Basically just created a simple nodejs app that has an API where I can pass the orderid in the URL and it loads the information using EJS template along with some custom CSS.

I click the orderid in the table:

Screenshot from 2025-03-02 18-14-21

I updated the orderid field event handler action which passes the order id in the URL:

Screenshot from 2025-03-02 18-27-09

It loads my page with the template containing data for the orderid record.

I have a print invoice button at the top of the page. It loads the print window where it has the custom CSS and EJS template allowing me to print. This is all I want. It's a roundabout way but I just want to print the page. This is definitely a capability Retool should have without having to do a lot for us to make it work.

2 Likes

Amazing work around @hexx!

I agree this should be easy to do in Retool. I am forwarding all of these details to our engineering team and hopefully we can add in a 'click to print' button with functionality to Retool and also allow for the styling as needed to correctly format the PDF.

Apologies for the inconvenience and I will hopefully have good news for these features coming out!

OK. Thanks for the follow up. With the solution I did and having the pop up print window I have an option from that window to save as PDF so for my scenario this is sufficient. However, there should be a better capability for directly creating the PDF.

1 Like