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:
Clicking on the expand / collapse tool collapses the Panel body (Expander element):
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!
Mitchell Simoens
Related Posts
-
Ext JS to React: Migration to Open Source
Worried about Migrating from Ext JS? Modus has the Answers Ideraโs acquisition of Sencha has…
-
Ext JS to React: FAQ
This is part of the Ext JS to React blog series. React is Facebook's breakout…