Ext JS to React: Panel

   JavaScript
Ext JS to React: 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 Ext JS Panel component makes it easy to include a collapsible container with a header element containing a title, collapse control, and any number of other icon tools you choose. In this article we’ll look at how we can set up the collapse wrapper and header element needed to approximate a panel component. The collapsed state of the panel may be managed by either a passed prop or a user control for the same flexibility you might expect from an Ext JS Panel. Additionally, we’ll use the all new Font Awesome 5 font package for our collapse control.

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.

First, let’s install the font icon packages that we’ll use in our panel header and panel instance code below:

npm i --save @fortawesome/fontawesome
npm i --save @fortawesome/react-fontawesome
npm i --save @fortawesome/fontawesome-free-solid
npm i --save @fortawesome/fontawesome-free-brands


React Header Class

Let’s create the Header component that our Panel component will use:

import React, { Component } from 'react';
import FontAwesomeIcon from '@fortawesome/react-fontawesome';
import faChevronDown from '@fortawesome/fontawesome-free-solid/faChevronDown';
import './Header.css';

class Header extends Component {
  render() {
    const { collapsible, preTools, postTools, title, toggleCollapse } = this.props;

    return (
      <div className="header">
        {title}
        <div style={{ flex: 1 }}></div>
        {preTools}
        {collapsible &&
          <FontAwesomeIcon
            className="collapse-tool header-tool"
            icon={faChevronDown}
            onClick={toggleCollapse}
          />
        }
        {postTools}
      </div>
    );
  }
}

export default Header;

Our header component accepts a title config to display text similar to the title config in an Ext JS Panel / Header. If collapsible is passed, the header will display a collapse / expand control that calls the function passed in as the toggleCollapse prop. Here is where we use the faChevronDown font icon from our import above. Arbitrary tools, any element or React component you’d like, may be passed in using the preTools and postTools props to live in the space next to the collapse control. We’ll demonstrate this option in our code example further down in the article. Finally, you’ll see there is a full-width spacer div set between the title and the tools to push each to the opposite ends of the header.

React Header CSS

The Header class imports its own Header.css file:

.header {
  display: flex;
  border: 1px solid #3d83cc;
  border-bottom-width: 0;
  background-color: #3d83cc;
  padding: 4px 8px;
  color: white;
}
.header-tool {
  cursor: pointer;
  transition: all 200ms cubic-bezier(0.25, 0.46, 0.45, 0.94);
  margin-left: 6px;
}
.expanded .collapse-tool {
  transform: rotate(-180deg);
}


React Expander Class

Next, we’ll set up the expander logic and JSX used to collapse and expand the panel body. We’ve abstracted the expander out to be its own component. This makes it easy to use in our Panel component or any other component we want to have a collapsible element. The panel’s body element and any “docked” items are rendered within the expander element using a render prop returning the panel body’s JSX.

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

class Expander extends Component {
  static defaultProps = {
    expanded: true
  }

  componentDidMount = () => {
    this.setHeight(this.props.expanded);
    this.forceUpdate();
  }

  componentWillReceiveProps = ({ expanded }) => {
    this.setHeight(expanded);
  }

  setHeight = (expanded) => {
    const { scrollHeight } = this.expandWrap;
    this.wrapHeight = expanded ? scrollHeight + 'px' : 0;
  }

  render () {
    const { className = 'expander-wrap', style = {} } = this.props;
    Object.assign(style, {
      height: this.wrapHeight
    })

    return (
      <div
        ref={el => this.expandWrap = el}
        className={className}
        style={style}
      >
        {this.props.render(this.props)}
      </div>
    );
  }
}

export default Expander;

To animate the expand / collapse action we need to set the height of the wrapping Expander div explicitly. The setHeight method fetches the scrollHeight property of the wrapping div for use by the render method. When the Expander is mounted or receives props the height is re-evaluated. The expanded prop is set to true by default. Passing in expanded as false will collapse the expander div to a height of 0. Our Panel’s toggleCollapse click handler will do just that, which we’ll see in the Panel component code below.

React Expander CSS

The CSS file for the Expander class:

.expander-wrap {
  overflow-y: auto;
  transition: all 200ms cubic-bezier(0.25, 0.46, 0.45, 0.94);
  border: 1px solid #3d83cc;
  border-top-width: 0;
}


React Panel Class

Our Panel class will incorporate the Header and Expander class defined above. We’ll accept any of the props needed by the Header or Expander and pass them along so that the props can be used directly on a Panel instance similar to how the title config option is used in an Ext JS panel and passed on to the Header.

import React, { Component } from 'react';
import Header from './Header';
import Expander from './Expander';
import './Panel.css';

class Panel extends Component {
  static defaultProps = {
    expanded: true
  }

  state = {
    expanded: this.props.expanded
  }

  componentWillReceiveProps = ({ expanded }) => {
    this.setState({ expanded });
  }

  toggleCollapse = () => {
    this.setState({
      expanded: !this.state.expanded
    });
  }

  render() {
    const { collapsible, expandDir, preTools,
      postTools, style = {}, title } = this.props;
    const { expanded } = this.state;
    const showHeader = title.length || collapsible;
    const className = `panel${expanded ? ' expanded' : ''}`;

    return (
      <div className={className} style={style}>
        {showHeader &&
          <Header
            title={title}
            collapsible={collapsible}
            toggleCollapse={this.toggleCollapse}
            preTools={preTools}
            postTools={postTools}
          />
        }
        <Expander expanded={expanded} expandDir={expandDir} render={() => (
          <div className="body-el">
            {this.props.children}
          </div>
        )} />
      </div>
    );
  }
}

export default Panel;

The bulk of the Panel class is the JSX used to render the panel along with its header and expander. If neither the title nor collapsible prop is passed the header will not be rendered. Whether the panel is collapsed or expanded is dictated by the panel’s expanded property in the component state. The expanded property is set by the expanded prop initially as well as when the component is already mounted and receives new props. The expanded prop is also set when the toggleCollapse method is called. The toggleCollapse method is passed as a prop to the header component for use with the expand / collapse control. Within the expander we have a panel body div that will hold any elements / components passed to a panel instance.

React Panel CSS

Below is the CSS used by the Panel class:

.panel {
  display: flex;
  flex-direction: column;
}
.body-el {
  padding: 8px;
}


React Panel Example

Now that we’ve looked at how we might abstract a panel, header, and the expand / collapse mechanism out, let’s take a look at how we might use a Panel component within an application.

import React, { Component } from 'react';
import Panel from './Panel';
import FontAwesomeIcon from '@fortawesome/react-fontawesome';
import faReact from '@fortawesome/fontawesome-free-brands/faReact';

class App extends Component {
  render() {
    return (
      <Panel
        title="My Panel"
        collapsible
        style={{width: '300px'}}
        preTools={
          <FontAwesomeIcon
            className="header-tool"
            icon={faReact}
            onClick={() => console.log('React button handler')}
          />
        }
      >
        1
        <br />
        2
        <br />
        3
      </Panel>
    );
  }
}
export default App;

The App component makes use of our Panel class and passes the props needed for our custom panel instance. The panel receives a title and is set as collapsible (boolean props passed with no associated value evaluate to true). We constrain its width to 300px by passing in a style object. The preTools prop is passed on to the header within the panel. By passing a component to preTools we’re able to render an additional user control with a bespoke click handler defined within the component. At render, it will reside just before the collapse / expand control. Finally, we pass in a few arbitrary elements as children of the panel, which end up being the contents hidden from view when the panel is collapsed.

By default the panel is expanded:

Ext JS to React: Panel, Expanded



Clicking on the expand / collapse tool collapses the Panel body (Expander element):

Ext JS to React: Panel, Collapsed




Conclusion

With a little bit of setup we were able to define a flexible set of components that can easily be reused within one or more projects. All of the functionality of our panel could have easily resided within a single component. The Header is effectively just a simple div with a flexbox layout. The expander logic could be wrapped by the Panel class itself versus being defined separately. In this exercise, we explored how that logic could be broken off and easily used across various components, but of course, your own level of encapsulation used will likely vary according to your needs or that of your employer. Stay tuned to this series for our next article on recreating the Ext JS tab panel in React!


This website uses cookies

These cookies are used to collect information about how you interact with our website and allow us to remember you. We use this information in order to improve and customize your browsing experience, and for analytics and metrics about our visitors both on this website and other media. To find out more about the cookies we use, see our Privacy Policy.

Please consent to the use of cookies before continuing to browse our site.

Like What You See?

Got any questions?


>