Qt's Home

The Story of a french guy discovering the world

ReactJS - How to Make a Collapsible Content Panel

| Comments

Before we start

This is a small and easy guide about something you can do easily with React and CSS3.

First, a few things you need to know about before we start :

How-to

What we are building is something like this JQuery UI plugin.
We have content, and titles. When the user clicks on a title, it opens the accordion below while closing the others.

React modelisation

Let’s start with the simple HTML content that we want to have (For React very beginners) :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<div class="container">
  <div class="section-1">
      <h2 class="section-title">Section 1</h2>
      <p class="section-content">Our content for the section 1</p>
  </div>

  <div class="section-2">
      <h2 class="section-title">Section 2</h2>
      <p class="section-content">Our content for the section 2</p>
  </div>

  <div class="section-3">
      <h2 class="section-title">Section 3</h2>
      <p class="section-content">Our content for the section 3</p>
  </div>
</div>

This is not a file we are making, but this is what we will try to model with React.

So, first things first, let’s put the content in a simple json object (there are many ways to get data to build react components but we’ll talk about that in another post).

backendData.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
var backendData = [
  {
    "title":"Section 1",
    "content":"Our content for the section 1"
  },
  {
    "title":"Section 2",
    "content":"Our content for the section 2"
  },
  {
    "title":"Section 3",
    "content":"Our content for the section 3"
  }
]

Then, we build our components for the ground up. First, the section component we’ll instantiate:

section.jsx

1
2
3
4
5
6
7
8
9
10
11
12
13
14
/**
* @jsx React.DOM
*/
/* section.jsx */
Section = React.createClass({
  render: function() {
    return (
      <div className={"section" + this.props.key}>
        <h2 className="sectionTitle">{this.props.data.title}</h2>
        <p className="sectionContent">{this.props.data.content}</p>
      </div>
    );
  }
})

(I decided to put the data given by my “back-end” in a single JS object. Single responsibility, DRY code, let’s say I have reasons)

Then, we make the container component, that creates nested sections with our data:

container.jsx

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
/**
* @jsx React.DOM
*/
Container = React.createClass({
  buildSections: function(sectionList){
    var sections = sectionList.map(function(section, index){
      /* Remember to add a 'key'. React wants you to add an identifier when you instantiate a component multiple times */
      return <Section key={index} data={section} />
    })
    return sections;
  },
  render: function() {
    var sections = this.buildSections(this.props.data);
    return (
      <div className="container">
        {sections}
      </div>
    );
  }
})

So far so good, we get pretty much exactly the html code above if we instantiate our page with something like this :

1
2
3
4
React.renderComponent(
  <Container data={backendData} />,
  document.getElementsByTagName('body')[0]
);

Now on to setting up our closed/open states

Closed/Open states

First, let’s hide the sectionContents with some simple CSS.

1
2
3
.sectionContent {
  overflow: hidden
}

The only thing to note here is that we will be using heights, from 0 to … what we need. So, to do so, we need to make it so that the overflow is hidden instead of toggling display:noneand display:block

Now, let’s trigger these changes in our components :

section.jsx

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
/**
* @jsx React.DOM
*/
Section = React.createClass({
  getInitialState: function(){
    return {
      open: true
    }
  },
  toggleContent: function(){
    this.setState({
      open: !(this.state.open)
    })
  },
  getContentToggleHeight: function(){
    if(this.state.open){
      return "3em"
    } else {
      return "0"
    }
  },
  render: function() {
    var contentStyle = { height: getContentToggleHeight() };
    return (
      <div className={"section section" + this.props.key}>
        <h2 className="sectionTitle" onClick={this.toggleContent} >{this.props.data.title}</h2>
        <p className="sectionContent">{this.props.data.content}</p>
      </div>
    );
  }
})

In here, assign a height based on the state of our component to our sectionContent. I chose 3em pretty randomly because I like fixed things, you can decide to calculate the height you need instead. The inherit and auto don’t work well with transitions, but they will work if you don’t need them. This state is changed when you click on the title. It triggers the toggleContent function that inverts the closed state.

So, now we have a closed and a open state. We toggle between them based on the state of our Section component, defined by how many times we clicked on that title.

If you are looking for a way to make tabs open all at once, stop here. If you’re looking for that JQueryUI style One tab only thing, keep on to the next part

Closing all of them when one is open

Single responsibility principles tells us our Sections don’t know about the other sections. That means our section cannot tell the others to close. The container, however, knows about all of the sections…

container.jsx

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
/**
* @jsx React.DOM
*/
Container = React.createClass({
  getInitialState: function(sectionList){
    return { openSectionIndex: -1 }
  },
  buildSections: function(sectionList){
    var sections = sectionList.map(this.buildSection)
    return sections;
  },
  buildSection: function(section, index){
      var openStatus = (index === this.state.openSectionIndex);
      /* Remember to add a 'key'. React wants you to add an identifier when you instantiate a component multiple times */
      return <Section key={index} data={section} toggleOne={this.toggleOne} open={openStatus} />
  },
  toggleOne: function(id){
    if(this.state.openSectionIndex === id){
      this.setState({openSectionIndex: -1});
    } else {
      this.setState({openSectionIndex: id});
    }
  },
  render: function() {
    var sections = this.buildSections(this.props.data);
    return (
      <div className="container">
        {sections}
      </div>
    );
  }
})

Now, we actually play with the state of the parent component. It keeps track of the id of the tab that is open, and starts with -1.

We pass the toggleOneSection callback to the child component (the section), it takes an id (the id of the thing we want to toggle), and it sets the openSectionIndex of the state accordingly to our argument, or back to nothing (-1).

We then build sections with an “open” prop, a boolean that will be the thing that is used to get the class of our sectionContent, instead of the Section’s state. React building the page on the go, from the javascript, in the way of a simple diff, the children will just be changed instead of generated again, which will trigger whatever we have on them (listeners, animations, etc…).

Now to the section :

section.jsx

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
/**
* @jsx React.DOM
*/
Section = React.createClass({
  toggleContent: function(){
    this.props.toggleOne(this.props.key)
  },
  getHeight: function(){
    if(this.props.open){
      return "3em"
    } else {
      return "0"
    }
  },
  render: function() {
    var style = { height: this.getHeight() }
    return (
      <div className={"section section" + this.props.key}>
        <h2 className="sectionTitle" onClick={this.toggleContent} >{this.props.data.title}</h2>
        <p className="sectionContent" style={style} >{this.props.data.content}</p>
      </div>
    );
  }
})

As I said above, getContentToggleClass now uses the props instead of the state, and I got rid of the other state functions. The toggleContent internal callback now call the callback we called in the props, with that key that we gave it at the very beginning.

Now, up to animations

Animations

This is the very easy part. To trigger the animations, we just put a “transition” on the container selector, in the css.

style.css

1
2
3
4
5
6
7
8
.sectionContent {
  overflow: hidden;
  -webkit-transition: height 0.3s ease-in;
  -moz-transition: height 0.3s ease-in;
  -o-transition: height 0.3s ease-in;
  -ms-transition: height 0.3s ease-in;
  transition: height 0.3s ease-in;
}

This tells the interpreter that when the height changes, it has to take 0.3s to transition into the new height, with ease-in giving it a slow start for a cleaner interface (just a matter of preferences on this one).

And we now have our accordion menu.

Conclusion

You now have a clean accordion menu. I tend to add a few styling thing that I consider the bare minimum on that like :

1
2
3
4
.sectionTitle {
  display: block;
  cursor: pointer;
}

But that’s just me. Also, you don’t need to use H2s, you can use whatever you want, links work too but remember to preventDefault if you use them.

Let me know what you thought about that guide, I may have missed things here and there, and thank you for reading!

Last minute edit :

You can check the code on Github. Download it, run it, check it out!

Comments