Hello World
Hello World

Reputation: 144

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.

Upvotes: 0

Views: 2817

Answers (3)

Lucas Paix&#227;o
Lucas Paix&#227;o

Reputation: 981

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

Upvotes: 3

jawkhan
jawkhan

Reputation: 57

From what I understand, you're essentially trying to take a screenshot of an area and send that in the form data.

You can use a library called use-react-screenshot. It's really easy to implement and you'll need to set the reference area using the createRef hook from react.

Once you've got the screenshot, you can silently upload that somewhere to a database, alongside that form data, so that you can reference the screenshot when looking at the form data.

With the current method, I don't think there is a way to set the file input programmatically.

I hope this helps.

Upvotes: -1

Honza Kvoch
Honza Kvoch

Reputation: 91

You cannot set the value of a file input unfortunately, even if you give it a "file" yourself. You will have to intercept the form submission and construct your own request while adding the attachment. FormData API is useful for that.

Here's the link to the updated sandbox: https://codesandbox.io/s/html2canvas-jspdf-forked-eun59q?file=/src/index.js
And here's the used jsPDF function docs: https://artskydj.github.io/jsPDF/docs/jsPDF.html#output

Upvotes: 1

Related Questions