Search code examples
javascriptreactjspdfjspdfhtml2canvas

Attach pdf to input in ReactJs


I am making a react application where I am in the need to generate a pdf from specific div.

Current scenario:

-> Made an app with a printable div and upon click button, the pdf is generated and downloaded.

Code:

  <div id="printable-div">
    <h1>Generate PDF</h1>
    <p>Create a screenshot from this div, and make it as a PDF file.</p>
    <p style={{ color: "red" }}>
      *Then do not download instead attach to contact us form as attachment.
    </p>
  </div>
  <button id="print" onClick={printPDF}>
    Contact Us
  </button>

printPDF Function:

  const printPDF = () => {
    setIsShowContact(true);
    const domElement = document.getElementById("printable-div");
    html2canvas(domElement).then((canvas) => {
      const doc = new jsPdf();
      doc.save(`attachment.pdf`);
    });
  };

On click of the Contact Us button, two actions happen.

-> Pdf file is downloaded.

-> A form with inputs Name, Email and Attachment will be shown.

Working Codesandbox:

Edit html2canvas-jspdf (forked)

Requirement:

Here the requirement is onclick of the Contact Us button, the pdf should be generated but not downloadable instead the generate pdf needs to be attached to the input type="file" in the contact form.

We need to send information of the data user have right now in the specific div id="printable-div" to backend api as pdf attachment on click of the contact button.

In real application, this is like an enquiry of a product, where user selects a product with some config's and finally that product info will be shown to user based on the config they choosen. Then the user will click Contact Us button, so the printable-div will have that product information which user searched, so we need to capture it as pdf and attach to input and send to backend on form submission.

Kindly help me with the inputs on achieving this scenario of making the generated pdf to attach as attachment to the input field.


Solution

  • The problem


    1. You need to convert the PDF correctly, although the intention is to attach the PDF in the input field, you are downloading a blank PDF.

    The first step would be to download the PDF correctly, referring to the DIV element whose id is printable-div, and after that, instead of downloading, attach it to the input field.

    The invalid code is here:

    const printPDF = () => {
       setIsShowContact(true);
       const domElement = document.getElementById("printable-div");
       html2canvas(domElement).then((canvas) => {
          const doc = new jsPdf(); <<- YOU`RE CREATING AN EMPTY PDF AND
          doc.save('attachment.pdf'); <<- DOWNLOADING THIS EMPTY PDF
       });
    };
    

    The solution is very simple, just use the argument canvas passed to the callback function instead of generating a new PDF

    2. You need to append the .files property and not add

    instead the generate pdf needs to be attached to the input type="file" in the contact form.

    It is impossible to add new items to the .files of input[type="file"] field that belongs to the FileList class, on the other hand, it is possible to change it, that is, remove the old FileList and attach a new one with the necessary file(s).

    Which in this example would just be a single file.

    The solution


    1. You need to convert the canvas that was passed as a callback from the html2canvas function to a file.

    You can do this in the following way:

    const canvas2file = (canvas, fileName = 't.jpg') =>
      new Promise(resolve => {
        canvas.toBlob((blob) => {
          resolve(new File([blob], fileName, { type: "image/jpeg" }))
        }, 'image/jpeg');
      })
    

    2. You need to use this function in the promise that is expected by the html2canvas function, that is:

    html2canvas(domElement)
       .then(canvas2file)
    

    3. You will need to get a reference (or document.querySelector / document.getElementXXX) to the input field whose type is file, and also a state variable for the file itself that was converted previously (by the canvas2file function), that is:

    function App() {
      ...
      const fileRef = useRef(); //Reference to input[type="file"]
      const [file, setFile] = useState(); //State variable that contains the File
      ...
    }
    

    4. Modify the printPDF function to save the File to the state variable

    const printPDF = () => {
        setIsShowContact(true);
        const domElement = document.getElementById("printable-div");
        html2canvas(domElement)
          .then(canvas2file)
          .then(setFile);
    };
    

    5. Use the useEffect hook to detect the change of the File in the state variable, that is, every time the user clicks on "Contact Us", a new File will be generated through the canvas2file function, and this file will be stored in the file state variable.

    After detecting this change, we remove the .files (of type FileList) from the input[type="file"] and we will re-attach a new FileList to the input, example:

    useEffect(() => {
        if(!fileRef.current) return;
        let list = new DataTransfer();
        list.items.add(file);
        fileRef.current.files = list.files;
        console.log(fileRef.current)
    }, [file])
    

    The code


    const { useEffect, useRef, useState } = React;
    
    const canvas2file = (canvas, fileName = 't.jpg') =>
      new Promise(resolve => {
        canvas.toBlob((blob) => {
          resolve(new File([blob], fileName, { type: "image/jpeg" }))
        }, 'image/jpeg');
      })
    
    function App() {
      const [isShowContact, setIsShowContact] = useState(false);
      const fileRef = useRef();
      const [file, setFile] = useState();
    
      useEffect(() => {
        if(!fileRef.current) return;
        let list = new DataTransfer();
        list.items.add(file);
        fileRef.current.files = list.files;
        console.log(fileRef.current)
      }, [file])
    
      const printPDF = () => {
        setIsShowContact(true);
        const domElement = document.getElementById("printable-div");
        html2canvas(domElement)
          .then(canvas2file)
          .then(setFile);
      };
    
      return (
        <div className="App">
          <div id="printable-div">
            <h1>Generate PDF</h1>
            <p>Create a screenshot from this div, and make it as a PDF file.</p>
            <p style={{ color: "red" }}>
              *Then do not download instead attach to contact us form as attachment.
            </p>
          </div>
          <br />
          <button id="print" onClick={printPDF}>
            Contact Us
          </button>
          <br />
          <br />
          <br />
          {isShowContact && (
            <form>
              <div id="contact">
                <div className="block">
                  <label>Name:</label>
                  <input type="text" defaultValue="John Doe" />
                </div>
                <br />
                <div className="block">
                  <label>Email:</label>
                  <input type="email" defaultValue="[email protected]" />
                </div>
                <br />
                <div className="block">
                  <label>Table pdf as attachment:</label>
                  <input ref={fileRef} type="file" />
                </div>
              </div>
            </form>
          )}
        </div>
      );
    }
    
    const rootElement = document.getElementById("root");
    ReactDOM.render(<App />, rootElement);
    label {
      display: inline-block;
      width: 75x;
      text-align: right;
    }
    <!DOCTYPE html>
    <html lang="en">
    
    <head>
        <meta charset="utf-8">
        <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
        <meta name="theme-color" content="#000000">
        <script src="https://unpkg.com/react@18/umd/react.production.min.js"></script>
      <script src="https://unpkg.com/react-dom@18/umd/react-dom.production.min.js"></script>
      <script src="https://cdnjs.cloudflare.com/ajax/libs/jspdf/2.5.1/jspdf.umd.min.js"></script>
      <script src="https://cdnjs.cloudflare.com/ajax/libs/html2canvas/1.4.1/html2canvas.min.js"></script>
      
        <title>React App</title>
    </head>
    
    <body>
        <noscript>
            You need to enable JavaScript to run this app.
        </noscript>
        <div id="root"></div>
        <!--
          This HTML file is a template.
          If you open it directly in the browser, you will see an empty page.
    
          You can add webfonts, meta tags, or analytics to this file.
          The build step will place the bundled scripts into the <body> tag.
    
          To begin the development, run `npm start` or `yarn start`.
          To create a production bundle, use `npm run build` or `yarn build`.
        -->
    </body>
    
    </html>

    Try this


    Due to Stackoverflow Sandbox policies, the code above will probably not run, for this reason I am hosting the working code on Codesandbox.

    Demo URL: https://codesandbox.io/s/html2canvas-jspdf-forked-5ennvt?file=/src/index.js