Search code examples
javascriptreactjspdfjspdfreact-to-pdf

React Component To PDF


I am working on a feature that will allow the user to take the last stage of their pipeline and get a nice PDF out of it.

I have the server and client all playing nicely. What I have been unable to accomplish is getting the whole document into the pdf. It only captures a small portion.

I don't need multiple pages although that would be nice. I just want to capture everything no matter how long the render might be. There is potential to have 100 items for example image and checkboxes next to them. I need a whole pdf of that and so far I can only get a snapshot of a little portion of it.

I tried messing with with height options and various packages with absolutely no luck.

I'll provide what the renderSimpleForm ends up looking like on the browser. and what it comes up as a PDF.


code & images


import { jsPDF } from "jspdf";
import * as htmlToImage from 'html-to-image';

        const handleSubmit = () => {
            // //Before sending the action show in progress text and disable button
            setPdf({inProgress: true})

            //Creating the pdf blob
            htmlToImage.toPng(document.getElementById('simpleForm'))
            .then(function (dataUrl) {
              var link = document.createElement('a');
              link.download = 'my-image-name.jpeg';
              const pdf = new jsPDF('p', 'mm', 'a4');
              const imgProps= pdf.getImageProperties(dataUrl);
              pdf.addImage(dataUrl, 'PNG', 0, 0);
              //Send action to server
              ClassInstancesActions.StorePassportPDFToDisk(pdf.output('blob'), productID)
            });
        }

        return (
            <div id="simpleForm" style={{ height: '9999px !important', width: '9999px !important' }}>
                {renderSimpleForm()}
                {showError()}
                {renderSubmitCancel(disableSubmit)}
            </div>
        );
    };
THIS IS THE OUPUT OF MY SUMIT BUTTON PDF how it comes out THIS IS HOW IT LOKS ON THE BROWSER How the Browser Looks with the submit button and items to convert to PDF


Solution

  • Ok after some time I have a solution that worked for my needs which were :

    1. Point to a div in a React Component, and pdf it
    2. Preserve styling

    and extra:

    1. Have the ability to programmatically customize and control with my metadata in a DB
    2. The customization was to add header, footer, page numbers, front page, and I can technically control every single page

    I used https://www.npmjs.com/package/@progress/kendo-react-pdf

    I used its savePDF function that allowed me to also have a template creating type functionality because it come free and is passed in as a prop in the options argument you'll see in the code.

    Also there is a css selector that targets JUST the PDF'ed div passed already no need to specify what to show and what not to show, so you can actually manipulate the PDF right before it comes out completely from here.

    In the video demo you'll see I have a MASSIVE div comes out to OVER 40 pages! Preserved my material and css styling and I could add whatever I want at will. I even added a watermark!

    Hope this helps people out!

    enter image description here

    //component that has the div i need pdf'ed
    //
    
    var reactToPdfUtils = require('../../../../../reactToPdf.js');
    
        const handleSave = (sourceElement) => {
            console.log('handleSave in SFV!');
            reactToPdfUtils.useSavePDFNOW(pdfProps, cb, sourceElement)
            function cb(sendDataContent){   
                console.log(sendDataContent)
            }
        };
        
    //////////////////////////////////////////////////////////////////////////////////////////////
    
    
    
    //reactToPdf.js      file that houses the library function I manipulate to get the result
    
    
    import React, { useEffect } from 'react';
    import { savePDF } from '@progress/kendo-react-pdf';
    import { drawDOM, exportPDF } from '@progress/kendo-drawing';
    
    export const useSavePDFNOW = (pdfProps, cb, sourceElement) => {
        //pdfprops i pass in from meta data in my mongoDB, 
        //I made a cb to let me know when the funtion is done
        //I also do elaborate checking here myself for example
        //to make sure there is only one div that that id 
       
            //create a template
            //this function is declared then passed in later to savePDF, 
            //the magic in the library allows    you to manipulate each page if you wanted. You can do some         //cool stuff here.
     try {
            const onEveryPage = (props) => {
                if (hasAfirstPage) {
                    if (props.pageNum === 1) {
                        document.querySelectorAll('#toPDF')[0].style.cssText = 'margin-top: -188.333px;height:100%;';
                        // document.querySelectorAll('kendo-pdf-page')[0].style.cssText = '';
                        return <Fragment></Fragment>;
                    } else {
                        return (
                            <div style={{ zIndex: '-999999', justifyContent: 'center', display: 'flex' }}>
                                <div id='header' style={{ textAlign: 'center', fontSize: headerFontToUse + 'px', backgroundRepeat: 'no-repeat', backgroundSize: '100%', backgroundColor: 'transparent', height: headerHeightToUse, width: headerWidthToUse, backgroundImage: `url(${headerSrcToUse})`, position: 'absolute', top: 10 }}>
                                    {headerTextToUse}
                                </div>
                                <div id='watermarkImage' style={{ backgroundRepeat: 'no-repeat', backgroundSize: '100%', backgroundColor: 'transparent', height: waterMarkHeightToUse, width: headerWidthToUse, opacity: 0.5 /* Firefox, Chrome, Safari, Opera, IE >= 9 (preview) */, backgroundImage: `url(${waterMarkSrcToUse})`, position: 'absolute', bottom: -90, left: 300 }}></div>
                                <div id='pageNums' style={{ position: 'absolute', bottom: 0, right: '10px' }}>
                                    Page {props.pageNum} of {props.totalPages}
                                </div>
                                <h6 id='footer' style={{ fontSize: footerFontToUse + 'px', backgroundColor: 'transparent', position: 'absolute', bottom: 0, margin: '6 auto' }}>
                                    {footerTextToUse}
                                </h6>
                            </div>
                        );
                    }
                } else {
                    return (
                        <div style={{ zIndex: '-999999', justifyContent: 'center', display: 'flex' }}>
                            <div id='header' style={{ textAlign: 'center', fontSize: headerFontToUse + 'px', backgroundRepeat: 'no-repeat', backgroundSize: '100%', backgroundColor: 'transparent', height: headerHeightToUse, width: headerWidthToUse, backgroundImage: `url(${headerSrcToUse})`, position: 'absolute', top: 10 }}>
                                {headerTextToUse}
                            </div>
                            <div id='watermarkImage' style={{ backgroundRepeat: 'no-repeat', backgroundSize: '100%', backgroundColor: 'transparent', height: waterMarkHeightToUse, width: headerWidthToUse, opacity: 0.5 /* Firefox, Chrome, Safari, Opera, IE >= 9 (preview) */, backgroundImage: `url(${waterMarkSrcToUse})`, position: 'absolute', bottom: -90, left: 300 }}></div>
                            <div id='pageNums' style={{ position: 'absolute', bottom: 0, right: '10px' }}>
                                Page {props.pageNum} of {props.totalPages}
                            </div>
                            <h6 id='footer' style={{ fontSize: footerFontToUse + 'px', backgroundColor: 'transparent', position: 'absolute', bottom: 0, margin: '6 auto' }}>
                                {footerTextToUse}
                            </h6>
                        </div>
                    );
                }
            };
            //MaterialUI font fixing may have to do this elsewhere too specific
            parentWrapper.querySelectorAll('[class*=MuiTypography], [class*=Text], [class*=formControl]').forEach(function (el, i) {
                el.setAttribute('style', 'color: black; overflow: visible');
            });
    
            //save pdf on client side, send blob to dev to figure out what he or she wants to do with it
            let pdfBlob;
            savePDF(
                parentWrapper,
                {
                    pageTemplate: onEveryPage,
                    paperSize: 'a4',
                    fileName: 'Testing',
                    keepTogether: '.menu',
                    scale: 0.6,
                    title: 'Ametek/Powervar Passport',
                    margin: { top: marginTopToUse, bottom: marginBottomToUse }
                },
                () => {
                    // Server side rendering tested comes out exactly the same so long as the props match
                    drawDOM(parentWrapper, { pageTemplate: onEveryPage, scale: 0.6, paperSize: 'A4', margin: { top: 180, bottom: 10 } })
                        .then((group) => {
                            return exportPDF(group);
                        })
                        .then((dataUri) => {
                            // Send action to database
                            pdfBlob = dataUri.split(';base64,')[1];
                            ClassInstancesActions.StorePassportPDFToDisk(pdfBlob, 'testing');
                            cb(pdfBlob);
                            parentWrapper.remove();
                        });
                }
            );
        } catch (error) {
            console.log(error);
        }
    };
    //This is in a style sheet
    //i have a div within the div I pdf with an id of #divToDisableInteraction
    //As long as there in there you can do any css magic to your pdf document here
    //I tested this to high heaven you can get most things done here
    //including adding an image url
    
    
    kendo-pdf-document #divToDisableInteraction {
        visibility: hidden;
      }