DevExtreme React Grid - Plugin Writing Basics

Oliver's Blog
12 July 2017

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’s render).
  • 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 Getters 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.

Click here to return to the blog series overview

Free DevExpress Products - Get Your Copy Today

The following free DevExpress product offers remain available. Should you have any questions about the free offers below, please submit a ticket via the DevExpress Support Center at your convenience. We'll be happy to follow-up.
No Comments

Please login or register to post comments.