There is a lot you can do with the KendoReact Grid. Read on to get an introduction into commonly used features and get ideas about how it can be added to your projects today.
The KendoReact Data Grid (or Data Table, depends on what you're used to) is one of the most popular components from of our React UI component library. It makes a lot of sense why this is the case, many developers are tasked with displaying data within their applications and what better way to present said data than in a tabular fashion?
There's more than just displaying data in rows and columns though. This native UI component, which is a part of our native UI component library built from the ground-up for React (which means zero dependencies), has a ton of features built in to help users organize, modify, and export their data. Some highlights include:
- Paging
- Sorting
- Filtering
- CRUD Operations
- Export to PDF and Excel
- Reordering and Resizing of Columns
- Virtualization
And that's not even the full list! For this blog post I wanted to take a look at some of the more popular features and how you can implement them, sticking to paging, sorting, filtering, and grouping. Overall this should give you a great foundation for how you can add the KendoReact Grid in to your applications!
Installing the KendoReact Grid
Before I get any further I should cover just how to prepare your project to start using the KendoReact Grid.
First, we should npm install
all of the packages that we may need:
npm install --save @progress/kendo-react-grid @progress/kendo-data-query @progress/kendo-react-inputs @progress/kendo-react-intl @progress/kendo-react-dropdowns @progress/kendo-react-dateinputs
We're installing a few packages here, but mainly we're adding in the Grid, all of the KendoReact inputs (like drop downs and date pickers) as well as the ability to work with our internationalization and globalization packages.
Next, within our component we can import our package module:
// ES2015 module syntax
import { Grid } from '@progress/kendo-react-grid';
Or use the CommonJS format:
// CommonJS format
const { Grid } = require('@progress/kendo-react-grid');
Finally, we should make sure that the component is styled in some way. We have three designs (or themes) that you can use: the Default (our home-grown theme), Bootstrap (v4), and Material themes. For this particular sample we will be using Material since this is based on the guidelines coming from Material Design and one of the most popular design languages today.
To add in one of our themes all you have to do is another npm install
like this one:
npm install --save @progress/kendo-theme-material
Then, to actually use this theme in our application (as in, where we need to reference our CSS) we have a couple of options. For more detail as to exactly what you can do please check out our "Styling & Themes" documentation article, but in this case I did a super simple inclusion of our theme in the header of my index.html
:
<link rel="stylesheet" href="https://unpkg.com/@progress/kendo-theme-default@latest/dist/all.css" />
Not necessarily something recommended for production - we cover more real-life scenarios in the article I linked above - but certainly something that will work for this article!
It All Starts with Data
Now that things are installed and imported in our project, let's kick things off with the easiest scenario: binding to an array.
Let's say we have the following array in my state that we want to show in our KendoReact Grid:
state = {
gridData: [
{ "firstName" : "Clark", "lastName" : "Kent", "heroName" : "Superman" },
{ "firstName" : "Bruce", "lastName" : "Wayne", "heroName" : "Batman" },
{ "firstName" : "Kendra", "lastName" : "Saunders", "heroName" : "Hawkgirl" },
{ "firstName" : "Diana", "lastName" : "Prince", "heroName" : "Wonder Woman" }
]
};
All we really need to do is the following:
<Grid data={this.state.gridData} />
That's it! We end up with a Grid like this:
As we can see the Grid took all of our fields, automatically created columns for them and displayed them all on a single page. However, there are some things that stick out like the header not necessarily looking that great (just taking the field name), and maybe we want to display the name of our super hero first rather than last. The way that we solve this is by defining a set of columns within the Grid to represent our data. These columns let us also take over specifics that we may want to do on a column-by-column basis (think about providing custom templates depending on our data) down the road.
That's looking much better! Notice how the order of the columns have now changed, and the headers are looking a lot better.
Adding Interactivity into the Mix
Paging
For paging there are a few paths we can take. Since we're going to be working with local data it means that we are responsible for slicing the data down to the proper size that we need for the pages we're dealing with.
What we're doing for now is fully taking over paging based on the superhero data that we mentioned above. We're taking this approach just to help step through how paging works within the KendoReact Grid on a basic level. There are many other ways, including making the Grid itself more stateful, or working with libraries like our Data Query
framework and even Redux to change things. For more samples you can refer to our paging documentation section.
A couple of things that I want to point out are terms that we use in the configuration of the Grid and paging: skip, take
, and total
. The names kind of give it away, but lets jump in to each one.
skip
aligns with how far in our data array we should go. This would be 0 for our initial page, but if we have a page size of 10 and want to jump to the second page we would now have a skip
of 10 to start on the next "page" of data.
take
is pretty much our page size. If we set this to 10 it means that every page will have 10 items loaded in it.
total
just lets the pager know the total number of items that we are binding to. This helps around the calculation to display "X - Y of Z items" where our total is "Z."
With that in mind the way that we enable paging within the KendoReact Grid is through setting the pageable
property to true, and then defining the take
and skip
options.
In our case we only have four data items so to make something of a sample we can make a page size of two, giving us two total pages. Nothing super exciting, but again this is to give you an idea of how paging works in the Grid. Since take
and skip
are dealing with our data and keeps the current state of the Grid let's add them to our component's state right away like so:
class App extends React.Component {
constructor(props) {
super(props);
this.state = {
gridData: [
{ "firstName" : "Clark", "lastName": "Kent", "heroName" : "Superman" },
{ "firstName": "Bruce", "lastName": "Wayne", "heroName" : "Batman"},
{ "firstName": "Kendra", "lastName": "Saunders", "heroName" : "Hawkgirl"},
{ "firstName": "Diana", "lastName": "Prince", "heroName" : "Wonder Woman"}
],
skip: 0,
take: 2
}
So, we skip
0, starting with my first item, and we're only setting up take
to be 2 (grabbing just Superman and Batman from our array).
Another piece that we have to do to implement paging is to subscribe to the onPageChange
event. This event is in charge of letting us know when the page changes and in which direction (if we're paging forward or backwards). This is really just accomplished through updates to skip
and take
, but it also gives us a chance to modify our data tied to the Grid to the appropriate page. In our case this can be handled through a simple array.slice()
, but in more advanced cases we'd do a little more with our data.
Our case is super simple, all we have to do is update our state's skip
and take
variables to what the event gives us, which means we end up with this:
this.pageChange = (event) => {
this.setState({
skip: event.page.skip,
take: event.page.take
})
}
Now we just have to configure our Grid to use all of what we've set up in our component.
<Grid
data={this.state.gridData.slice(this.state.skip, this.state.take + this.state.skip)}
pageable={true}
skip={this.state.skip}
take={this.state.take}
total={this.state.gridData.length}
onPageChange={this.pageChange} >
<Column field="heroName" title="Super Hero" />
<Column field="firstName" title="First Name" />
<Column field="lastName" title="Last Name" />
</Grid>
Note what we did in the data
property, as well as what we did with total
. The latter is easy, we simply say that the total number of items we have is the length of our data array. What we did with data
is just a workaround for us not having a "real" data connection here, rather just a local array. So, we use array.slice()
and divide up the array based on the skip
and take
variables we have in our state.
Here's the resulting Grid, with paging and all!
Sorting
With paging added in, let's see what it takes to work with sorting.
Sorting is pretty easy to set up. First we want to add a "sort" variable to our state, just to keep track of the sorting in the Grid. While we will just sort over a single column, this should be an array since we may want to sort over multiple columns. So, just adding in sort: []
to our state.
On the actual Grid we want to set the sortable
property on the Grid, which needs a GridSortSettings
object that can define if we want to sort on single or multiple columns, and if we want to give the user the ability to un-sort (remove sorting). We will keep things simple here, but if you want to dive into this more here's an advanced sample. We'll just set this to true for now ending up with sortable={true}
added to our declaration Grid.
Just like with paging, we have to make sure a field
option is set within one of our columns so we have bound the Grid to some field in the data.
We should also define the sort
property, which will give us the highlighted look of what column is currently sorted as well as the arrows that will appear next to the header text (for ascending or descending sorting). This is the field that we already defined on our state earlier, state.sort
.
Finally, we need to subscribe to the onSortChange
event. This is just like the paging event where it gives us the opportunity to take what the user is trying to update in terms of sorting and apply it to our data. Since we're doing this locally we can just update the sort order, but if we had another data store method we would just apply the sort manually across our data.
this.shortChange = (event) => {
this.setState({
sort: event.sort
})
}
To give an idea of what this sort
variable might look like, a typical sort object can look like this:
sort: [
{ field: "heroName", dir: "asc" }
]
The above is saying that we are sorting on the heroName
field, and we're doing so in an ascending fashion. We could technically set this from the start and define a sort order from the beginning, but for now we'll just leave things blank.
To get the fundamentals of sorting, this is all we need to do. However, as organizing the sorting of data can be cumbersome, we can lean on the KendoReact Data Query framework to help here. This has a ton of helpful tools for us, one being orderBy
which we'll use here to order our array by our sort order.
Note that this is being used to help deal with the local array as a data store. For more advanced scenarios (using state management etc.) we would not really need the Data Query framework.
We already added this to our project when we first did our npm install
but let's import this in our component.
import { orderBy } from '@progress/kendo-data-query';
We can now use orderBy()
to do something like this:
const data = [
{ name: "Pork", category: "Food", subcategory: "Meat" },
{ name: "Pepper", category: "Food", subcategory: "Vegetables" },
{ name: "Beef", category: "Food", subcategory: "Meat" }
];
const result = orderBy(data, [{ field: "name", dir: "asc" }]);
console.log(result);
/* output
[
{ "name": "Beef", "category": "Food", "subcategory": "Meat" },
{ "name": "Pepper", "category": "Food", "subcategory": "Vegetables" },
{ "name": "Pork", "category": "Food", "subcategory": "Meat" }
]
*/
As we can see all that's needed is to pass in an array and then the sort object to this function. In our case this means that we need to call orderBy
on our original set of data, and then pass in the sort object as well.
data={orderBy(this.state.gridData, this.state.sort)}
However, if we want to also have paging still we need to use array.slice
again. This should be done on the result of orderBy
, so we can just chain it to the end of our orderBy
call.
data={orderBy(this.state.gridData, this.state.sort).slice(this.state.skip, this.state.take + this.state.skip)}
With all of that configured our Grid setup should look like this:
If we run this code we'll see that we can sort our columns simply by clicking on the header and we get that nice indicator to showcase what direction we're sorting in. We're off to the races here!
Filtering
Next up is filtering. This is pretty much the same setup as we have with sorting above. We need to set up a property that defines that we can offer filtering, we provide binding to our state to let us indicate what is currently being filtered (like the sort order indicator above), and finally work with an event when filter changes. Our filters are also set up with an object that defines the filter and properties around filtering, like what kind of filter we're applying ("contains" or "starts with" etc.).
We don't necessarily need to add a filter variable to our state, so we'll skip this for now. If we wanted to filter something ahead of time we could do so pretty easily by defining this object to whatever filter we want to apply.
On the actual Grid we first set up filterable={true}
that will immediately make our filter icons and the filter row appear on top of every column. Then, we set the filter
property to be equal to the state variable we defined before, so filter={this.state.filter}
.
We then subscribe to our event, onFilterChange
, and use this to update our state when a user filters.
this.filterChange = (event) => {
this.setState({
filter: event.filter
})
}
As a quick reference, the filter
variable is expecting a CompositeFilterDescriptor
, which really is just an array of FilterDescriptors
along with a logic variable that defines if we are using "and" or an "or" filter. The actual FilterDescriptor
is a bit long to go through, so I recommend looking over the documentation article I just linked to in order to see how this is built out manually.
The last part that we have to do is actually modify our data. We're at a point where filtering, sorting, and paging needs to be applied to the Grid and this can quickly become hard to track. How do we apply the sort order, filter order, and eventually even grouping on our array? Well, there's the manual way and there's the easy way: the KendoReact Data Query process
function.
Quick aside: Data Query and the Process() Function
This deserves its own mini-section here as this will save us time and effort when it comes to massaging our data for this sample. As mentioned earlier this is not a requirement to use with the Grid. In fact, many of you will have your own state management already set up. This is something that is super helpful for dealing with data in local arrays or without existing state management. Perfect for this particular sample. Also, depending on where you are in your React journey this might be something that you rely on for your first project or two while working with the KendoReact Grid or some of the other data bound components.
The process()
function simply takes in our initial data as its first parameter, and as the second it takes an object that contains the skip
, take
, sort
, filter
, and group
(more on grouping soon) variables that come from the Grid (or are pre-defined) and applies all of these options on our data set, outputting everything in to a DataResult
object, which the KendoReact Grid uses to have an idea of current page, filter, sort, and group options.
Since we're not using Redux or something similar here we'll be relying on this method throughout the rest of the sample. This ultimately saves a ton of time binding to our data bound components like the Grid with a local array, just like we're doing here.
Back to Filtering
Now that we know about the process
function we can import this instead of our orderBy
(from the same package though).
import { process } from '@progress/kendo-data-query';
Then, in our data
prop we just do the following:
data = { process(this.state.gridData, this.state) }
How easy is that? Because we already are defining all of the variables we need in our component's state we can just pass in this.state
without creating a new object! The outcome is the following, which now has us filtering, sorting, and paging across all of our data!
Cleaning Things up a Bit
Before we proceed any further, you may have noticed that it is quite busy in our component right now. We've got all of these settings that have been configured on the Grid, all of the fields on our state, and all of these events that are firing. Just like we used process()
to simplify our data binding, could we do the same with setting up our Grid?
Maybe I'm making it too easy to set this up, but the short answer is that yes, it's certainly possible to make things easier! Let's chat about onDataStateChange
.
The onDataStateChange
event fires every time the state of the Grid changes. This means that all of our calls to onPageChange
, onSortChange
, onFilterChange
(and soon onGroupChange
when we get to grouping) can get replaced with one single onDataStateChange
subscription instead.
We'll want to use this event, but we may want to take a look around the rest of our code first. Right now we do a lot with setting everything right on the root of our state object. It would be a bit more structured if we define a variable to specifically hold all information relating to the Grid, so let's call this gridStateData
and put our skip
and take
variables there.
this.state = {
gridStateData: {
skip: 0,
take: 2
}
}
With that, we can move to implementing onDataStateChange
with the following:
this.dataStateChange = (event) => {
this.setState({
gridStateData: event.data
});
}
Next, let's make the state of the component a little bit simpler and move our data outside of the state and instead pass it in to our React component, something you'll probably be looking to do even in simpler applications. This will be outside of our component's scope and right above the ReactDOM.render
function. Don't forget to add a prop and pass in the data!
const appData = [
{ "firstName" : "Clark", "lastName": "Kent", "heroName" : "Superman" },
{ "firstName": "Bruce", "lastName": "Wayne", "heroName" : "Batman"},
{ "firstName": "Kendra", "lastName": "Saunders", "heroName" : "Hawkgirl"},
{ "firstName": "Diana", "lastName": "Prince", "heroName" : "Wonder Woman"}
];
ReactDOM.render(
<App gridData={appData} />,
document.querySelector('my-app')
);
This means that we will have to update the data
prop on our Grid to be the following:
data={process(this.props.gridData, this.state.gridStateData)}
Notice how we're calling this.props.gridData
here since we're now passing this into the component through a prop.
Another area that we can look into, since we're using process()
and onDataStateChange
to update and set the state upon every sort, filter, page, and group action, is to eliminate a lot of redundant properties as well.
While we technically have to use things like sort
, skip
, take
, etc. within the Grid - why write them on the Grid when they are readily available within our state.gridStateData
? We can use JSX Spread Attributes to help us here. We just need to add {...this.state.gridStateData}
to our Grid's declaration. We end up with this in the end.
<Grid
data={process(this.props.gridData, this.state.gridStateData)}
{...this.state.gridStateData}
filterable={true}
sortable={true}
pageable={true}
onDataStateChange={this.dataStateChange} >
<Column field="heroName" title="Super Hero" />
<Column field="firstName" title="First Name" />
<Column field="lastName" title="Last Name" />
</Grid>
Look at how clean that is in comparison! Just as a reference, here's what we have so far in our component.
Grouping
The last piece we should cover is grouping. There are a couple more things to keep in mind when setting up a group, but starting with how the initial configuration might look ends up being similar to what we've done so far. Much like sorting and filtering, we need to set of our groupable
, group
, and onGroupChange
configuration options. Configuring these will give us the ability to drag-and-drop a header to start grouping, or group on a field initially.
There's another part of grouping that we may not initially think of, and this is the Group Header of any group. This is what lets us provide information about our group which is initially just the value of the field we're grouping on, but what about adding in additional information like aggregates here? Also, this contains the expand and collapse icons, which should be tracked somewhere in our state and consumed by the Grid.
This is why there are two other configuration options we will need to set up: onExpandChange
, which fires every time we collapse or expand a group, as well as expandField
, which lets us define if an item is expanded or collapsed based on the value of this field.
With that information fresh in our heads, let's go ahead and set up some grouping! First, let's add groupable={true}
on our Grid. We don't need to define onGroupChange
though since we use onDataStateChange
. Also, group
will be defined once we group thanks to the spread we are doing with {..this.state.gridStateData}
.
This just leaves the two additional configuration options to set up. Let's set expandField="expanded"
. the expandField
prop is what will check if a data item is expanded or not (will only be added to our group header items) and it doesn't matter that we have not defined this elsewhere, even in our original. The Grid will simply add this if it's not available when we expand or collapse. We can get away with this since we are working with a local array, but other scenarios may call for keeping track of expanded items separately from our original data.
After this we will need to set up onExpandChange
to make sure we capture when an item is expanded and update the state accordingly. So, we add onExpandChange={this.expandChange}
to our Grid and then set up our this.expandChange
function like this:
this.expandChange = (event) => {
event.dataItem[event.target.props.expandField] = event.value;
event.target.setState({});
}
A note to be made here is that event.target.setState({});
is purely for the sake of this demo since we're dealing with local data. The KendoReact Grid (which is the event.target
) does not have an internal state, which is why we are setting all of these variables in state.gridStateData
. Calling setState({})
with an empty object will refresh the Grid based on the updated information around if an item is expanded or not. In a more real-world scenario this wouldn't be our setup since we probably wouldn't be dealing with local arrays like this.
Looking at the first line in this function we're accessing our expandField
variable in a safe way on our data item since this might come up as undefined
otherwise. Also, it gives us flexibility in case we update the expandField
that we want to track expand and collapsed state in later. We set this to event.value
which will be true or false depending on if we are expanding or collapsing the group.
That should be all we need to add in grouping! What is left is to actually try it out by running our sample and dragging a column header to the area that appears once we set groupable
on our Grid.
Here's the source code to the finished product, which is a Grid that can handle paging, sorting, filtering, and grouping! For fun you can always swap out the way you load data (maybe through a JSON file somewhere) and see that this will still work since we've created a fairly generic setup with this.
But Wait, There's More!
This blog post covered a lot so far, but we just went with a basic set of features. We covered this all through binding to a local array rather than something like Redux (which we can certainly bind to by the way). So, we really just scratched the surface and there's even more the KendoReact Grid can do!
Just as a small teaser sample, there's editing, hierarchy, PDF export, Excel export, cell templates, column resizing, column reordering, and way more! Beyond this there are also customizations that can be done with how we display filters, more around grouping - the list goes on and on.
Overall its impossible to cover everything the KendoReact Grid can do in one single blog post since there's so much that can be done with the component. This is really why the the KendoReact Grid documentation exists, and there's plenty of more samples to check out just what the Grid can do! What we tried to accomplish here is to give kind of a 101-level introduction to some of the the most commonly used features and give ideas about how the KendoReact Grid can be added to your projects today!
If you're new to KendoReact, you can learn more about it here or feel free to jump right into a free 30 day trial.
Top comments (0)