This post is part of a series about the DevExtreme React Grid. You can find the introduction and overview to the post series by following this link.
As I explained in the first few posts of this series, the React Grid relies heavily on plugins for its functionality. The post on standard plugins outlines the use of those plugins that come in the box with the grid. On top of that, it is possible to create your own plugins to extend grid functionality.
Please note that the APIs described in this post are subject to an internal review right now. The React Grid is still at an “alpha” stage right now and details about those APIs (and others) might change in the future, before a final release version is reached.
In this post I’ll introduce the basics of plugin development for the React Grid. I’m using three rather contrived examples, in order to keep my descriptions short and to the point. I’m also preparing the stage for the last post I’m currently planning for the series, where I will extract my remote data loading functionality into a reusable plugin.
Plugin structure
A plugin for the React Grid is a React component in its own right. It has a render
method (and it can be implemented as a functional component), but the rendering doesn’t generate any visible elements. The outer element of a plugin is a PluginContainer
, and inside that container any number of elements of four different types can be included:
- The
Getter
element allows the plugin to provide a value to the Grid. - The
Template
element defines a visual element that will be rendered somewhere (though not as a result of the plugin’srender
). - The
Watcher
reacts to changes in the Grid state. - The
Action
element allows the plugin to provide executable functionality to other plugins.
A first plugin with a Getter
At the start of the code, I’m importing all the elements I’ll be using from DevExpress.DXReactCore
(that corresponds to @devexpress/react-core
):
const { Getter, Watcher, Template, TemplatePlaceholder, PluginContainer } = DevExpress.DXReactCore;
In the first sample I won’t be using the Watcher
, Template
and TemplatePlaceholder
yet. My plugin looks like this:
class TestPlugin extends React.PureComponent { render() { return (<PluginContainer><Getter name="rows" connectArgs={getter => [getter("rows")]} pureComputed={rows => rows.concat([{ name: "Test Album", artist: "Computer", year: 0 }])} /></PluginContainer> ); } }
You can see the render
method, the PluginContainer
and the Getter
I already described. Three properties are set for the Getter
.
The name
is listed first, but logically it is used last. After the value returned by the Getter
has been calculated, the name given here is used to store the result in Grid state (yes, that’s Grid state, not plugin state).
connectArgs
receives a delegate, which in turn receives a getter
function. This delegate is executed first, and you are expected to access values from Grid state with the help of getter
and return a list of those that you’re interested in. My example accesses the rows
list (that’s the same one we’ve been setting in Grid props in previous examples). rows
is also the name
of the Getter
, so you can see at this point that the plugin has a chance to analyze the rows
and/or “modify” (technically, replace) them.
The final property I’m using is pureComputed
, another delegate. This delegate is called after connectArgs
, with the parameters that connectArgs
returned. In my case, I will receive the rows
from Grid state, and I go on to concat
another row to that list.
Note that pureComputed
is expected to be functionally pure. You should not trigger side effects here (e.g. remote data loading), and you should regard the information that is passed to the function as immutable. The concat
function I’m using returns a new list, which keeps the function pure.
To summarize, my plugin works through three steps:
1 - Using the getter
in connectArgs
, it retrieves the rows
state field from the Grid.
2 - pureComputed
is called with that rows
value, and it returns a new array consisting of the old rows
plus one new row.
3 - The value returned by pureComputed
is stored in the Grid state field rows
again, because the name
of the Getter
is rows
.
It is possible to have several Getter
s in a plugin. They are evaluated strictly along the same lines as above, one by one, from top to bottom. Each Getter
is able to “see” any information generated by the previous ones.
To use the plugin in the Grid, I add it just like any of the standard plugins:
... return (<Grid rows={rows} columns={columns}><SortingState /><LocalSorting /><TestPlugin /><TableView /><TableHeaderRow allowSorting /></Grid> );
I have included sorting functionality in this sample to point out where the TestPlugin
should appear between the others. Since the plugin modifies the rows
, it needs to appear before TableView
, which renders the rows.
The ordering of LocalSorting
and TestPlugin
is interesting. Using the order shown above, the TestPlugin will add its extra row to the end of the list after sorting has already occurred. If you swap the two plugins, you will see that the extra row becomes subject to sorting together with the others.
Here’s the sample for you to play with:
Using templates
The Template
plugin element, not surprisingly, defines a template. You can use a new name or one that already exists. To “override” a template that exists already, keep in mind that all plugins are evaluated in order. If you want to use the previous content of a template in your definition, you use the TemplatePlaceholder
component, which is also available to render content from other templates within your own.
Here is a plugin that uses the root
template to display an overlay on top of the grid. Since the plugin functionality is simple, I have chosen to use a functional component implementation:
const Overlay = () =><PluginContainer><Template name="root"><div><TemplatePlaceholder /><div className="overlay"><div>This is my overlay</div></div></div></Template></PluginContainer>;
Note that some CSS is used to style the overlay in my sample.
The following pair of plugins uses the standard template footer
(which is not used by the grid at this time) to show some text. The PluggableFooter
introduces its own placeholder footerText
and the plugin FooterText
defines the template footerText
, reusing any existing content from previous definitions.
const PluggableFooter = () =><PluginContainer><Template name="footer"><div><TemplatePlaceholder name="footerText" /></div></Template></PluginContainer>; const FooterText = props =><PluginContainer><Template name="footerText"><span><TemplatePlaceholder />{props.text}</span></Template></PluginContainer>;
In my sample, I’m applying these three plugins like this:
...<TableView /><TableHeaderRow allowSorting /><Overlay /><PluggableFooter /><FooterText text="Some footer text" /><FooterText text=" - More footer text" /> ...
The templating system is simple and powerful, and there are several more features that I’m not going to describe in detail here. It is possible to define templates with a predicate
property, which applies the template conditionally, and to utilize getters for access to state as well as actions to execute functionality exported by other plugins.
Note that the standard templates provided by the Grid are currently high level ones, like root
and footer
. We are considering customization solutions for individual nested elements, but we are hesitant to create very complex structures of “named” templates that might also introduce performance issues. Further investigation is ongoing at this time and I’ll post updates in the future.
Here is the complete sample for template plugins:
The Watcher
If you would like your plugin to react to changes to the Grid state, you can include a Watcher
. Using a delegate, you extract values from Grid state (similar to the Getter
), and another delegate is invoked if those values have changed compared to a previous run. Here’s the Watcher
from my sample:
<Watcher watch={getter => [getter('sorting')]} onChange={(action, sorting) => { const artistSorting = sorting.find(s => s.columnName === 'artist'); this.setState({artistSorting: !!artistSorting}); }} />
The delegate assigned to the watch
property retrieves the sorting
configuration from Grid state. This is passed to the onChange
delegate together with an action
parameter (which is not used in this sample). In the delegate, I find out whether sorting currently includes the artist
column and set the plugin state accordingly.
Note that this pattern of recording information in state is a common one. Remember that the plugin is part of the render
function, which should be pure. This means you should not trigger side effects within the Watcher
.
For the sample, I’m using the new state value in a Template
, which conditionally shows an overlay similar to that in the sample above:
<Template name="root"><div><TemplatePlaceholder /> { this.state.artistSorting &&<div className="overlay"><div>Artist Sorting Active</div></div> }</div></Template>
To test the functionality, sort the grid data by different columns (just click the column headers). You will see that overlay is shown when you sort by the artist
column, and it goes away as soon as you sort by something else.
Here is the sample:
Actions
The fourth and final plugin element is called Action
. I’m not going to describe this element here in detail because it is a rather advanced scenario to create your own actions. The Grid and its various standard plugins export a number of actions that can be used in plugins (Watcher
and Template
elements can gain access to actions).
Coming up
The next post in the series will take advantage of the plugin elements introduced above to integrate the remote-data-loading functionality from this post in a plugin. You will also see an example of using actions exported by standard plugins.