npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2024 – Pkg Stats / Ryan Hefner

react-reports

v1.0.5

Published

Generate Pixel-Perfect A4 Print Ready / PDF Reports

Downloads

8

Readme

React-Reports

Generate Pixel-Perfect A4 PDF/Print-Ready Reports

react-reports is a library meant to help you generate print-ready reports that will bring immediate value to you customers.

  • It creates A4 documents that are ready for print.
  • Easy conversion to PDF using Puppeteer, Selenium, etc..

Features

  • Automatic table of contents.
  • Automatic page splitting.
  • Built-in async support.
  • Built-in API integration.

Installation

Easy to install:

yarn add react-reports
npm install react-reports

Usage Example:

The following example will use the component to split your elements into multiple pages:

  • It will split it into 2 different pages.
  • Page one includes the "One" and "Two" divs.
  • Page two includes the "three" and "four" divs.
  • Table of contents is generated automatically using the component.
  • You must wrap your report with the context .
function App() {
  return (
    <div className='App'>
      <ReportProvider>
        <TableOfContents />
        <PageGroup name='Automatic Page Split'>
          <div style={{ height: '300px', width: '100%', background: 'red' }}>One</div>
          <div style={{ height: '700px', width: '100%', background: 'blue' }}>Two</div>
          <div style={{ height: '500px', width: '100%', background: 'yellow' }}>Three</div>
          <div style={{ height: '200px', width: '100%', background: 'green' }}>Four</div>
        </PageGroup>
      </ReportProvider>
    </div>
  );
}

export default App;

Component API

ReportProvider

The report provider stores all the relevant logic needed to generate the report. It receives a config object that has the following properties:

The following should be passed to the ReportProvider using the "config" property.

| Property | Usage | | | ------------- | ---------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | initialValues | Accepts an array of objects { putOnProp: string, value: any } | Data can be accessed using the useReport() Hook. | | | putOnProp | set the property the initial value will be set on | | | value | The value being set on the putOnProp property | | apis | Accepts an array of objects { request: Promise, processingFunction: (response) => response), putOnProp: string } | Data can be accessed using the useReport() Hook. | | | request | Pass the promise you wish to call | | | processingFunction | Process the promise response before the values are being set in the context | | | putOnProp | set the property the processed response will be set on | | loader | Override default loader, { text: string, component: Reference to a component} | Default loader displays: "Generating Report" | | | text | Override the default loader text | | | component | Replace the loader with your own custom loader | | header | Override default page header, { display: boolean, component: Reference to a component, height: number } | Each page has a default header, you can override it with a custom header using this property. | | | display | Show / Hide the header. | | | component: ({pageName,pageNumber,headerClass }) => Component | Pass a reference to custom header component, your component will be injected with the props: "pageName" and "pageNumber", you can style the default header using the "headerClass". | | | height | Currently the height of the header must be pre-calculated by the developer, please set the height of the component including padding, margins and border, to correctly calculate the pages. | | footer | Override default page footer, { display: boolean, component: Reference to a component, height: number } | Each page has a default footer, you can override it with a custom footer using this property. | | | display | Show / Hide the footer. | | | component: ({pageName,pageNumber,footerClass}) => Component | Pass a reference to custom footer component, your component will be injected with the props: "pageName" and "pageNumber", you can style the default footer using the "footerClass". | | | height | Currently the height of the footer must be pre-calculated by the developer, please set the height of the component including padding, margins and border, to correctly calculate the pages. |

useReport()

The report initial values and apis being set in the config could be accessed by your components using the useReport() hook. | Property | Usage | | ------ | ------ | | initial | All the initial values passed here will be put on an object with the relevant property names you've passed to "putOnProp" property and their matching values. | requests | All the apis you've set, after being called and processed will have their responses set here, they will be using the "putOnProp" property | rejectedRequests | All apis that were rejected will have their error set here under the relevant "putOnProp" property |

The initial values and apis you set will be called before all report page processing is started, this means the report waits for all the requests to return a response until it starts working on your report.

PageGroup

The core component of the library, in charge of creating the pages based on the height of your components.

  • The component has several rendering phases it takes to render your pages for print.
  • It will first measure all the direct children heights during the "MEASURE" phase.
  • it will then calculate the pages based on their heights and including your usage of the "header", footer and "repeating" component as described below, during the "SPLIT_TO_PAGES" phase.
  • It will then reach the "PAGES_READY" phase and render your pages.

    Different phase will render your components FROM SCRATCH.

| Property | Usage | | --------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | name | Pass the name that will be injected to the header and footer components. | | maxPages | Limit the number of pages the PageGroup generates, when dealing with a large amount of data such as displayed tables, you might decide to only display the first 2 pages instead of dozens of pages it is going to generate. | | repeating | { top: { component: Reference to component, height: number }, bottom: { component: Reference to component, height: number } }, each page can have repeating component that will be displayed at the top and bottom of each page being created, could be useful for tables, when you wish to have the table header appear at every page. |

Usage:

// Usage of repeating components of the PageGroup:
const repeatingComponents = {
  top: {
    component: CustomRepeatingTop,
    height: 50,
  },
  bottom: {
    component: CustomRepeatingBottom,
    height: 50,
  },
}
<ReportProvider>
    <PageGroup name='Automatic Page Split' maxPages={5} repeating={repeatingComponents}>
      <div style={{ height: '300px', width: '100%', background: 'red' }}>One</div>
      <div style={{ height: '700px', width: '100%', background: 'blue' }}>Two</div>
      <div style={{ height: '500px', width: '100%', background: 'yellow' }}>Three</div>
      <div style={{ height: '200px', width: '100%', background: 'green' }}>Four</div>
    </PageGroup>
</ReportProvider>

Page

The page component can be used to create custom pages, it can receive the page numbers automatically, or be left outside of the page numbers calculation.

This component will not split into multiple pages, if such functionality is needed use the component instead.

The component is built using this component.

| Property | Usage | | ------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | name | string, Pass the name that will be injected to the header, footer and repeating components. | | automaticPageNumber | boolean, by default set to true, and will receive a page number automatically, when set to false it will not receive a page number. | | pageId | string, when setting the automaticPageNumber to false you page "id" attribute will need a unique identifier, you can set it using this property. | | repeating | { top: { component: Reference to component, height: number }, bottom: { component: Reference to component, height: number } }, each page can have repeating component that will be displayed at the top and bottom of each page being created, could be useful for tables, when you wish to have the table header appear at every page. | | showHeader | boolean, decide whether to display the header or not. | | showFooter | boolean, decide whether to display the footer or not. |

Table Of Contents

A Component that automatically generates a table of contents based on the components you've used. Currently, single pages will be left out of the table of contents.

The table of contents will have links generated directing you to the relevant page.

<TableOfContents />

This component will not split into multiple pages, if such functionality is needed use the component instead.

Asynchronous behavior

The library prefers that all your async API operations will be dealt with by passing the relevant Promises to the "apis" config property, and then accesing their values by using the useReport() hook.

This makes sure that the page calculation will be based on the actual space your component takes out of the page. Sometimes you will still have component that will finish their rendering after an async operation of unknown time. To allow for asynchronous behavior, I'm delayed the "MEASURE" phase in the component, that is until you specifically notify the height of your elements when they are rendered to the screen, and you must also save their state using the provided functions.

measureAsync

The component detects a direct child with the "measureAsync" property and injects relevant properties to handle the async operations.

A direct child meaning a child of :

<PageGroup name='Group One' repeating={PageGroupRepeating}>
  <div className='one'>Page One - with margins</div>
  <AsyncChild measureAsync /> // Asynchronous DIRECT child
  <div style={{ height: '700px', width: '100%', background: 'blue' }}>Two</div>
 </PageGroup>

The "measureAsync" property will inject into your custom child component the following properties:

| Property | Usage | | -------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | _notifyHeight | After your async operations are complete use this function to notify the of your component height, page generation will not proceed until this is done. | | _saveState | In order to avoid duplicate async operations, save any of your component state using this function. | | _savedState | Any state saved will be available for your async component later. |

When the report is generating the page between the "MEASURE" phase and "PAGES_READY" phase, ENTIRELY new children are created causing async children to mount again from scratch, to avoid these issues, use the _saveState and _savedState properties.

Usage:

export const AsyncChild = ({ _notifyHeight, _saveState, _savedState }) => {
  const [texts, setTexts] = useState(_savedState ? _savedState.texts : []);
  const asyncElement = useRef();
  const timeoutId = useRef();

  // Async loading data
  useEffect(() => {
   // Simple async operation, mimicking an API request fetching texts.
    timeoutId.current = setTimeout(() => {
      setTexts(['Async text one', 'Async text two', 'Äsync text three']);
    }, 2000);

    // Cleaning up after the our setTimeout operation incase this component is re-rendered .
    // This avoids the error of: You are trying to set the state of an unmounted component.
    return () => {
      clearTimeout(timeoutId.current);
    };
  }, []);

  useEffect(() => {
    // 1 - You must notify the height of the child only after it has rendered all
    //     of its async behavior, APIs, images, text, etc...
    // 2 - You must also save your async information using _saveState to avoid recalling the state
           in the next child creation (between phases).
    // If our fetched texts are available and we have nothing in the _savedState property,
    // Notify the height of the component, and save its state once.
    if (texts.length > 0 && !_savedState) {
    // Use the measureHeight function to get the correct height of your elements.
      _notifyHeight && _notifyHeight(measureHeight(asyncElement.current));
      _saveState &&
        _saveState({
          texts,
        });
    }
  });

  return (
    // Use Overflow auto for correct height calcualtions
    // Referencing it will return an element with correct height.
    <OverflowAuto ref={asyncElement}>
      <div className={'async-text'}>
        {texts.map((text, index) => {
          return (
            <h2 key={index}>
              {index + 1}. {text}
            </h2>
          );
        })}
      </div>
    </OverflowAuto>
  );
};

measureHeight

Use the measureHeight to correctly measure your elements height, and notify the Pass it a refernce to a DOM element.

OverflowAuto

To make sure that the height is being calculated correctly use the component to wrap you child with a div with "overflow: auto", this makes sure that your child component will help correct height measurements of margins,padding and border.

You must pass a reference using "useRef" to access that childs height and notify the .

Summary of Asynchronous Behavior

By Combining all of the above AsyncMeasure, measureHeight, OverflowAuto, _notifyHeight, _saveState, and _savedState you have complete control over asynchronous operation allowing you to generate the report pages with async components.

Remember that asyncMeasure is only available as a direct child of the component.

AsyncImage

To correctly calculate the height of remote images I've created the AsyncImage component, It implements all the above asynchronous behavior I've mentioned, and must be used as a direct child of the component.

Usage:

  <AsyncImage
    measureAsync
    url='https://media-cdn.tripadvisor.com/media/photo-s/1a/86/7c/6d/img-src-x-onerror-alert.jpg'
    imageProps={{ alt: 'Alt tag' }}
  />

Breakdown:

  • Again measureAsync is used to indicate that we are dealing with an async child.
  • A remote image is being loaded using the url property.
  • You can pass additional props to the element using imageProps.

You can use regular images when you are sure they have already been loaded in memory, or by hard-coding them. That is without using the AsyncImage component.

<img src='data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAA.....'/>

PageSize

The PageSize enum holds the suppored page size, and is accessible to import. Currently only A4 documents are supported and the size is 793X1120. This size is used to split the component into different pages based on their heights.

Generating a PDF from the report

Some of the magic of the react-reports library is by how easy it is to create a pdf file with the generated report.

When the report is ready for print / pdf generation it will add the following element to the DOM, allowing your chosen library to create the PDF file once it appears:

<div id="rr-ready-for-print"></div>

Example using Puppeteer:

Puppeteer is using NodeJS, if you are unfamiliar with it, it is a head-less browser, meaning it runs a Chromium based browser on your machine, allowing you full control over its opeartions.

Here it will be used to generate a PDF file.

Save the following file under index.js, and use this command:

node index.js <Application route of the report> <optionalPathToFile><fileName.pdf> <DOM element to wait for>
Usage:
node index.js http://localhost:3000/pathToReport file.pdf "#rr-ready-for-print"
const puppeteer = require('puppeteer');

(async () => {
 // Accepts command line arguments to generate the report:
  const userArgs = process.argv.slice(2);
  const location = userArgs[0];
  const path = userArgs[1];
  const waitForSelector = userArgs[2];

  const browser = await puppeteer.launch({
    //If you are using a self-signed certificate, you can ignore it using this property.
    ignoreHTTPSErrors: true,
    headless: true,
  });

  try {
    const page = await browser.newPage();

    // Set the browser viewport.
    await page.setViewport({
      width: 1920,
      height: 1080,
      deviceScaleFactor: 1,
    });

    // Enter your report at the relevent page,
    // Use all available page loaded events to make sure all components have been rendered.
    await page.goto(location, { waitUntil: ['load', 'domcontentloaded', 'networkidle0', 'networkidle2'] });

    // Wait for '#rr-ready-for-print' element to appear,
    // Indicating that the report is ready for print/PDF generation.
    await page.waitFor(waitForSelector, { timeout: 600000, visible: true });
    await page.emulateMediaType('print');

    // Using a safe buffer to letting any rendering that occurs to finish,
    // even though it should be ready.
    await page.waitFor(400);

    // Create a path and save to the relevant path:
    await page.pdf({ path: path, format: 'A4' });

    // PDF was saved close the head-less broswer.
    await browser.close();

  } catch (e) {
    console.log('Found Error:' ,e);
    browser.close();
  }
})();

Pitfalls

The library is not yet complete and has the following issues:

  • You cannot have text writting as a direct child of the component, it will crash the report.