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 Grid component gets all the mentions. But, what about the lowly List? Yes, the List has quietly been taking care of our column-less data presentation needs for years now and maybe it should have a little spotlight, too. Grids and Lists share a common pedigree and their goal is largely common: display a dataset as items, often rows, that the user is able to interact with. Lists are simpler animals compared to the Grid. For the simpler use cases, that’s a good thing. In the following sections, we’ll show just how easy it is to author a List component using React. We’ll demonstrate how you can add functionality like selection and clickable disclosure icons separate from the List class. This allows your List to be as lean or as complete as your use case may dictate.
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.
Writing a List
React, along with JSX and JavaScript itself, makes creating a simple list extremely easy. You may be surprised how little React code it takes to generate a list component using an array of data. For these examples we’ll use the following data set:
export default [{ id: 1, firstName: 'Haydee', lastName: 'Fennell', company: 'NitroSystems', hireDate: '27/11/2002' }, { id: 2, firstName: 'Stan', lastName: 'Garling', company: 'Ulogica', hireDate: '13/05/2001' }, { id: 3, firstName: 'Gavin', lastName: 'Paquette', company: 'MediaDime', hireDate: '27/01/2008' }, { id: 4, firstName: 'Vernon', lastName: 'Drolet', company: 'Qualcore', hireDate: '04/05/1998' }, { id: 5, firstName: 'Marcus', lastName: 'Brier', company: 'GrafixMedia', hireDate: '30/08/1995' }, { id: 6, firstName: 'Haley', lastName: 'Pullman', company: 'Eluxa', hireDate: '05/06/2007' }, { id: 7, firstName: 'Raylene', lastName: 'Seal', company: 'OpenServ', hireDate: '28/12/2005' }, { id: 8, firstName: 'Dannielle', lastName: 'Sager', company: 'Infratouch', hireDate: '18/12/1997' }, { id: 9, firstName: 'Madelyn', lastName: 'Sprowl', company: 'Hivemind', hireDate: '09/12/1992' }, { id: 10, firstName: 'Drew', lastName: 'Hollis', company: 'Hivemind', hireDate: '01/07/1993' }];
React ListItem
class
Our list view is essentially a div
with child rows created using an array of data objects. Let’s first define the ListItem
class that we’ll use in each of our list examples:
import React from 'react'; import './ListItem.css'; const ListItem = (props) => { const { children, className = '', company, firstName, hireDate, id, lastName, onClick, toolPosition } = props; const toolCls = toolPosition === 'left' ? 'item-tools-left' : ''; return ( <div className={`list-item ${className} ${toolCls}`} onClick={onClick} data-id={id} > <div className="body"> <div className="main">{lastName}, {firstName}</div> <div className="secondary"> {company} <span className="meta"> (hired: {hireDate})</span> </div> </div> <div className="tools">{children}</div> </div> ); }; export default ListItem;
ListItem
explained
The ListItem
class is a container with two direct child items. The first child item (decorated with the "body"
class name) contains the body content of our list items as a template consuming the data object passed to it via props. In this example we’re outputting a few properties (name, etc.), but in a real world application we could display any number of properties with any number of custom React components. The <div className="body">
portion of the ListItem
can be substituted with other components with their own HTML element structure and style rules.
Example List Body class
For example, we could define the following class:
const AltEmployee = ({ company, firstName, lastName }) => ( <div className="body">{firstName} {lastName} (<i>{company}</i>)</div> );
It could then be composed into ListItem
as needed:
// … from the above example <div className="body"> <div className="main">{lastName}, {firstName}</div> <div className="secondary"> {company} <span className="meta"> (hired: {hireDate})</span> </div> </div> // … could be replaced with the following <AltEmployee firstName={firstName} lastName={lastName} company={company}/>
After the "body"
element we see the second child item, a div
with the class name of "tools"
. It will render any children passed to the ListItem
. We’ll look at how to pass in child items similar to the disclosure buttons seen in Ext JS / Sencha Touch a bit further down the article. For now, we’ll just set up the structure allowing any child items we choose to be composed within the ListItem
.
List.css
Here are the CSS styles we’ll use for the ListItem
’s contents, including any child items. We make use of CSS flexbox (IE11+) to manage the layout of the ListItem
‘s child elements, including the ability to reverse the child items’ order by passing the prop of toolPosition="left"
to a ListItem
to arrange the "tools"
element before the "body"
element.
.list-item { border-bottom: 1px solid #eaeaea; padding: 5px; cursor: pointer; user-select: none; display: flex; } .list-item.item-tools-left { flex-direction: row-reverse; } .list-item.item-tools-left .tools { margin-right: 12px; } .list-item .body { flex: 1; } .list-item .main { font-weight: bold; } .list-item .meta { color: #a5adaf; } .list-item .tools { display: flex; align-items: center; justify-content: center; } .list-item .tools > * { margin-left: 8px; }
Simple list example
Let’s create a simple list using the ListItem
class and our data set:
import React from 'react'; import data from './data'; import Selectable from './Selectable'; import ListItem from './ListItem'; import Tool from './Tool'; function handleItemClick (e) { console.log('item clicked'); } const App = () => ( <div style={{width: '400px'}}> {data.map(item => <ListItem key={item.id} onClick={handleItemClick} {...item}/> )} </div> ); export default App;
In this example we keep the list pretty simple letting the ListItem
JSX and its accompanying CSS do the heavy lifting. An item-click event handler can be created and passed in using the onClick
prop.
List selection
At this point, our List implementation is only as complex as it needs to be. So, how can we add functionality to our list to be used on an as-needed basis? In the Mixins article of this series, we looked at a couple of common patterns that can be used to share code between classes. Not only does this promote the DRY principle, it ensures a consistent selection behavior for our users. One of those patterns is referred to as “render props” and is exactly the method we can use to enhance our list.
Selectable
class
Let’s take a look at the Selectable
class we’ll use to enable ListItem
selection:
import React, { Component } from 'react'; import './Selectable.css'; class Selectable extends Component { static defaultProps = { itemSelector: '.list-item' } state = { selected: [] } handleClick = (e) => { const item = this.getItemFromEvent(e); if (!item) { // if the click starts on one item and ends on another return; } const selectedId = parseInt(item.getAttribute('data-id'), 10); const { data, selection, onSelection } = this.props; let { selected } = this.state; let add = true; if (selection === 'multi') { const last = selected[ selected.length - 1 ]; if (last) { const lastIdx = data.findIndex(item => item.id === last); const currentIdx = data.findIndex(item => item.id === selectedId); if (lastIdx === currentIdx) { // indices are the same, deselect selected.splice(currentIdx - 1, 1); add = false; } if (e.shiftKey) { if (lastIdx !== -1 && currentIdx !== -1) { // get all items between the last selected item // and the current clicked item if (lastIdx < currentIdx) { for (let i = lastIdx + 1; i < currentIdx; i++) { selected.push(data[ i ].id); } } else { for (let i = lastIdx - 1; i > currentIdx; i--) { selected.push(data[ i ].id); } } } } else if (!e.ctrlKey) { // shift or ctrl keys were not pressed, need to clear out // so only the item being clicked on is selected selected.length = 0; } } } else { // single mode, clear out the array selected.length = 0; } if (add) { selected.push(selectedId); } if (selected.length > 1) { // remove duplicates selected = [ ...new Set(selected) ]; } this.setState({ selected }); if (typeof onSelection === 'function') { onSelection(selected.map(id => data.find(item => item.id === id))); } } getItemFromEvent (e) { let el = e.target; let matcher = el.matches ? 'matches' : 'msMatchesSelector'; let selector = this.props.itemSelector; while (!el[matcher](selector) && (el = el.parentElement)); return el; } getSelectedCls = (id) => { return this.state.selected.includes(id) ? 'list-item-selected' : ''; } render () { const { getSelectedCls } = this; return ( <div className="selection-wrap" onClick={this.handleClick}> {this.props.render({ getSelectedCls })} </div> ); } } export default Selectable;
Our Selectable
class is comprised of 4 class members:
state
: The state object’s selected item is updated by thehandleClick
method as the user clicks onListItem
s. It includes the ID’s from the data backing each selectedListItem
.handleClick
: This method coordinates the adding / removing of ID’s to the Selectable instance’s state as the user clicks onListItem
s. It responds to shift / ctrl key presses for each user click to coordinate the selected array appropriately.
: A helper method forgetItemFromEvent
handleClick
used to fetch theListItem
‘s wrapping element no matter which element / descendant element is clicked within theListItem
.getSelectedCls
: This method is passed to the render prop function used to render all child items. It’s called by the child items to determine whether a selected class name is to be applied to each item as it’s rendered. This allows the selection logic / decoration responsibility to be managed by the Selectable class, not any of the components / elements rendered within its render prop function (we’ll see it in action in the example to follow).
Selectable CSS
Below is the styling for the selected ListItem
s:
.list-item-selected { background-color: #eaeaea; }
Selectable list example
Here is how we can use our Selectable
component within the composition of our previous list example with only a couple of modifications:
import React from 'react'; import data from './data'; import ListItem from './ListItem'; import Selectable from './Selectable'; function handleItemClick (e) { console.log('item clicked'); } function handleSelectionChange (selected) { console.log(`selection count: ${selected.length}`); } const App = () => ( <div style={{width: '400px'}}> <Selectable selection="multi" data={data} onSelectionChange={handleSelectionChange} render={({ getSelectedCls }) => { return data.map(item => { const { id } = item; return ( <ListItem key={id} onClick={handleItemClick} className={getSelectedCls(id)} {...item} /> ); }); } }/> </div> ); export default App;
In this example, we left the handleItemClick
click handler and introduced another handler as well, handleSelectionChange
. Each will be called during user interactions with the list. The handleItemClick
handler receives the click event as a parameter while handleSelectionChange
receives the array of IDs for any selected items as the selection changes.
The Selectable
component is composed within our “list” div
in place of the items array from our first example. Its render prop function passes the getSelectedCls
method belonging to the Selectable
class. Each ListItem
calls getSelectedCls
to get an additional “selected” class for its wrapping element, but only if the Selectable
instance has recorded that particular ListItem
‘s ID as being “selected”.
To select more than one ListItem
at a time, set Selectable
‘s selection
prop to "multi"
. Holding the shift and / or control (command) key while selecting items in the list grows the selection range / items respectively.
List tools
Finally, let’s include “disclosure” tools to the list items like we’ve seen in Sencha Touch and Ext JS. We defined our ListItem
s with a containing element for rendering any child items passed in. We’ll define a Tool
class for this purpose specifically. It will make use of FontAwesome icons so first let’s install the FontAwesome packages we’ll use in our example:
npm install --save @fortawesome/fontawesome npm install --save @fortawesome/react-fontawesome npm install --save @fortawesome/fontawesome-free-solid
Note: You may find you need to run npm install
once more after installing the FontAwesome packages.
Tool
class
Now let’s define our Tool
class:
import React from 'react'; import FontAwesomeIcon from '@fortawesome/react-fontawesome'; import * as Icons from '@fortawesome/fontawesome-free-solid'; const Tool = ({ icon = 'ArrowRight', onClick }) => ( <FontAwesomeIcon icon={Icons[`fa${icon}`]} onClick={onClick}/> ); export default Tool;
Complete list example
With the Tool
class defined, we can now pass Tool
s as child items of each ListItem
. Let’s take a look at an example we built on our previous selection example allowing us to demonstrate a selectable list with Tool
s:
import React from 'react'; import data from './data'; import ListItem from './ListItem'; import Selectable from './Selectable'; import Tool from './Tool'; function handleItemClick (e) { console.log('item clicked'); } function handleSelectionChange (selected) { console.log(`selection count: ${selected.length}`); } function handleToolClick (e) { e.stopPropagation(); console.log('tool click'); } const App = () => ( <div style={{width: '400px'}}> <Selectable selection="multi" data={data} render={({ getSelectedCls }) => { return data.map(item => { const { id } = item; return ( <ListItem key={id} onClick={handleItemClick} className={getSelectedCls(id)} {...item} > {item.company === "Hivemind" && <Tool icon="Archive" />} <Tool onClick={handleToolClick}/> </ListItem> ); }); }}/> </div> ); export default App;
In this final example we bring it all together. Tool
s are displayed in the ListItem
s including one that is optionally displayed with the “Archive” icon when ListItem
has a company
value of “Hivemind”. We continue to call the ListItem
click handler and the selection change handler and we’ve added a Tool
click handler as well.
Conclusion
Enabling selection and triggering actions on a list of items is a common UI requirement. You can always use a pre-built library to handle this for you but this task is pretty simple using React alone. Allowing selecting and tools to be opt-in features using mixins can keep a list simple and lightweight, yet allow other lists the ability to add in the functionality when needed.
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…