Ext JS to React: Tab Panel

   JavaScript
Ext JS to React: Tab Panel

This is part of the Ext JS to React blog series. You can review the code from this article on the Ext JS to React Git repo.

The Tab Panel component is a commonly used view in both desktop and mobile UIs. Using tabbed views maximizes the real estate available for otherwise large, even full-screen layouts. The switchable view effectively offers different “pages” of UI within a given application view. In this article we’ll take a look at a sample implementation of tabs using React to render our views and handle user interactions.

Note: While not a requirement for React, this article’s code examples assume you’re starting with a starter app generated by create-react-app.

React Tab Class

In Ext JS, Tab Panels are passed items with text attributes and / or icons to display in auto-generated Tabs. To accomplish the same thing in React we need to first build our Tab and TabPanel classes. We can then look at an example implementation using both classes along with the CSS needed for styling and layout. Our Tab class will be used internally by our TabPanel class.

import React from 'react';
import FontAwesome from '@fortawesome/react-fontawesome';
import * as Icons from '@fortawesome/fontawesome-free-solid';
import './Tab.css';

const Tab = props => {
  const { tabtext, icon, activetab, cardindex, onClick } = props,
        isActive = activetab === cardindex ? ' active' : '';

  return (
    <div className={`tab${isActive}`} onClick={onClick}>
      {icon ? <FontAwesome icon={Icons[`fa${icon}`]} /> : ''}
      {tabtext}
    </div>
  );
};

export default Tab;

This example requires installing the FontAwesome icon modules:

npm install --save @fortawesome/fontawesome
npm install --save @fortawesome/react-fontawesome
npm install --save @fortawesome/fontawesome-free-solid

Initially, we import React, FontAwesome, and the FontAwesome icons. Next, we’ll extract some variables from the props passed in from the parent TabPanel container (which we’ll define in the next section). This will give us:

  • tabtext as text: the tab text
  • icon (if provided)
  • activetab: the currently active index / tab
  • cardindex: the index where this tab resides
  • The derived isActive state indicating whether the active index matches our tab index. We’ll use this to set an “active” CSS class on our tab to differentiate from inactive tabs
  • onClick: our TabPanel‘s tab-click handler method

Finally, we’ll return the JSX that defines the shape of our tab. This will include the class decoration when the tab is active, the click handler, and the tab text / icon.

React Tab CSS

Below is the CSS imported by the above Tab.js:

.tab {
  text-align: center;
  cursor: pointer;
  border: 2px solid transparent;
}
.tab:hover {
  background-color: #d8d8d8;
}
.tab.active:hover {
  background-color: transparent;
}
.tab .svg-inline--fa {
  margin-right: 8px;
}

React TabPanel Class

Next, let’s look at the TabPanel class:

import React, { Component } from 'react';
import Tab from './Tab';
import './TabPanel.css';

class TabPanel extends Component {
  static defaultProps = {
    activetab: 0,
    className: '',
    position: 'top'
  };

  constructor(props) {
    super(props);

    this.state = {
      activetab: props.activetab
    };
  }

  render() {
    const { children, className, position } = this.props;
    const { activetab } = this.state;

    return (
      <div {...this.props} className={`${className} tab-panel ${position}`}>
        <div className={`tab-strip`}>
          {React.Children.map(children, (child, i) => (
            <Tab
              onClick={this.onTabClick.bind(this, i)}
              {...child.props}
              cardindex={i}
              activetab={activetab}
            />
          ))}
        </div>
        <div className="card-ct">
          {React.Children.map(children, (child, i) => {
            let { className } = child.props;
            className = className ? ` ${className}` : '';

            const isActive = i === activetab ? ' active' : '';
            const cardProps = {
              ...child.props,

              className: `card${className}${isActive}`,
              cardindex: i,
              activetab: activetab
            };

            return React.cloneElement(child, cardProps);
          })}
        </div>
      </div>
    );
  }

  onTabClick(activetab) {
    this.setState({
      activetab
    });
  }
}

export default TabPanel;

React TabPanel Class Explained

The static defaultProps property sets the defaults for various props on the TabPanel. The constructor method sets the initial state and activetab property. The onTabClick method handles the Tab click that sets the activetab property that decorates the active tab and shows the active child card. The render method:

  • Combines any className string passed in with those added by the class
  • Collects the activetab from the component state object to inform the tabs / cards which one is currently active / visible
  • In the return:
    • Create the wrapping TabPanel element that will house the tab container and card container in a flex layout
    • The tab container is added and we iterate over the child nodes (cards) passed to our TabPanel to create Tab instances passing in the child nodes’ text and icon along with the current node index and the active tab index from the TabPanel‘s state. The active tab is decorated as active when the cardindex matches the activetab.
    • The child node (card) container is added and we loop over the child nodes, this time calling React.cloneElement in order to add a few props like className, cardindex, and activetab to the original nodes that were passed in. The cloned elements are returned in an array as the child nodes of the parent element. The card whose cardindex matches the activetab is shown while the other cards are hidden using CSS rules.

React TabPanel CSS

Before we look at implementing our TabPanel, let’s first show the CSS used to make our TabPanel lay out correctly and toggle active tabs / cards:

.tab-panel {
  display: flex;
}
.tab-panel.top {
  flex-direction: column;
}
.tab-panel.bottom {
  flex-direction: column-reverse;
}
.tab-panel.left {
  flex-direction: row;
}
.tab-panel.right {
  flex-direction: row-reverse;
}
 
.tab-strip {
  background-color: #e8e8e8;
  display: flex;
  justify-content: flex-start;
}
.tab-strip.indicator {
  justify-content: center;
}
.tab-panel.top .tab-strip,
.tab-panel.bottom .tab-strip {
  flex-direction: row;
}
.tab-panel.left .tab-strip,
.tab-panel.right .tab-strip {
  flex-direction: column;
}

.tab-panel.top .tab,
.tab-panel.bottom .tab {
  padding: 12px 6px;
}
.tab-panel.left .tab,
.tab-panel.right .tab {
  padding: 6px 12px;
}
.tab-panel.top .tab.active {
  border-bottom-color: #1e8bfb;
}
.tab-panel.left .tab.active {
  border-right-color: #1e8bfb;
}
.tab-panel.bottom .tab.active {
  border-top-color: #1e8bfb;
}
.tab-panel.right .tab.active {
  border-left-color: #1e8bfb;
}
 
.card-ct {
  flex: 1;
  display: flex;
} 
.card {
  padding: 12px;
  flex: 1 0 auto;
  display: none;
}
.card.active {
  display: block !important;
}

React TabPanel Example

Below is the code used to create a TabPanel with two tabs / cards each with tab text + icon:

import React, { Component } from 'react';
import TabPanel from './TabPanel';

class App extends Component {
  render() {
    return (
      <TabPanel style={{ height: '400px', width: '600px' }}>
        <div tabtext="Home" icon="Home">
          Content for the first panel
        </div>
        <div tabtext="User" icon="User">
          ... and the second panel
        </div>
      </TabPanel>
    );
  }
}

export default App;


Ext JS to React: Tab Panel


Ext JS to React: Tab Panel



In our instance example, we’ve passed in a style prop to give the TabPanel specific dimensions. The activetab index may be passed in as a prop (defaults to 0). Another available prop is `position` that orients the tabs on one of the four TabPanel‘s edges (default is “top”). Our TabPanel class appends the position passed (top, left, right, and bottom) to the className of the TabPanel‘s outer element. The CSS rules then position the tabs accordingly using flexbox.

Conclusion

There are a number of pre-built UI solutions that exist in the React community. A few worth mentioning are Material UI, Semantic, and Kendo UI. If you’d like to animate and / or swipe cards within your TabPanel be sure to check out the code in the Swipeable Example on Material UI’s tab component page. In our next blog article we’ll take a look at the carousel whose utility closely resembles that of the tab panel, but with a bit leaner navigation UI.


Like What You See?

Got any questions?