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.
Container classes in Ext JS are responsible for sizing and positioning any direct child components rendered to them. The sizing and positioning is done using the Ext JS layout system which incorporates CSS and JavaScript to manage how the child items are rendered. The classic toolkit used JavaScript to calculate the dimensions and spacing of the child items while the modern toolkit is able to use newer CSS specs. This shift allows the browser to handle the setting of child dimensions and spacing. Your React components can leverage the same CSS principles to manage the most common layouts in a simple and performant manner using CSS flexbox (IE11+). A number of pre-built layout libraries such as react-bootstrap and a couple we’ll demonstrate here may also come in handy as you architect the look and feel of your application.
In this article, we’ll explore how to manage parent / child component layouts using some of the more popular layouts from the Ext JS framework. Each layout technique will be shown in isolation, but can ultimately work in a larger composition as you’re used to seeing in Ext JS. Meaning, for example, a border-type layout’s west region could have child components structured in an accordion-type layout. In the following examples we’ll keep the React code as simple as possible so that our focus is narrowed to the layout techniques themselves instead of on the React API.
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.
Box / Card Layout Class
To start off, we’re going to define a box / card layout component that will take care of the layouts for our fit, card, hbox, and vbox examples. Below is the Layout
class that we’ll place at src/Layout.js
:
import React, { Fragment } from 'react'; import './Layout.css'; export default ({ children, type }) => { const layoutClassName = type ? `layout-${type} ` : ''; return ( <Fragment> {React.Children.map(children, child => { const { className: childClassName = '' } = child.props; const className = `${layoutClassName}${childClassName}`; return React.cloneElement(child, { className }); })} </Fragment> ); };
The Layout
class accepts a type prop and a child item (though technically the layout will be applied to any and all direct child items). The Layout
class returns a React Fragment which is essentially a DOM-free component. It may be used to wrap / interact with child items composed within it without returning any unnecessary DOM elements. Within the wrapping Fragment
we loop over the child items composed within the Layout
and apply a className
of “layout-type” using the type
string passed in. This is done by cloning the child element and passing in as its className
our “layout-type” className
along with the className prop if set on the child instance.
And that’s really all there is to the Layout
class. Its job is to poke one more class name on to the component passed in. The heavy lifting is then done by CSS.
Box / Card Layout CSS
The CSS imported by our Layout
class at src/Layout.css
is:
/* FIT */ .layout-fit { display: flex; box-sizing: border-box; } .layout-fit > * { flex-grow: 1; } /* CARD */ .layout-card { display: flex; box-sizing: border-box; } .layout-card > * { flex-grow: 1; } /* HBOX */ .layout-hbox { display: flex; } /* VBOX */ .layout-vbox { display: flex; flex-direction: column; }
We’ll use this Layout
class and CSS for the following fit, card, hbox, and vbox examples.
Fit Layout
The fit layout is pretty straightforward. For the fit layout we want a single child to take up 100% height and width of the parent. To test this, we’ll just add a style
prop to the parent with explicit dimensions and the Layout
class will add a single className
of “layout-fit”. In the Layout.css
file we’ll define a rule for layout-fit
saying its direct child has a height of 100%.
import React from 'react'; import Layout from './Layout'; const containerStyle = { width: 200, height: 200, border: '4px solid #923131' }; const itemStyle = { backgroundColor: '#ff4b4b' }; const App = props => { return ( <Layout type="fit"> <div style={containerStyle}> <div style={itemStyle} /> </div> </Layout> ); }; export default App;
You can review the sample code on the Git repo.
Card Layout
The card layout aims to show one child item at a time as though it was the only item in a “fit” layout. While all of the card views will be represented in the class, only one needs to be rendered at a time. This is a bit of a philosophical departure from the line of thinking used in Ext JS. Let’s say that card 0 is a form and it’s been populated, but not submitted, and you navigate to the next card. If structured properly, the form data is managed by an ancestor component or a global state manager, not the form component itself. When the form card is active once again, the data is fed back to it via its props.
The fact that the views in React need only display data fed to them, not manage the data itself, means that our card layout can be very lean. In the example below, we pass in an activeCard
prop (defaulting to 0). We set the card items in an array and render only the item from the card array at the index indicated by the activeCard
prop.
import React from 'react'; import Layout from './Layout'; const containerStyle = { width: 100, height: 100, border: '1px solid gray', color: 'white' }; const itemStyleA = { background: '#ee4d77', padding: 4 } const itemStyleB = { background: '#b15b90', padding: 4 } const App = ({ activeCard = 0 }) => ( <Layout type="card"> <div style={containerStyle}> {[<div style={itemStyleA}>one</div>, <div style={itemStyleB}>two</div>][activeCard]} </div> </Layout> ); export default App;
Passing in an activeCard
prop value of 1 renders the second card in the items array (in src/index.js
):
ReactDOM.render(<App activeCard={1}/>, document.getElementById('root'));
You can review the sample code on the Git repo.
hbox Layout
The hbox layout is where we will really start to see the benefit of the CSS flexbox layout system. The hbox layout organizes its child items horizontally with no wrapping. This is the layout that’s applied by default to Header
and Toolbar
components in Ext JS. Child items can have an explicitly set width, sized naturally based on content, or can have a style attribute of flexGrow that’s similar to the flex
config option you’re used to in Ext JS. In the following example, the container element has its className
set to “layout-hbox”, which sets its CSS display
to flex
. Its child items are explicitly sized, configured with a flex of 1, configured with a flex of 2, and finally naturally sized.
import React, { Component } from 'react'; import Layout from './Layout'; const containerStyle = { width: 300, height: 100, color: 'white' }; class App extends Component { render() { return ( <Layout type="hbox"> <div style={containerStyle}> <div style={{ width: 40, background: '#df8f2e' }}>fixed</div> <div style={{ flexGrow: 1, background: '#f26f38' }}>flex 1</div> <div style={{ flexGrow: 2, background: '#ee4d77' }}>flex 2</div> <div style={{ background: '#b15b90' }}>wrap</div> </div> </Layout> ); } } export default App;
You can review the sample code on the Git repo.
Vbox Layout
The vbox layout is very nearly identical to hbox except that that items are arranged in a column. Let’s look at the previous example with a vertical twist. This is the layout that’s applied by vertically oriented Header
and Toolbar
components in Ext JS.
import React, { Component } from 'react'; import Layout from './Layout'; const containerStyle = { width: 100, height: 300, color: 'white' }; class App extends Component { render() { return ( <Layout type="vbox"> <div style={containerStyle}> <div style={{ height: 40, background: '#df8f2e' }}>fixed</div> <div style={{ flexGrow: 1, background: '#f26f38' }}>flex 1</div> <div style={{ flexGrow: 2, background: '#ee4d77' }}>flex 2</div> <div style={{ background: '#b15b90' }}>wrap</div> </div> </Layout> ); } } export default App;
The only difference in our CSS is that we add flex-direction: column
to orient our child items vertically instead of horizontally.
You can review the sample code on the Git repo.
Border Layout
Ext JS made a resizable border layout very easy to achieve. While a bit less elegant than the implementation you’re accustomed to, it’s easy enough to create a layout with the four cardinal regions (north, south, east, and west) and a center region that takes up the remaining space. To do this we’ll use an existing third-party package: react-split-pane.
The following example sets up the SplitPane
components needed to organize our border layout. Each SplitPane
component will manage two child items which is why we use multiple SplitPane
s below to accommodate all border regions. By default the first sub-component is the one that receives the sizing props. To specify the second component (like we do with both the “south” and the “east” components) the property primary="second"
is set on the SplitPane
.
import React, { Component } from 'react'; import './App.css'; import SplitPane from 'react-split-pane'; class App extends Component { render () { return ( <SplitPane split="horizontal" minSize={50} maxSize={300} defaultSize={100}> <div>North</div> <SplitPane split="horizontal" primary="second"> <SplitPane split="vertical"> <div>West</div> <SplitPane split="vertical" primary="second" defaultSize={200} maxSize={400} minSize={100}> <div>Center</div> <div>East</div> </SplitPane> </SplitPane> <div>South</div> </SplitPane> </SplitPane> ); } } export default App;
The CSS used to decorate the splitters is:
.Resizer { background: #f1f1f1; z-index: 1; box-sizing: border-box; } .Resizer:hover { transition: all .25s ease; } .Resizer.horizontal { height: 11px; cursor: row-resize; width: 100%; } .Resizer.horizontal:hover, .Resizer.vertical:hover { background: #d6d6d6; } .Resizer.vertical { width: 11px; cursor: col-resize; } .Resizer.disabled { cursor: not-allowed; } .Resizer.disabled:hover { background: #f1f1f1; }
You can review the sample code on the Git repo.
Accordion Layout
For the accordion layout, we’ll turn once again to a third party solution instead of writing our own collapse markup / manager. The react-sanfona
module serves pretty well for our purposes allowing us to configure the accordion for only a single panel open at a time or multiple. Each child panel has a title and body component resulting in a similar look to the panels used in an Ext JS accordion layout. To allow multiple child items to be opened at the same time you can use the prop allowMultiple={true}
on the parent Accordion
node.
import React, { Component } from 'react'; import { Accordion, AccordionItem } from 'react-sanfona'; import './App.css'; class App extends Component { render () { return ( <Accordion style={{width: '400px'}}> {[1, 2, 3, 4, 5].map(item => ( <AccordionItem title={`Item ${item}`} expanded={item === 1}> {`Item ${item} content`} </AccordionItem> ) )} </Accordion> ); } } export default App;
The react-sanfona package doesn’t ship with its own CSS package, but does provide a clear structure for us to attach our own styling to. Below are the CSS rules we’ll add to App.css to produce the example seen below:
.react-sanfona { border: 1px solid #ccc; border-radius: 3px; } .react-sanfona-item-title { background-color: #FFF2EE; border-top: 1px solid #ccc; color: #555; padding: 20px; text-transform: uppercase; } .react-sanfona-item:first-child .react-sanfona-item-title { border-top: none; } .react-sanfona-item-expanded .react-sanfona-item-title { background-color: #f76915; color: #fff; } .react-sanfona-item-body-wrapper { color: #555; padding: 20px; position: relative; }
You can review the sample code on the Git repo.
Conclusion
Child components often need to be organized into a developer-designated layout to be user friendly. Fortunately, in React applications the common layouts needed are easily accomplished with a little CSS or by importing a pre-built package. Even the border layout could be simplified. If all that is needed is the layout and not user-resizable spacers, that can easily be accomplished with CSS flexbox like we used for hbox and vbox layouts. Ultimately, combining third party solutions like accordion, resizable panes, and collapsible components will result in the basic utility you’re looking for when emulating the layout capabilities found in the Ext JS framework. If you’ve discovered a great layout manager for any of the above layout types in your travels, please feel free to post it in the comments below!
Seth Lemmons
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…