A fairly common piece of functionality that you’ll find in applications is a list of data which can be drilled into for more detailed information. This functionality is usual topped off with the ability to edit the data inline within the list.

In this week’s post I’d like to walk you through the process of how I would put together a simple version of this common solution. Hopefully after reading this article and perhaps following along you’ll be able to build further on this or be inspired to come up with a different spin on the same idea.

Want to follow along but haven’t done the previous tutorials? Clone down this repo to give you a starting point. Once you’ve cloned it down run the command npm install in the root of the project and you’ll be good to go!

An ulterior objective of this post is to also demonstrate just how much re-usability  you can get out of the components that you build in React.

If you read my post explaining why we like componentised UIs then you’ll know that re-usability is one of the main reasons we do all this componentised UI malarky in the first place! But I thought it might surprise you just how quickly you begin gaining that benefit from this.

As part of this post we’ll be reusing the form that we built in my post about building dynamic data capture forms. I’ll also point out that this post is part of a wider tutorial series that I’ve been writing on this site, if you’d like to follow along from the beginning then feel free to click on the “Tutorials” link in the menu at the top of the page.

 

Conjuring a list component

For the rest of the post we’ll be inspecting a number of code snippets so to give you some context here is the folder structure of the files we’ll be creating. You’ll notice that I’ve created a new folder for the component as per the structure I outlined in my post about structuring react apps for scale:

File structure for new component

Let’s begin with the src/scrollTemplateList/containers/ScrollTemplateList.jsx file. This is the definition for the container component for our list, it will house our state and farm this data off to our presentation components keeping in line with the container/presentation component pattern.

 

import React, { Component } from 'react';
import ScrollTemplateListItem from '../components/ScrollTemplateListItem';
require('../styles/scrollTemplateList.css');
class ScrollTemplateList extends Component {
constructor(props) {
super(props);
this.state = {
data: [
{
Your_Name: 'Zac Braddy',
Message_Body: ['I invite you to be a code-venturer'],
Their_Name: 'Other Guy',
},
],
};
this.handleAddClick = this.handleAddClick.bind(this);
this.handleRemoveClick = this.handleRemoveClick.bind(this);
this.handleToggleDetailClick = this.handleToggleDetailClick.bind(this);
this.createNewEntryForMulti = this.createNewEntryForMulti.bind(this);
this.removeEntryForMulti = this.removeEntryForMulti.bind(this);
this.changeSingleValue = this.changeSingleValue.bind(this);
this.changeMultiValue = this.changeMultiValue.bind(this);
}
handleAddClick() {
const data = this.state.data;
data.push(
{
Your_Name: `<<new${data.length}>>`,
Message_Body: [`<<new${data.length}>>`],
Their_Name: `<<new${data.length}>>`,
}
);
this.setState({
data,
});
}
handleRemoveClick(indexToDelete) {
const data = this.state.data;
data.splice(indexToDelete, 1);
let rowDisplayingAdvancedDetail = this.state.rowDisplayingAdvancedDetail;
if (rowDisplayingAdvancedDetail === indexToDelete
|| rowDisplayingAdvancedDetail > this.state.data.length - 1) {
rowDisplayingAdvancedDetail = -1;
}
this.setState({
data,
rowDisplayingAdvancedDetail,
});
}
handleToggleDetailClick(indexToToggle) {
if (indexToToggle !== this.state.rowDisplayingAdvancedDetail) {
this.setState({
rowDisplayingAdvancedDetail: indexToToggle,
});
} else {
this.setState({ rowDisplayingAdvancedDetail: -1 });
}
}
createNewEntryForMulti(index, fieldName) {
const newState = Object.assign({}, this.state);
newState.data[index][fieldName] = this.state.data[index][fieldName].concat('');
this.setState(newState);
}
removeEntryForMulti(index, fieldName, id) {
const newState = Object.assign({}, this.state);
newState.data[index][fieldName].splice(id, 1);
this.setState(newState);
}
changeSingleValue(index, fieldName, newValue) {
const newState = Object.assign({}, this.state);
newState.data[index][fieldName] = newValue;
this.setState(newState);
}
changeMultiValue(index, fieldId, newValue) {
const newState = Object.assign({}, this.state);
const fieldNameParts = fieldId.split('_');
let fieldName = [];
for (let i = 0; i < fieldNameParts.length - 1; i++) {
fieldName = fieldName.concat(fieldNameParts[i]);
}
fieldName = fieldName.join('_');
const subId = fieldNameParts[fieldNameParts.length - 1];
newState.data[index][fieldName] = this.state.data[index][fieldName];
newState.data[index][fieldName][subId] = newValue;
this.setState(newState);
}
render() {
return (
<div className="scroll-template-list-resting">
<div className="grid-header grid-row">
<div className="grid-cell">Their Name</div>
<div className="grid-cell">Your Name</div>
<div className="end-of-row-header-spacer" />
</div>
{
this.state.data.map((d, idx) =>
(
<ScrollTemplateListItem
key={idx}
formData={d}
index={idx}
rowDisplayingAdvancedDetail={this.state.rowDisplayingAdvancedDetail}
handleToggleDetailClick={this.handleToggleDetailClick}
removeItem={this.handleRemoveClick}
createNewItemForMessageBody={this.createNewEntryForMulti}
changeSingleValue={this.changeSingleValue}
changeMultiValue={this.changeMultiValue}
removeItemFromMessageBody={this.removeEntryForMulti}
/>
)
)
}
<button
className="scroll-template-list-add-button scroll-template-list-item
glyphicon
glyphicon-plus btn btn-success"
onClick={this.handleAddClick}
/>
</div>
);
}
}
export default ScrollTemplateList;

 

If you’ve been following along with the tutorial series you shouldn’t find anything exceptional in this component.

You can see that we have  number of handlers to modify the state of the list. These include a handler to add a new empty entry to the list and a handler to remove an item. Also included is a handler that toggles whether we are displaying the “extra detail” view of a row.

The next couple of handlers are used to modify the properties of each individual item in the list. You may notice that these item handlers are remarkably like handlers in the src/common/containters/ScrollCreator.jsx component, this duplication is one of the problems that the Flux architectural pattern solves. But we can talk about that in future tutorials.

Finally, you can see the render function. Within we are rendering a containing div which houses a header row followed by the state.data array mapped out to ScrollTemplateListItem components.

As you know each react component created by a map function like this needs a unique key prop. In previous tutorials I’ve acheived this by defining a variable which I’ve incremented with each iteration and used that as my key.

But recently I’ve been reading React: Up & Running (Link for UK Shoppers) so that I can do a review in the near future, spoiler alert, it’s going to be a positive one! The trick I’ve used here I was shown in that book. The trick being that instead of declaring a variable and incrementing I use the map function to provide the key for me.

It turns out that the callback function that you pass into Array.map() can have a second parameter which gets populated with the index that is currently being operated on. I thought that was awesome so I wanted to share that with you in case it could help you as well.

 

A list with no items, ain’t a list!

Whilst looking at the ScrollTemplateList we noticed that we were using a component called ScrollTemplateListItem to handle the presentation of the individual data items. We’ve defined that component in src/scrollTemplateList/components/ScrollTemplateListItem.jsx, let’s take a look at the implementation of that:

 

import React, { PropTypes } from 'react';
import ScrollCreatorForm from '../../scrollCreatorForm/components/ScrollCreatorForm';
require('../styles/scrollTemplateListItem.css');
const ScrollTemplateListItem = (props) => {
const {
index,
handleToggleDetailClick,
removeItem,
createNewItemForMessageBody,
removeItemFromMessageBody,
changeSingleValue,
changeMultiValue,
formData,
rowDisplayingAdvancedDetail,
} = props;
return (
<div className="scroll-template-list-item">
<div className="scroll-template-list-item-simple-detail">
<button
onClick={() => {
handleToggleDetailClick(index);
}}
>
<div className="scroll-template-list-item-content">
<div className="grid-row">
<div className="grid-cell">{formData.Their_Name}</div>
<div className="grid-cell">{formData.Your_Name}</div>
</div>
<div className="scroll-template-list-item-overlay-resting" />
</div>
</button>
<button
className="scroll-template-list-item-delete-button btn btn-danger"
onClick={() => {
removeItem(index);
}}
>
<i className="glyphicon glyphicon-remove" />
</button>
</div>
{
(index === rowDisplayingAdvancedDetail)
?
(<div className="scroll-template-list-item-advanced-detail">
<ScrollCreatorForm
addOnClickMulti={
(fieldName) => {
createNewItemForMessageBody(index, fieldName);
}
}
removeOnClickMulti={
(fieldName, id) => {
removeItemFromMessageBody(index, fieldName, id);
}
}
onChangeSingle={
(e) => {
changeSingleValue(index, e.target.id, e.target.value);
}
}
onChangeMulti={
(e) => {
changeMultiValue(index, e.target.id, e.target.value);
}
}
formData={formData}
/>
</div>)
: ''
}
</div>
);
};
ScrollTemplateListItem.propTypes = {
formData: PropTypes.object.isRequired,
handleToggleDetailClick: PropTypes.func.isRequired,
removeItem: PropTypes.func.isRequired,
createNewItemForMessageBody: PropTypes.func.isRequired,
removeItemFromMessageBody: PropTypes.func.isRequired,
changeSingleValue: PropTypes.func.isRequired,
changeMultiValue: PropTypes.func.isRequired,
index: PropTypes.number,
rowDisplayingAdvancedDetail: PropTypes.number,
};
export default ScrollTemplateListItem;

 

The render function begins with a containing div for the list item. You can see there are two buttons in this containing div.

The first button houses the simple data that is being displayed. This “simple view” is just the recipient and the sender of the scrolls. You may be asking, why does this row data need to be wrapped by a button? The answer is that we want to provide the ability for the user to click on a row and have that row expand to show it’s details.

We might have done this using a div with an OnClick handler but that isn’t very accessibility friendly. If you were to try this the react-a11y plugin built into the linting in our pipeline will promptly inform you if you try to do this.

After this first button there’s another and this is the delete button for the list item. These two buttons will be overlayed over each other with CSS so that the delete button sits at the end of it’s corresponding row to give an intuitive user experience.

What comes next is the creation of the advanced detail section. This only gets rendered if this is the row that the container components has told us needs to be expanded.

If we determine that we do want to render the advance detail we will render an instance of a ScrollCreatorForm and pass down, through props, the change handlers that were passed to us by the list container component. Once again if you’re interested in how the ScrollCreatorForm component works then you can checkout the tutorial in which we built that component.

 

What’s left? Mere polish and plumbing.

As it stands our ScrollCreatorForm has both the form as well as a print preview of the scroll we’re creating. The print preview is obviously still a useful component and we’ll probably reintroduce it later in the series but for the purposes of our list all it does is add unnecessary noise in our UI so let’s remove it from the for now.

Open up the src/scrollCreatorForm/components/ScrollCreatorForm.jsx component and then find and remove the code below:

<div className="scrollCreatorPanel">
<ScrollPrintPreview
formData={formData}
/>
</div>

This will also mean removing the import statement for the ScrollPrintPreview component from the top of the file to avoid getting linting errors.

Now that we’ve got our new list component and we’ve got it using our newly modified ScrollCreatorForm all that is left is to make our app use our new ScrollTemplateList component rather than the old ScrollCreator container component. I’ve highlighted the lines that need to change in the src/index.jsx file below:

/* globals document */
import React from 'react';
import ReactDOM from 'react-dom';
import ScrollTemplateList from './scrollTemplateList/containers/ScrollTemplateList';
ReactDOM.render(
<ScrollTemplateList fields={fields} />,
document.getElementById('the-kingdom')
);

Finally we’ve got just a bit of prettying up to do. As usual I’ll say; this isn’t a tutorial in css and I’m merely providing enough css for our examples to look nice. If you’re interested in the css and find anything difficult to understand then please feel free to leave a comment or contact me on social media and we can have a chat about it. Otherwise copy pasting these files in the necessary places will do.

 

src/scrollTemplateList/styles/scrollTemplatelist.css

.scroll-template-list-resting {
margin: 10%;
border: 0.0625em solid #ddd;
opacity: 1;
background-color: #fdfdfd;
border-radius: 0.3125em;
display: flex;
flex-direction: column;
padding: 1.25em;
}
.grid-row {
display: flex;
}
.grid-cell {
flex-grow: 1;
}
.grid-header {
margin: 0.625em 1.25em;
font-weight: 700;
}
.end-of-row-header-spacer {
width: 2.5em;
}
.btn.btn-success.scroll-template-list-add-button {
padding: 0.375em 42%;
margin: 0.3125em 1.25em;
}

 

src/scrollTemplateList/styles/scrollTemplatelistItem.css

.scroll-template-list-item {
margin: 0.3125em 1.25em;
border: 0.0625em solid #ddd;
background-color: #fafafa;
border-radius: 0.125em;
padding: 0.3125em;
position: relative;
display: flex;
flex-direction: column;
}
.scroll-template-list-item-simple-detail {
display: flex;
}
.scroll-template-list-item-simple-detail button:nth-child(1)
{
flex-grow: 1;
background-color: transparent;
border: none;
}
.scroll-template-list-item-simple-detail button:nth-child(1),
.scroll-template-list-item-simple-detail button:active,
.scroll-template-list-item-simple-detail button:focus
{
outline: none;
}
.scroll-template-list-item-advanced-detail {
margin: 0.625em;
padding: 0.3125em;
z-index: 200;
}
.scroll-template-list-item-content {
flex-grow: 1;
text-align: left;
}
.scroll-template-list-item-content .grid-cell
{
flex-basis: 50%;
}
.scroll-template-list-item-delete-button {
z-index: 100;
}
.scroll-template-list-item .scroll-template-list-item-overlay-resting {
position: absolute;
top: 0;
right: 100%;
bottom: 100%;
left: 0;
width: 25%;
margin-left: 32.5%;
height: 100%;
background-color: transparent;
opacity: 0.1;
transition: background-color 0.5s ease-in-out, width 0.5s ease-in-out, margin-left 0.5s ease-in-out;
}
.scroll-template-list-item:hover .scroll-template-list-item-overlay-resting {
position: absolute;
top: 0;
right: 100%;
bottom: 100%;
left: 0;
width: 100%;
height: 100%;
opacity: 0.1;
background-color: #aaa;
margin-left: 0;
transition: background-color 0.1s ease-in-out, width 0.5s ease-in-out, margin-left 0.5s ease-in-out;
}
.scroll-template-list-add-button-text {
text-align: center;
flex-grow: 1;
}

 

It’s…..alive!!!

With those last few css files in place the app should be ready to run. If you run an npm start command in the root of the application directory then webpack dev server should fire up and you’ll have the application in front of you.

What you will see is our list pre-populated with initial data. You can imagine this would be pre-populated data might be delivered via an ajax call in a production app.

Once everything is loaded you can add new rows with the big green plus button you can remove rows by clicking the red X buttons at the end of each row and if you want to expand a row you can click on it. If you want to minimize a row’s detail you can click on it again or click on another row.

With the row expanded you’ll see the form that we made in previous tutorials and this obviously works the way it did previously.

 

It’s so beautiful, what does it mean?!

Let’s take a moment to reflect on what this tutorial has shown us:

  1. React components are supremely re-usable provided you’ve followed the established best practices for writing them.
  2. By composing react components of react components which are themselves composed of even more react components (read it slowly) you might find that you’ve created a flow through the code that begins at the container component and flows down a deep well of components until the data reaches the presentation components at the bottom. This is somewhat of an anti pattern which we can address with the Flux architectural pattern which we will discuss in a future tutorial.
  3. This tutorial has also given us some cool ideas for how we might implement a UI that allows for data to be listed with both a simple and advanced detail view and with an editor for each row. This type of UI component is somewhat of a repeating pattern in enterprise applications so having a boiler plate solution to start with can prove quite useful.

 

Casting spells of increased Awesomeness

The component we’ve built here gives a pretty good basis to jump off from towards making a similar component to suit the needs of your own applications.

In my opinion, the component here is good for an example but make no mistake there’s plenty of work left to do. Let’s take a look at a few places you could start improving this:

  • First and foremost the css needs work to make the list responsive.
  • As mentioned above, the communication between components is beginning to get a little muddled. You might be able to justify implementing a flux pattern depending on how much you were planning on building off the back of this.
  • We could be smarter about the placeholders we use when we make a new row in the list.

The point I’m trying to impart here is that what you’ve been given is the beginning of development for this component not the end. But I do hope the concepts have been demonstrated well enough for you to make sensible decisions on how you can progress with this component to turn it into exactly what you want.

That’s it for this week’s tutorial. I hope you enjoyed following along. If you’d like to strike up a conversation about the concepts presented here then don’t hesitate to leave a comment below or catch me on any of the social media’s that you can find links for at the top of the page. Till next week code-venturers!