0

I'm trying to make an accordion app with react, i have data coming in from an API and i have the basic outline of the app but i'm not sure how to handle the click on the accordion.

This is my code:

This is where i set the data

App.js

class App extends Component {
  constructor(){
    super();
    this.state = {
      myData: {}
    };
  }

  componentDidMount() {
    axios.get(linkToApi)
      .then(responseData => {
        this.setState({ mydata: responseData.data });
      })
      .catch(error => {
        console.log("Porblem getting data", error);
      });
  }

  render() {
    return (
      <div className="App">
        <Accordion data={this.state.myData} />
      </div>
    );
  }
}

export default App;

Accordion.js

const Accordion = props => {
   let accordionElements = Object.keys(props.data).map(function(keyName, keyIndex) {
     return <AccordionElement
            {...props.data[keyName]}
            key={props.data[keyName].id}
          />;
 })


return (
   <ul className="accordion">
     {accordionElements}
   </ul>
 );
 }
 export default Accordion;

AccordionElement.js

const AccordionElement = props => {

 const handleOnClick = (e) => {
   e.preventDefault();
   //
 }

 return (
   <li style={listItemStyle} onClick={handleOnClick}>
     <h1 data={props}>{ props.name }</h1>
     <ul style={descriptionStyle}>
       <li>Description: { props.description }</li>
     </ul>
   </li>
 );

}

export default AccordionElement;

I want to be able to show or hide the description under it when the heading is clicked. I'm not really sure how i would go about this, any ideas?

ragebunny
  • 1,582
  • 10
  • 33
  • 53
  • Possible duplicate of [React JS onClick event handler](https://stackoverflow.com/questions/28511207/react-js-onclick-event-handler) – Seano666 Oct 26 '17 at 15:48

1 Answers1

1

I'd use state to manage if the description is visible at a given time. So something like this:

class AccordionElement extends React.Component {

  constructor(props) {
    super(props);
    this.state = {
      open: false
    }
  }

  const handleOnClick = (e) => {
    e.preventDefault();
    this.setState({ open: !this.state.open });
  }

  renderDescription() {
    const description = (
      <ul style={descriptionStyle}>
        <li>Description: { props.description }</li>
      </ul>
    );
    return this.state.open ? description : null;
}

  return (
    <li style={listItemStyle} onClick={handleOnClick}>
      <h1 data={props}>{ props.name }</h1>
      {this.renderDescription()} 
    </li>
  );

}

export default AccordionElement;

EDIT: I've got a few kinks ironed out; this should now work as expected, keeping Accordion and AccordionElement as functional components. Code below:

class App extends React.Component {
  constructor(){
    super();
    this.state = {
      myData: []
    };
    this.setOpenStatus = this.setOpenStatus.bind(this);
  }

  componentDidMount() {
    // this would be inside the API call
        const myData = [
        {
        id: 1,
        name: 'Item 1',
        description: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Ut varius metus tellus, eu tincidunt est viverra vitae. Fusce et mollis libero.'
      }, {
        id: 2,
        name: 'Item 2',
        description: 'Ut interdum ut justo ac euismod. Phasellus vitae pellentesque lectus, et cursus erat. Suspendisse eget risus gravida tellus rutrum gravida et vitae felis.'
      }, {
        id: 3,
        name: 'Item 2',
        description: ' Cras euismod massa eu mi consequat mollis. Sed aliquam tellus sed sem dictum feugiat. Nullam pretium purus sed ipsum pharetra luctus.'
      }
    ];
    myData.forEach(item => {
        item.open = false;
    });
    this.setState({ myData });
  }

  setOpenStatus(id) {
    console.log(this.state);
    const myData = this.state.myData;
    myData.forEach(item => {
        if (item.id == id) {
        item.open = !item.open;
      } else {
        item.open = false;
      }
    });
    this.setState({ myData });
  }

  render() {
    return (
      <div className="App">
        <Accordion data={this.state.myData} setOpenStatus={this.setOpenStatus} />
      </div>
    );
  }
}

const Accordion = props => {
   let accordionElements = [];
   const {data} = props;
   if (data && data.length > 0) {
     data.forEach(item => {
       console.log('props', item);
       accordionElements.push(<AccordionElement
              {...item}
              setOpenStatus={props.setOpenStatus}
              key={item.id}
            />);
     });
   }


return (
   <ul className="accordion">
     {accordionElements}
   </ul>
 );
 }

const AccordionElement = props => {

 const handleOnClick = (e) => {
   e.preventDefault();
     props.setOpenStatus(props.id);
 }

 const renderDescription = () => {
    return props.open ? props.description : null;
 }

 return (
   <li onClick={handleOnClick}>
     <h1>{ props.name }</h1>
     <span>{ renderDescription() }</span>
   </li>
 );

}

The setOpenStatus(id) function is passed through as props to the AccordionElement, which is then called on the click handler. This finds the ID in the myData state array and toggles the open boolean, and then changes the other open booleans to false, so only the one clicked on can be true. jsfiddle link.

Tom Oakley
  • 6,065
  • 11
  • 44
  • 73
  • This part of the app doesn't have access to the state, if there another way? Also, i've updated the question to show more code. – ragebunny Oct 26 '17 at 16:14
  • 1
    Yeah I realised after you can’t use state on a component like you declared. You could add an ‘open’ attribute on your each data object (after it’s loaded) and then pass down a function from App to AccordionElement which changes the open attribute for a given object? I’ve left work now but I can write up a solution when I’m back at my laptop (~1 hour). – Tom Oakley Oct 26 '17 at 16:19
  • 1
    You will have to manage state somewhere, so just use the answer above as an example to place it in `AccordionElement ` or `Accordion`. I'd say the best place to keep it in `Accordion` because if you'd like to keep only one open at the time you can easily close others. For that you'll have to make one of the two as React class instead of the pure function. You can manage it through via global states via redux and keep both components as is – Max Gram Oct 26 '17 at 16:23
  • @ragebunny tried to figure this out tonight while still using stateless components; it's pretty tough. I think for at least one of the components (probably `Acordion`, so you can do what Max suggested above), you should switch to a normal `React.Component` if possible, and go from there. Stateless components are great but not being able to set state or use component lifecycle methods is pretty limiting and in this case are not suitable (in my opinion). I may have another go at some point but no promises. – Tom Oakley Oct 26 '17 at 21:04
  • @ragebunny example updated with a (hopefully) working example. Let me know if it works. – Tom Oakley Oct 27 '17 at 21:04