A while back, one of our customers asked for a demonstration on how to create a dynamic dashboard, like those created with the Google Analytics app. The image below shows an example of one of these dashboards:

react dynamic dashboard

Here are some key features of interest to the customer:

  • A catalog of pre-defined configurable tile types
  • Tiles featuring arbitrary UI elements such as rich text, charts, grids, and gauges
  • Customizable dashboards that let users add, remove, reorder, and configure tiles
  • Touch screen, drag and drop functionality for mobile users

We created Wijmo’s Dynamic Dashboard and made it available to all our users. The sample was implemented using Wijmo and Angular 1.x. When we released the Wijmo React interop , we decided to create a React-based version.
Below we outline the steps for porting the dynamic dashboard sample to React. The React version of the sample looks exactly like the original, but the implementation is quite different.

Application Architecture

The dynamic dashboard app has two components: the frame and the tiles. The dashboard frame is a stateful component. It defines a catalog of tile types available and a list of the tiles that make up the current dashboard. It also provides logic for adding, removing, and reordering the tiles.

The tiles are stateless (in the React sense). They receive data from the frame via properties (passed in as element attributes). The app defines nine types of tiles, including various charts, gauges, graphs, and a grid.

In a traditional OOP application, all tiles extend a base tile class. In React (and many other JavaScript frameworks), you are encouraged to use composition instead of inheritance. This is a limitation. Composition and inheritance are different mechanisms, each has its uses, and one does not fully replace the other.

In our app, we follow the React team’s advice and implement our tiles using composition rather than inheritance. We define a Tile component that provides the tile border (with a header and buttons). The Tile component has a “content” property that defines the actual tile content.

Try Wijmo's React controls

Download the latest version of Wijmo

Download Now!

Dashboard Frame Component

The app component implements our dashboard frame. It is instantiated with the following code snippets from the index.html and index.jsx files:

index.html
<body>
  <div id=”app”></div>
</body>
index.jsx
ReactDOM.render(React.createElement(App), document.getElementById(‘app’));

The app parameter in the call to React.createElement is our dashboard frame component. It is implemented as follows:

export class App extends React.Component {
  constructor() {
    super(...arguments);
    // Color palette
    this.palette = [ ... ];
    // Icons assets
    this.icons = { ... };
    // Tile names and types
    this.tileCatalog = [
      { name: 'Grid', tile: Grid, icon: this.icons.grid },
      { name: 'Radial Gauge', tile: RadialGauge, icon: this.icons.radialGauge },
      { name: 'Linear Gauge', tile: LinearGauge, icon: this.icons.linearGauge },
      { name: 'Bar Chart', tile: BarChart, icon: this.icons.barChart },
      ...
    ];
    this.key = 0;
    this.state = {
      isWideMenu: false,
      tileCatalog: new wjcCore.CollectionView(this.tileCatalog),
      tiles: this.getTiles(),
      key: this.key,
      data: this.getData(),
    };
  }

  // tiles currently in use
  getTiles() { ... }

  // generate some data to show in the tiles
  getData() { ... }

  // gets a tile content by name
  getTileContent(name) {
    const { data, tileCatalog } = this.state;
    const arr = tileCatalog.items;
    for (let i = 0; i < arr.length; i++) {
      if (arr[i].name == name) {
        return React.createElement(arr[i].tile, {
          data: new wjcCore.CollectionView(data),
          palette: this.palette
        });
      }
    }
    throw '*** tile not found: ' + name;
  }

  // adds a tile to the dashboard
  addTile(name) {
    const { tiles, key: stateKey } = this.state;
    const key = stateKey + 1;
    this.setState({ tiles: [{ name, key }, ...tiles], key });
  }

  // removes a tile from the dashboard
  removeTile(tileIndex) {
    const tiles = this.state.tiles.filter((item, index) => index != tileIndex);
    this.setState({ tiles: tiles });
  }

  // initialize component after it has been mounted
  componentDidMount() { ... }

  // allow users to re-order elements within a panel element
  // we work with the DOM elements and update the state when done.
  enableItemReorder(panel) { ... }

  // render the dashboard
  render() { ... }
}

The app component’s state has five elements:

  • isWideMenu: determines whether the tile menu displays the entire side menu or only the icons
  • tileCatalog: the list of available tile types that can be added to the dashboard
  • tiles: a list of the tiles currently on the dashboard
  • key: the identifier used when creating the next tile
  • data: data shown on the dashboard and passed as a prop to all tiles

Below, the component’s render method shows how the state generates the dashboard.

Create a return statement that will render all our elements:

render() {
  ... 
  return (<div className="container">
    <div className={`menu ${isWideMenu ? 'menu--open' : ''}`}>
      {renderMenuToggle}
      {renderMenuItems}
    </div>
    <div className="hr"/>
    <div className="content">
      <div className="dashboard">{renderDashboard}</div>
    </div>
  </div>);
}

JSX syntax looks a lot like HTML, with some minor differences. Instead of “class,” for example, we must use “className.” That is because although it looks like HTML, this is really JavaScript code, and the DOM element’s property is called “className,” not “class.”

Next, create the renderMenuToggle and renderMenuItems object that allow users to select tiles and add them to the dashboard, as well as shrink and expand the menu:

const { tiles, isWideMenu } = this.state;
// animated toggle menu
const renderMenuToggle = (<div className="menu-toggle" onClick={() => this.setState({ isWideMenu: !isWideMenu })}>
  <svg width="30" height="20" viewBox="0 0 30 20" fill={this.palette[2]}>
    <rect x="10" y="5" width="11" height="1"/>
    <rect x="10" y="15" width="11" height="1"/>
    <polygon style={{ transition: 'all 250ms ease-out 50ms', transformOrigin: 'center center', transform: isWideMenu ? 'scaleX(-1)' : 'scaleX(1)' }} points="..."/>
  </svg>
</div>);
// menu items
const renderMenuItems = (<React.Fragment>{this.tileCatalog.map((item) => (<div key={`Menu ${item.name}`} className="menu-item" title={item.name} onClick={() => this.addTile(item.name)}>
  <svg width="64" height="64" viewBox="0 0 64 64">
    {item.icon.map((entity, key) => (<React.Fragment key={`Menu Item ${key}`}>{entity}</React.Fragment>))}
  </svg>
  <div className="menu-item-name">{item.name}</div>
</div>))}
</React.Fragment>);

When the user clicks on one of the icons, the addTile method is called and implemented as follows:

addTile(name) {
  const { tiles, key: stateKey } = this.state;
  const key = stateKey + 1;
  this.setState({ tiles: [{ name, key }, ...tiles], key});
}

First, the method makes a copy of the current tiles array and increments the key that uniquely identifies tile instances.

Then it adds a new item to the tiles array with the name specified by the catalog’s currentItem, and calls setState to update the state with the new tiles array.

When the setState is called, React updates the UI so it shows the updated tiles array.
The code used by these items to render the dashboard tiles is typical React. It uses the array.map method to generate a list of components that make up the UI. In this case, the components are instances of Tile following properties:

  • header: the tile name displayed in the tile header
  • content: a component that represents the tile content
  • onRemove: callback invoked when the user clicks the “delete” button on the tile header
  • index: the index of the tiles within the tile array
  • key: the item identifier React uses to optimize rendering

Note: The key and index parameters are used for different purposes. The app component uses the index parameter to identify tiles in the tiles array, specifically when deleting tiles. The index of a tile may change if tiles are removed or reordered.

The React framework uses the key parameter, to establish a mapping between array elements and components. The key does not change when tiles are removed or reordered.

const { tiles, isWideMenu } = this.state;
...
// displayed when the dashboard is empty
const renderBlankTile = (<div className="blank">
  <svg width="24" height="24" viewBox="0 0 24 24" fill={this.palette[0]}>
    <path d="..."/>
  </svg>
  <div>Click on an item on the menu bar to add the new tile to the dashboard.</div>
</div>);

// list of tiles
const renderTiles = (<React.Fragment>{tiles.map((item, index) => (<Tile header={item.name} content={this.getTileContent(item.name)} onRemove={this.removeTile.bind(this)} index={index} key={item.key}/>))}</React.Fragment>);

const renderDashboard = tiles.length ? renderTiles : renderBlankTile;

The renderTiles item uses the getTileContent method to create and return a component of a given type.

It looks up the component type based on the name and calls React.createElement to instantiate the tile:

getTileContent(name){  
  const { data, tileCatalog } = this.state;  
  const arr = tileCatalog.items;  
  for(let i = 0; i < arr.length; i++) {  
    if(arr[i].name == name) {  
      return React.createElement(arr[i].tile, {  
        data: new wjcCore.CollectionView(data),  
        palette: this.pallete  
      });  
    }  
  }  
  throw ‘*** tile not found: ‘ + name;  
}

The removeTile method acts as an event handler. It is called by the Tile component when the user clicks the delete button on the tile header. The code uses the array.filter method to select all tiles except the one that was clicked, and calls setState to update the state with the new tiles array.

As before, the call to setState makes React update the UI showing the updated tiles array.

removeTile(tileIndex) {
  const tiles = this.state.tiles.filter((item, index) => index != tileIndex);
  this.setState({ tiles: tiles });
}

Tile Components

The Tile component represents a frame that holds all types of tile content. It is a component implemented as follows:

import * as React from 'react';
export const Tile = ({ header, content, index, onRemove }) => (<div className="tile" draggable={true}>
  <div className="tile-container">
    <div className="tile-header">{header}</div>
    <div className="buttons">
      <div className="button" title="Close Tile" onClick={() => onRemove(index)}>
        <svg width="24" height="24" viewBox="0 0 24 24">
          <path d="..."/>
        </svg>
      </div>
    </div>
  </div>
  <div className="tile-content">{content}</div>
</div>);

Create a frame with header, content, index, and onRemove properties. These properties all get assigned in the dashboard when the tile is created.

Notice how the outer div has its draggable attribute set to “true.” This enables HTML drag and drop operations.

The Tile component’s most interesting property is content, which represents the child component that contains the tile information. Our application defines nine types of tile content components, including charts, gauges, and grids. They all have a data property set by the parent component that contains the data shown on the tile.

Make the tile components as simple or complex as you like. Adding new tile types is easy: create the component to show the data in “props.data” in whatever way you want. Add the new tile type to the application component’s tileCatalog array.

Tile components may contain other components, including Wijmo data visualization controls. For example, here is the implementation of the BarChart Tile component:

// React  
import * as React from 'react';  
// Wijmo  
import * as wjcChart from '@grapecity/wijmo.chart';  
import * as wjChart from '@grapecity/wijmo.react.chart';  
export const BarChart = ({ data, palette }) => (<wjChart.FlexChart chartType={wjcChart.ChartType.Bar} itemsSource={data} palette={palette} bindingX="date">  
  <wjChart.FlexChartAxis wjProperty="axisX" format="MMM-yy"/>  
  <wjChart.FlexChartSeries name="Sales" binding="sales"/>  
  <wjChart.FlexChartSeries name="Expenses" binding="expenses"/>  
  <wjChart.FlexChartSeries name="Profit" binding="profit" chartType={wjcChart.ChartType.LineSymbols}/>  
</wjChart.FlexChart>);</td>

And here is the implementation of the Grid Tile component:

// React  
import * as React from 'react';  
// Wijmo  
import * as wjcGrid from '@grapecity/wijmo.grid';  
import * as wjGrid from '@grapecity/wijmo.react.grid';  
export const Grid = ({ data, palette }) => (<wjGrid.FlexGrid isReadOnly={true} headersVisibility={wjcGrid.HeadersVisibility.Column} selectionMode={wjcGrid.SelectionMode.ListBox} itemsSource={data}>  
  <wjGrid.FlexGridColumn header="ID" binding="id" width={50}/>  
  <wjGrid.FlexGridColumn header="Date" width="*" binding="date" format="MMM yyyy"/>  
  <wjGrid.FlexGridColumn header="Sales" binding="sales" format="c"/>  
  <wjGrid.FlexGridColumn header="Expenses" binding="expenses" format="c"/>  
  <wjGrid.FlexGridColumn header="Profit" binding="profit" format="c"/>  
</wjGrid.FlexGrid>);</td>

The main advantage of the React implementation of this app is the neat encapsulation of the Tile components. Each one is represented by a single JSX file that contains the logic and UI.

Drag and Drop Tiles

Reordering tiles with drag and drop functionality is a key feature of this application.
We already had this code written for the Angular version of the app. The code uses standard HTML5 drag and drop functionality which was quite easy to port to React.

Move the original drag and drop code into a method called enableItemReorder and call that from the main component’s componentDidMount method:

componentDidMount() {  
  // enable tile drag/drop  
  const panel = document.querySelector(‘.dashboard’);  
  this.enableItemReorder(panel);  
}

The enableItemReorder method adds handlers to the standard “dragstart”, “dragover”, “drop”, and “dragend” HTML5 events.

Because the HTML drag/drop events work with the actual HTML elements on the DOM (and not with the React virtual DOM), we must update the state when a drop operation is finalized. This is done when handling the “drop” event, as shown in the code snippet below:

enableItemReorder(panel) {  
  let dragSource = null;  
  let dropTarget = null;  
  // add drag/drop event listeners  
  panel.addEventListener('drop', (e) => {  
    if (dragSource && dropTarget) {  
      // finish drag/drop  
      e.stopPropagation();  
      e.stopImmediatePropagation();  
      e.preventDefault();  
      // re-order HTML elements (optional here, we are updating the state later)  
      const srcIndex = getIndex(dragSource);  
      const dstIndex = getIndex(dropTarget);  
      const refChild = srcIndex > dstIndex ? dropTarget : dropTarget.nextElementSibling;  
      dragSource.parentElement.insertBefore(dragSource, refChild);  
      // focus and view on the tile that was dragged  
      dragSource.focus();  
      // update state  
      let tiles = this.state.tiles.slice();  
      tiles.splice(srcIndex, 1);  
      tiles.splice(dstIndex, 0, this.state.tiles[srcIndex]);  
      this.setState({ tiles: tiles });  
    }  
  });  
}

The final piece is touch support. Since the code relies on native HTML5 drag/drop events, we added drag/drop touch support simply by including the DragDropTouch polyfill in our project.

The screenshot below shows the running application:

dynamic dashboard react

Conclusion

Porting the dynamic dashboard sample to React was surprisingly easy given the relative complexity of the app. Creating new tiles of arbitrary types, supporting drag and drop operations, and encapsulating arbitrary components within tiles are not trivial tasks.

Some significant benefits of using React in this sample were:

  • The ability to use JSX and get full design-time error checking, IntelliSense, and rich debugging for the entire app (including the JSX markup)
  • The clean state-driven architecture imposed by React (which may take a little getting used to, but tends to pay off later)
  • The ability to encapsulate tiles neatly into single-file components

Use the dynamic dashboard sample as a starting point for actual dashboard apps . Some features that could be added to improve the application are:

  • The ability to save and restore dashboard layouts to local storage
  • Adding states to tiles so they can be customized by the users

Learn more about Wijmo on our home page.

Learn more about using Wijmo in React applications at our React page.

Check out the completed dynamic dashboard sample by clicking the link below:

Dynamic Dashboard Sample

Try Wijmo's React controls

Download the latest version of Wijmo

Download Now!