Ext JS to React: List

   JavaScript

This is part of the Ext JS to React blog series.

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.

Ext JS to React: List, Simple

 

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 the handleClick method as the user clicks on ListItems. It includes the ID’s from the data backing each selected ListItem.
  • handleClick: This method coordinates the adding / removing of ID’s to the Selectable instance’s state as the user clicks on ListItems. It responds to shift / ctrl key presses for each user click to coordinate the selected array appropriately.
  • getItemFromEvent: A helper method for handleClick used to fetch the ListItem‘s wrapping element no matter which element / descendant element is clicked within the ListItem.
  • 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 ListItems:

.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.

Ext JS to React: List, Selection

 

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 ListItems 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 Tools 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 Tools:

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. Tools are displayed in the ListItems 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.

Ext JS to React: List, Tools

 

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.


Like What You See?

Got any questions?