1

I have code where I am showing multiple pages using next or previous buttons to see the other pages. The pages are shown and hidden using CSS. However, we want to download all the pages to a PDF book using jsPDF and html2canvas.

If you send the book element to jsPDF it will only show one page in the PDF. That's because on screen we are only showing 1 page at a time. If I show all the pages when you try to download the PDF (update the CSS) then it will save properly but will be a really weird UI experience.

I could create a separate JSX object that shows all the pages at once but never displays on the screen. Then I could access the pages programmatically, looping through the book element's children (the pages), creating images of each page and adding to the PDF book.

I've tried the above approach many times but each time I get an error about not being able to access the children. This is because the block never renders. How do I get a block like this to render without sending it to the screen?

I've been going crazy trying to figure out a solution to this!!

Here's some code to show what I'm trying:

const countChildren = (pdfRef) => {
    const numChildren = React.Children.count(pdfRef.current.props.children);
    const childContent = pdfRef.current.props.children.map((child) => child.props.children);
    return { numChildren, childContent };
};
function Book() {
  const pdfRef = useRef(null);

  const downloadPDF = async () => {
    const pdfDoc = new jsPDF({
      orientation: "landscape",
      format: printSize,
      unit: "in",
    });

    //content.children[0].children.length
    //pdfRef.current.children.length
    const { totalPages, childContent } = countChildren(pdfRef); // get total number of pages
    console.log('totalPages',totalPages);
    for (let i = 1; i <= totalPages; i++) {
      const pageContent = pdfRef.current.children[i - 1]; // get page content element
      const canvas = await html2canvas(pageContent); // convert page content to canvas
      const imgData = canvas.toDataURL("image/png"); // convert canvas to image data
      pdfDoc.addImage(imgData, "PNG", 0, 0, 11.7, 0, null, "SLOW");
      if (i < totalPages) {
        pdfDoc.addPage();
      }
    }

    pdfDoc.save("book.pdf");
  };

  const bookPDF = () => {
    return (
        <BookContainer className="pdf" ref={pdfRef}>
        {BookData.pages.map((item, index) => {
            return (
              <Page className="pdf" key={index} show={currentPage === index + 2 ? true : false}>
                  {item.animal} {item.environment} {item.copy}
              </Page>
            );
          })}
      </BookContainer>
    );
  };

return (
    <>
        <BookContainer>
          {BookData.pages.map((item, index) => {
            return (
              <Page key={index} show={currentPage === index + 2 ? true : false}>
                  {item.animal} {item.environment} {item.copy}
              </Page>
            );
          })}
        </BookContainer>
        <PageNavigation />
        <Button onClick={downloadPDF}>
          Download PDF
        </Button>
    </>
  );
}
Christine Wilson
  • 569
  • 2
  • 7
  • 14
  • You could probably set the `block` element to have the following CSS `position: abloslute; z-index: -1; visibility: hidden;` – Joel May 03 '23 at 10:16
  • The jsPDF will take into account the CSS styles applied to the block which means it'll be hidden while the other block will show. This is why I'm trying to create another block of elements that I never send to the screen but still render it so I can show all the pages without showing the user this longer version of the book – Christine Wilson May 03 '23 at 10:43
  • Could you render it to a string? https://react.dev/reference/react-dom/server/renderToString – Stuart Nichols May 03 '23 at 10:53
  • renderToString I looked into quite a bit but it requires DOMParser which is not supported by node. So that one doesn't actually work. So do we really need to view the JSX in order to run through the children elements? It looks like that's the general case. – Christine Wilson May 03 '23 at 13:20
  • @Joel Your solution did end up working. I just did the CSS so it always stays visible but off the page which worked like a charm (position: absolute; left: -10000px; top: auto;) If you add your comment as an answer I'll mark it the correct one. – Christine Wilson May 03 '23 at 14:42
  • 1
    I've put together a codesandbox using the 'renderToStaticMarkup' function. I've had to stub out a lot of your code, so can you please let me know if it is a good example of what you are trying to do. The only issue I've come across is that htmlToCanvas requires the HTML element to be rendered in the DOM in order to function https://stackoverflow.com/a/65632648/7172604. It has to be visible. Therefore you see I add and remove the element and you see a flash. I notice that you have already found a solution, but hopefully this helps somewhat. https://codesandbox.io/s/proud-fire-7juqc3 – Stuart Nichols May 03 '23 at 14:50
  • I could do a mix between css pushing it off page like before and this solution. What I did was to render the book twice - off screen showing all pages at once and on screen showing only one page at a time. I'm not a fan of the solution because I'm loading it twice before a person has chosen to download as a PDF. Your method would only render the second book when needed. You did a lot of work putting a test version up so thank you so much! And yes you're correct, I didn't include all my code because it was quite long.. so you're missing some of it – Christine Wilson May 03 '23 at 20:51
  • I just need time to actually implement your work into mine to test out the scenarios. I have an interview tomorrow so may be longer getting back :P – Christine Wilson May 03 '23 at 20:51

1 Answers1

1

As concluded by in the comments under the post, a simple solution is

.block {
    position: absolute; 
    z-index: -1; 
    left: -10000px;
    top: auto;
}
Joel
  • 353
  • 2
  • 7