Complex web projects often require the use of third-party widgets. But what if you’re using a framework and the widget is only available in pure JavaScript?
To use a JavaScript widget in your project, the best approach would be to create a framework-specific wrapper.
ag-Grid
is a JavaScript widget for displaying information in a datagrid. It allows you to dynamically sort, filter, and select information. ag-Grid
also provides a React wrapper, ag-grid-react
.
In this article, you will use ag-grid-community
and ag-grid-react
as the basis for learning about wrapping a third-party widget into a React component. You will set up the mapping between React Props and the widget’s configuration options. And you will also expose a widget’s API through a React component.
To follow along with this article, you will need:
In general, most JavaScript widgets have:
That’s exactly how you interact with ag-Grid
. You can find a good description of the grid’s properties, events, callbacks, and API in the official documentation.
In short, the datagrid defines:
Here’s a very basic pure JavaScript configuration that demonstrates the usage of grid options:
let gridOptions = {
// PROPERTIES - object properties, myRowData and myColDefs are created somewhere in your application
rowData: myRowData,
columnDefs: myColDefs,
// PROPERTIES - simple boolean / string / number properties
pagination: true,
rowSelection: 'single',
// EVENTS - add event callback handlers
onRowClicked: function(event) { console.log('a row was clicked'); },
onColumnResized: function(event) { console.log('a column was resized'); },
onGridReady: function(event) { console.log('the grid is now ready'); },
// CALLBACKS
isScrollLag: function() { return false; }
}
First, the JavaScript datagrid is initialized like this:
new Grid(this._nativeElement, this.gridOptions, ...);
Then, ag-Grid
attaches the object with API methods to the gridOptions
that can be used to control the JavaScript datagrid:
// get the grid to refresh
gridOptions.api.refreshView();
However, when ag-Grid
is used as a React component, we don’t instantiate the datagrid directly. That’s the job of the wrapper component. All interactions with the instance of ag-Grid
occur through the component instance.
For example, we don’t have direct access to the API object attached by the grid. We will access it through the component’s instance.
We never pass configuration options and callbacks directly to the grid. A React wrapper component takes the options and callbacks through React Props.
All grid options that are available for vanilla JavaScript grid should be available in React datagrid as well. We also don’t directly listen for events on the instance of ag-Grid
. If we’re using ag-Grid
as a React component, all events emitted by ag-Grid
should be available through React components props.
This all means that a React specific datagrid wrapper around ag-Grid
should:
rowData
) and ag-Grid
’s configuration optionsag-Grid
and define them as component outputsag-Grid
to the gridOptions
through its propertiesThe following example demonstrates how React datagrid is configured in a template using React Props:
<AgGridReact
// useful for accessing the component directly via ref - optional
ref="agGrid"
// simple attributes, not bound to any state or prop
rowSelection="multiple"
// these are bound props, so can use anything in React state or props
columnDefs={this.props.columnDefs}
showToolPanel={this.state.showToolPanel}
// this is a callback
isScrollLag={this.myIsScrollLagFunction}
// these are registering event callbacks
onCellClicked={this.onCellClicked}
onColumnResized={this.onColumnEvent}
// inside onGridReady, you receive the grid APIs if you want them
onGridReady={this.onGridReady}
/>
Now that we understand the requirement, let’s see how we implemented it at ag-Grid
.
First, we need to define a React component AgGridReact
that represents our React datagrid in templates. This component will render a DIV
element that will serve as a container for the datagrid. To get a hold of the native DIV
element we use the Refs functionality:
export class AgGridReact extends React.Component {
protected eGridDiv: HTMLElement;
render() {
return React.createElement("div", {
style: ...,
ref: e => {
this.eGridDiv = e;
}
}, ...);
}
}
Before we can instantiate ag-Grid
, we also need to collect all options. All ag-Grid
properties and events come as React Props on the AgGridReact
component. The gridOptions
property is used to store all datagrid options. We need to copy all configuration options from React props as soon as they become available.
To do that, we’ve implemented the copyAttributesToGridOptions
function. It’s a utility function that copies properties from one object to the other:
export class ComponentUtil {
...
public static copyAttributesToGridOptions(gridOptions, component, ...) {
...
// copy all grid properties to gridOptions object
ComponentUtil.ARRAY_PROPERTIES
.concat(ComponentUtil.STRING_PROPERTIES)
.concat(ComponentUtil.OBJECT_PROPERTIES)
.concat(ComponentUtil.FUNCTION_PROPERTIES)
.forEach(key => {
if (typeof component[key] !== 'undefined') {
gridOptions[key] = component[key];
}
});
...
return gridOptions;
}
}
The options are copied in the componentDidMount
lifecycle method after all props have been updated. This is also the hook where we instantiate the grid. We need to pass a native DOM element to the datagrid when it’s being instantiated, so we’ll use the DIV
element captured using refs functionality:
export class AgGridReact extends React.Component {
gridOptions: AgGrid.GridOptions;
componentDidMount() {
...
let gridOptions = this.props.gridOptions || {};
if (AgGridColumn.hasChildColumns(this.props)) {
gridOptions.columnDefs = AgGridColumn.mapChildColumnDefs(this.props);
}
this.gridOptions = AgGrid.ComponentUtil.copyAttributesToGridOptions(gridOptions, this.props);
new AgGrid.Grid(this.eGridDiv, this.gridOptions, gridParams);
this.api = this.gridOptions.api;
this.columnApi = this.gridOptions.columnApi;
}
}
You can see above that we also check if there are children that are passed as columns and add then to configuration options as column definitions:
if (AgGridColumn.hasChildColumns(this.props)) {
gridOptions.columnDefs = AgGridColumn.mapChildColumnDefs(this.props);
}
Once the grid is initialized, we need to track changes to React Props to update configuration options of the datagrid. ag-Grid
implements an API to do that. For example, if the headerHeight
property changes there’s the setHeaderHeight
method to update the height of a header.
React uses componentWillReceiveProps
lifecycle method to notify a component about changes. This is where we put our update logic:
export class AgGridReact extends React.Component {
componentWillReceiveProps(nextProps: any) {
const changes = <any>{};
const changedKeys = Object.keys(nextProps);
changedKeys.forEach((propKey) => {
...
if (!this.areEquivalent(this.props[propKey], nextProps[propKey])) {
changes[propKey] = {
previousValue: this.props[propKey],
currentValue: nextProps[propKey]
};
}
});
AgGrid.ComponentUtil.getEventCallbacks().forEach((funcName: string) => {
if (this.props[funcName] !== nextProps[funcName]) {
changes[funcName] = {
previousValue: this.props[funcName],
currentValue: nextProps[funcName]
};
}
});
AgGrid.ComponentUtil.processOnChange(changes, this.gridOptions, this.api, this.columnApi);
}
}
Basically, we go over the list of ag-Grid
’s configuration properties and callbacks and check if any of them have changed. We put all changes in the changes
array and then process them using processOnChange
method.
The method does two things. First, it goes over the changes in React Props and updates properties on the gridOptions
object. Next, it calls API methods to notify the grid about the changes:
export class ComponentUtil {
public static processOnChange(changes, gridOptions, api, ...) {
...
// reflect the changes in the gridOptions object
ComponentUtil.ARRAY_PROPERTIES
.concat(ComponentUtil.OBJECT_PROPERTIES)
.concat(ComponentUtil.STRING_PROPERTIES)
.forEach(key => {
if (changes[key]) {
gridOptions[key] = changes[key].currentValue;
}
});
...
// notify Grid about the changes in header height
if (changes.headerHeight) {
api.setHeaderHeight(changes.headerHeight.currentValue);
}
// notify Grid about the changes in page size
if (changes.paginationPageSize) {
api.paginationSetPageSize(changes.paginationPageSize.currentValue);
}
...
}
}
Interacting with the React grid at run time is done through the grid API. You may want to adjust the size of the columns, set new data sources, get a list of all selected rows, etc. When the JavaScript datagrid is initiated, it attaches the api
object to the grid options object. To expose this object, we assign it to the component instance:
export class AgGridReact extends React.Component {
componentDidMount() {
...
new AgGrid.Grid(this.eGridDiv, this.gridOptions, gridParams);
this.api = this.gridOptions.api;
this.columnApi = this.gridOptions.columnApi;
}
}
And that’s it.
In this tutorial, we learned how to adapt a vanilla JavaScript library to function within the React framework.
For more information on this topic, you could reference the official React documentation on “Integrating with Other Libraries”.
Thanks for learning with the DigitalOcean Community. Check out our offerings for compute, storage, networking, and managed databases.
This textbox defaults to using Markdown to format your answer.
You can type !ref in this text area to quickly search our full set of tutorials, documentation & marketplace offerings and insert the link!