I'm pretty new to both React and Javascript, and I've started learning it by building a simple collapsible menu. It works as expected, except for that I can't figure out how to only highlight one item at a time. I suspect that the onClick method and state has to be owned by a higher level class than the sectionItem, but I'm really stuck in how to make this work. My first instinct would then be to iterate over all items in the menu every time something is clicked, and make sure that all other items are switched to active=false.
Is this the right way to think about this? Could someone explain how states work in React in this context, and how I should implement it here?
The entire code is available here: Menu on codepen.io
This is the code for the item I want to make highlightable. I've not yet been able to implement that only one item should be highlighted at once.
var SectionItem = React.createClass({
handleClick: function(){
if(!this.state.active) {
this.setState({
currentItem: this,
active: true,
class: "sectionitem active"});
}
},
getInitialState: function(){
return {
active: false,
class: "sectionitem"
}
},
render: function() {
return (
<div className={this.state.class} onClick={this.handleClick}>{this.props.title}</div>
);
}
});
To highlight text using React, we can create our own component where we check whether each word matches what we want to highlight. to create the Highlighted component to highlight the text set as the highlight option.
Keep a new state such as "selectedButton" then set this state with clicked button's id. Now, Look for if a button's "id" is equal to the "selectedButton" state. If yes, highlight it with a conditional class.
Use the spread syntax (...) to pass an object as props to a React component, e.g. <Person {... obj} /> . The spread syntax will unpack all of the properties of the object and pass them as props to the specified component.
the ${} is the syntax for variables (or other code to be executed) inside template literals (`).
Great start! Let's walk through some problems in your code first.
There's no need to create your entire nested structure inside your top-most component. This is very contrived:
for (i=0; i < this.props.menuitems.length; i++) {
if(this.props.menuitems[i].section !== lastSection) {
var section = this.props.menuitems[i].section;
var items = [];
for (j=0; j < this.props.menuitems.length; j++) {
if(this.props.menuitems[j].section == section) {
var itemName = this.props.menuitems[j].name;
items.push(<SectionItem title={itemName} key={itemName} />);
};
}
sections.push(<Section title={section} items={items} key={section} />);
lastSection = section;
}
}
Quite on the contrary. You should try and make each component responsible for the rendering of their own piece of information. We could improve this if we first treated your data. The problem is that your sections are not nested. What if, instead of this...
var MENU_ITEMS = [
{section: "About", name: "Hey", key: "Hey", selected: true},
{section: "About", name: "No", key: "No", selected: false},
{section: "About", name: "Way", key: "Way", selected: false},
{section: "People", name: "Cakewalk", key: "Cakewalk", selected: false},
{section: "People", name: "George", key: "George", selected: false},
{section: "People", name: "Adam", key: "Adam", selected: false},
{section: "Projects", name: "Pirate raid", key: "Pirate raid", selected: false},
{section: "Projects", name: "Goosehunt", key: "Goosehunt", selected: false},
];
We had this:
var sections = [
{
name: "About",
items: [
{name: "Hey", key: "Hey", selected: true},
{name: "No", key: "No", selected: false},
{name: "Way", key: "Way", selected: false}
]
},{
name: "People",
items: [
{name: "Cakewalk", key: "Cakewalk", selected: false},
{name: "George", key: "George", selected: false},
{name: "Adam", key: "Adam", selected: false}
]
},{
name: "Projects",
items: [
{name: "Pirate raid", key: "Pirate raid", selected: false},
{name: "Goosehunt", key: "Goosehunt", selected: false}
]
}
];
Then we could simplify Accordion
quite a bunch. We simply render one Section
for each section
:
var Accordion = React.createClass({
render: function() {
return (
<div className="main">
{this.props.sections.map(function(section){
return <Section key={section.name} section={section}/>
})}
</div>
);
}
});
Likewise, Section and SectionItem become quite simpler.
var Section = React.createClass({
handleClick: function(){
this.setState({
open: !this.state.open,
class: this.state.open ? "section" : "section open"
});
},
getInitialState: function(){
return {
open: false,
class: "section"
}
},
render: function() {
return (
<div className={this.state.class}>
<div className="sectionhead" onClick={this.handleClick}>{this.props.section.name}</div>
<div className="articlewrap">
<div className="article">
{this.props.section.items.map(function(item){
return <SectionItem key={item.name} item={item}/>
})}
</div>
</div>
</div>
);
}
});
var SectionItem = React.createClass({
handleClick: function(){
this.setState({
currentItem: this,
active: !this.state.active,
class: this.state.active ? "sectionitem" : "sectionitem active"
});
},
getInitialState: function(){
return {
active: false,
class: "sectionitem"
}
},
render: function() {
return (
<div className={this.state.class} onClick={this.handleClick}>{this.props.item.name}</div>
);
}
});
Now, to your original question. In a more complex application, you could benefit from something more robust like Flux. However, for now, following the techniques exposed in Thinking in React should solve your problem.
Indeed, one good way is to bring your state of "what is open" to the Accordion
component. You simply need to let your parent know that something is being clicked. We can do that through a callback passed as a prop
.
So, Accordion
could have an openSection
state, and an onChildClick
that receives the clicked section's name. It needs to pass onChildClick
to each Section
.
var Accordion = React.createClass({
getInitialState: function() {
return {
openSection: null
};
},
onChildClick: function(sectionName) {
this.setState({
openSection: sectionName
});
},
render: function() {
return (
<div className="main">
{this.props.sections.map(function(section){
return <Section key={section.name}
onChildClick={this.onChildClick}
open={this.state.openSection===section.name}
section={section}/>
}.bind(this))}
</div>
);
}
});
And Section
simply calls this function when clicked, passing in it's own name.
var Section = React.createClass({
handleClick: function(){
this.props.onChildClick(this.props.section.name);
},
render: function() {
var className = this.props.open ? "section open" : "section"
return (
<div className={className}>
<div className="sectionhead" onClick={this.handleClick}>{this.props.section.name}</div>
<div className="articlewrap">
<div className="article">
{this.props.section.items.map(function(item){
return <SectionItem key={item.name} item={item}/>
})}
</div>
</div>
</div>
);
}
});
You can extrapolate this solution to the SectionItem
problem.
The resulting codepen is here: http://codepen.io/gadr90/pen/wamQXG?editors=001
Good luck on learning React! You are on the right path.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With