In this third blog post of the Generate PDF files in React series, we take a look at how to export content and data in advanced UI components like the Data Grid and TreeList.
Welcome to the Generating PDF in React blog post series!
Now we will dive even deeper into ways to export HTML and CSS to PDF using React. Specifically, we will see an example of exporting advanced React UI components such as the KendoReact Data Grid and React TreeList, along with all of their content, to a PDF file. Both React components have PDF export features built-in, making exporting as simple as clicking a button.
For the Data Grid, we will also go through the steps of adding paging with local data. This is to see how the available configuration options for generating a PDF file can determine if we export just the current view or all data available to the Data Table. Additionally, we can configure if we should just export the currently displayed data, all data bound to the component (including data on the server), and even change the look and feel of the PDF file as we generate it.
Before we get started, I highly recommend checking out the KendoReact Grid Getting Started page, which covers installation instructions and links to several helpful additional documentation articles that help with understanding the React Data Grid.
Everything below assumes that we have set up an existing React project. Specifically, a project set up using create react app
.
The first step is to install the right npm packages, which we can do by copying and pasting the npm install command from the article we just linked to.
npm install --save @progress/kendo-react-grid @progress/kendo-data-query @progress/kendo-react-data-tools @progress/kendo-react-inputs @progress/kendo-react-intl @progress/kendo-react-dropdowns @progress/kendo-react-dateinputs @progress/kendo-drawing @progress/kendo-react-animation @progress/kendo-licensing
We also need to install the theme that we want to use in our application. KendoReact comes with support for three design languages—the Kendo UI Default theme, Bootstrap or Material Design. In all the samples below we will be using the Material theme, but the styling and themes overview page contains instructions for how to install your favorite theme.
As mentioned, we’ll be using the Material theme so let’s install that as well.
npm install --save @progress/kendo-theme-material
Note: if you haven’t used KendoReact before you should also follow the license key activation steps highlighted in this article.
Once we’ve taken care of the installation steps we can add the appropriate import statements in our React app. In our App.js we can add the following:
import * as React from 'react';
import '@progress/kendo-theme-material/dist/all.css';
import './style.css';
import { Grid, GridColumn as Column } from '@progress/kendo-react-grid';
As we can see we now have imported the Material Design theme as well as the appropriate Grid-specific items that we need.
The KendoReact Data Table (React Data Grid) can easily bind to just about any type of object and to make things simple we’ll just work with a simple array of objects. Once we know the fields available on our object we can define <Column />
components within our Grid declaration and use the field property to link a column to the appropriate field.
To make this simple to reference for everyone, here’s a StackBlitz project where we have added sample data to use through grid-sample-products.jsx
—imported as gridSampleProducts
—and then defined a React Grid with a subset of columns.
Paging as a concept allows users to take in data-intensive components like the React Data Table in smaller bite-sized chunks instead of a huge list of data items with a giant scrollbar. Paging also has several other benefits, including performance since less data needs to be displayed at once.
Today paging will help with showing how we can customize what to generate to PDF within the Grid. Specifically, do we want to export just the current page of data items or do we want to export all data that can be found on all pages of the Data Grid? While in our case all data will be provided at once to the Grid, this could even go as far as polling a server for all available data and generating a PDF file. So, this is an extremely powerful feature to provide out of the box.
To best understand paging, the KendoReact Data Grid Paging documentation article provides in-depth information and an example for how to implement paging.
Building off the StackBlitz example we linked above, we need to import an additional npm package that we initially installed and also import some additional pieces from React to ensure we can start working with hooks. So, in our App.js we can add:
import { useState, useEffect, useCallback } from 'react';
import { process } from '@progress/kendo-data-query';
For those interested, the @progress/kendo-data-query
package is a helper library that KendoReact offers to transform data in a format that the KendoReact Data Grid can easily use to help around aspects like paging, sorting, filtering, grouping and more. Having this as a separate package provides some separation of concerns and helps the React Data Table be super performant since a lot of the processing happens in the Data Query library.
Now, to understand paging, there are a few concepts to think about. First, paging across a large data collection can be done with just a few variables:
take
(“take this many data items from our data”)skip
because we can calculate how many data items we need to skip (if we know the current page and page size)Once we have these in mind, we also need to have a state defined for the Grid based on this information. Bringing these concepts into code and our App.js:
const [data, setData] = useState();
const [take, setTake] = useState(5);
const [skip, setSkip] = useState(0);
const dataState = {
take,
skip
};
With this configuration we now are saying that we are going to take five data items and we are skipping zero items, which means we start at 0 and count to 5.
Now here comes some initial magic from the Data Query library. If we want to take an array and transform it to something that our Data Grid can use for paging, we simply call the process() function:
const processedData = process(gridSampleProducts, dataState);`
It’s as simple as that!
Now, with the data in hand, we can update our Grid to grab data from our processedData variable and set the pageable property to let the Grid know that we want to display the pager at the bottom of the Grid.
<Grid
data={processedData}
rowHeight={40}
pageable
{...dataState}
onDataStateChange={onDataStateChange}
>
<Column field="ProductID" title="ID" />
<Column field="ProductName" title="Name" />
<Column field="Category.CategoryName" title="Category" />
<Column field="UnitPrice" title="Price" />
<Column field="UnitsInStock" title="In Stock" />
<Column field="Discontinued" title="Discontinued" />
</Grid>
We don’t need to fully write out pageable={true} as the default when this prop is defined is true. Additionally we can tie in our skip and take variables, which are used to display “X - Y of Z” data items at the bottom of the pager. The math is all taken care of automatically. We have these variables on our dataState
variable so we can use a JS spread operator to just apply these properties to our Grid configuration rather than manually type this out (a bit of a time saver)!
The last item to cover is the onDataStateChange
event, which will fire any time data is changed in the React Data Grid. For us this means paging, but this will also fire for sorting, grouping, filtering and so on. Here’s what we can use for this event:
const onDataStateChange = useCallback(
event => {
setTake(event.dataState.take);
setSkip(event.dataState.skip);
},
[setTake, setSkip]
);
In this case, we define a callback which will grab the event parameter that will contain the take and skip variables to indicate where in the data we are. We then update the current state of our component to ensure the Grid is aware of the new skip and take values. This will work both for paging forward and backward!
Here’s a StackBlitz project showing everything up until this point.
With all this done, we are now ready to add some PDF generation to the mix!
If you’ve read Part 1 and Part 2 of this blog post series, you know that we have to install and import the KendoReact PDF Processing library.
npm install --save @progress/kendo-react-pdf @progress/kendo-drawing @progress/kendo-licensing
While we are installing things again, let’s add the KendoReact Button and React Checkbox components as we’ll use them for some dynamic configuration.
@progress/kendo-react-buttons @progress/kendo-react-inputs @progress/kendo-react-intl @progress/kendo-drawing @progress/kendo-licensing
We are also going to update our import statements to not only include these new packages, but also add in useRef
from React:
import * as React from 'react';
import { useRef, useState, useEffect, useCallback } from 'react';
import '@progress/kendo-theme-material/dist/all.css';
import './style.css';
import {
Grid,
GridColumn as Column,
GridToolbar
} from '@progress/kendo-react-grid';
import { GridPDFExport } from '@progress/kendo-react-pdf';
import { Button } from '@progress/kendo-react-buttons';
import { Checkbox } from '@progress/kendo-react-inputs';
import { gridSampleProducts } from './grid-sample-products.jsx';
import { process } from '@progress/kendo-data-query';
Like in the other blog posts, the essence of exporting to PDF is to wrap around our Grid component with the tag. If we just wrap around our current Grid element with this tag, the Grid will disappear our page as this entire element is just responsible for exporting to PDF. So, we will need to essentially double-up to also show the Grid element. To make this easier, we can define a new element that can be reused.
const GridElement = (
<Grid
data={processedData}
rowHeight={40}
pageable
{...dataState}
onDataStateChange={onDataStateChange}
>
<GridToolbar>
<Button icon="pdf" onClick={onPdfExport} disabled={isPdfExporting} />
</GridToolbar>
<Column field="ProductID" title="ID" />
<Column field="ProductName" title="Name" />
<Column field="Category.CategoryName" title="Category" />
<Column field="UnitPrice" title="Price" />
<Column field="UnitsInStock" title="In Stock" />
<Column field="Discontinued" title="Discontinued" />
</Grid>
);
Then we can update our markup to be:
{GridElement}
<GridPDFExport ref={pdfExportRef}>{GridElement}</GridPDFExport>
We have a new reference here, pdfExportRef, which we can add to the beginning of our App.js file where we set up all of our hooks and such.
const pdfExportRef = useRef(null);
Another variable we should add is something that lets us know if we are actively exporting content as we need to disable the export button. That will ensure that users are not clicking our export button several times to generate PDF files. This becomes especially important when we export large data sets that may take some time to generate from. We’ll also prompt the user to select where to download the file.
const [isPdfExporting, setIsPdfExporting] = useState(false);`
If we look deeper at our new Grid markup, we see that we have a button element in our Grid Toolbar that needs an event handler.
const onPdfExport = useCallback(() => {
if (pdfExportRef.current) {
setIsPdfExporting(true);
pdfExportRef.current.save(processedData.data, onPdfExportDone);
}
}, [processedData, onPdfExportDone]);
What we are doing here is taking our Grid reference, updating that we are exporting content, and then calling the .save() function to start the exporting process. We also pass in onPdfExportDone
to give us a callback to hook into and do something after we have generated our file. In this case we just need to set isPdfExporting
to false.
const onPdfExportDone = useCallback(() => {
setIsPdfExporting(false);
}, []);
If we run this code in its current state, we can start generating React PDF files! However, what you’ll notice is that we only export the current page. In our sense we only get five data items each time, so that second page does not actually get exported. Let’s update things to cover this scenario so we can export all data at once.
To make this easier to see the difference between one export and the other, let’s set up a checkbox that determines if we are exporting all pages or just a single page.
<>
<div className="grid-export-area">
<h1>Grid Export</h1>
<Checkbox
onChange={allPageChange}
checked={allPageCheck}
label={'Export All Pages'}
/>
</div>
{GridElement}
<GridPDFExport ref={pdfExportRef}>{GridElement}</GridPDFExport>
</>
Now we just need to define allPageCheck
where we have the rest of our hooks defined.
const [allPageCheck, setAllPageCheck] = useState(false);
And the allPageChange
event is fairly simple, we’re just reversing whatever the variable is currently set to.
const allPageChange = event => {
setAllPageCheck(!allPageCheck);
};
The last step is to update onPdfExport to check if we are exporting all rows or just the current page.
const onPdfExport = useCallback(() => {
if (pdfExportRef.current) {
setIsPdfExporting(true);
if (allPageCheck) {
pdfExportRef.current.save(data, onPdfExportDone);
} else {
pdfExportRef.current.save(processedData.data, onPdfExportDone);
}
}
}, [processedData, onPdfExportDone]);
Notice that in this case we call .save()
and pass in all data rather than the processedData
variable. If we had additional data on the server this would be where we would make a request to our backend and grab all data if we don’t have all data available on the client.
An additional piece of code we need to add to ensure that we cover edge cases where processedData does not actually have data is the following:
useEffect(() => {
if (!processedData.data.length) {
setSkip(0);
}
setData(gridSampleProducts);
}, [processedData, data]);
This will “reset” the page we are on to the first page when there is no data available, which could be the case when we are updating the data of the grid dynamically during exporting. Essentially, when we export all pages, this becomes the piece of code that let’s us walk through our data and export every row.
That might be a lot to handle all at once, so to make things easier here’s a StackBlitz project that shows everything up and running.
With all that, we now have a Data Grid that can generate a PDF file from just the current data set, or the entire data set all at once.
The KendoReact TreeList is very similar to the React Data Grid, so I wanted to include a similar sample for the TreeList as a reference. Since it took us some time to get out Grid project up and running, and the TreeList configuration is essentially the same as the Grid’s, instead of going over every step again, I’ll pass the baton to you: Here’s a StackBlitz project showing how this can be done for the TreeList.
This blog post did cover quite a lot, so let’s summarize what we did.
Between Generating PDF in React: As Easy As 1-2-3 , Generating PDF from HTML in React Demo: Exporting Invoices, and this blog post, you now have a good handle on just how powerful the KendoReact PDF Generator library can be. Beyond simple HTML, we can generate PDF files from our React apps with more complex scenarios like invoices and even advanced data components like the KendoReact Data Grid and TreeList components!
If you like what you’ve seen so far, the next step would be to get to know KendoReact—our professional React UI components and data visualization library. Built from the ground up for React, KendoReact plays well with any existing UI stack. Its 90+ customizable and feature-rich components—including the PDF exporting capability you just read about—make it the perfect foundation for your internal UI library.
Carl Bergenhem was the Product Manager for Kendo UI.