0
votes

I am making a Accordion and when we click each individual item then its opening or closing well.

Now I have implemented expand all or collapse all option to that to make all the accordions expand/collapse.

Accordion.js

  const accordionArray = [
    { heading: "Heading 1", text: "Text for Heading 1" },
    { heading: "Heading 2", text: "Text for Heading 2" },
    { heading: "Heading 3", text: "Text for Heading 3" }
  ];
  .

  .

  .
{accordionArray.map((item, index) => (
        <div key={index}>
          <Accordion>
            <Heading>
              <div className="heading-box">
                <h1 className="heading">{item.heading}</h1>
              </div>
            </Heading>
            <Text expandAll={expandAll}>
              <p className="text">{item.text}</p>
            </Text>
          </Accordion>
        </div>
      ))}

And text.js is a file where I am making the action to open any particular content of the accordion and the code as follows,

import React from "react";

class Text extends React.Component {
  render() {
    return (
      <div style={{ ...this.props.style }}>
        {this.props.expandAll ? (
          <div className={`content open`}>
            {this.props.render && this.props.render(this.props.text)}
          </div>
        ) : (
          <div className={`content ${this.props.text ? "open" : ""}`}>
            {this.props.text ? this.props.children : ""}
            {this.props.text
              ? this.props.render && this.props.render(this.props.text)
              : ""}
          </div>
        )}
      </div>
    );
  }
}

export default Text;

Here via this.props.expandAll I am getting the value whether the expandAll is true or false. If it is true then all accordion will get the class className={`content open`} so all will gets opened.

Problem:

The open class is applied but the inside text content is not rendered.

So this line doesn't work,

{this.props.render && this.props.render(this.props.text)}

Requirement:

If expand all/collapse all button is clicked then all the accordions should gets opened/closed respectively.

This should work irrespective of previously opened/closed accordion.. So if Expand all then it should open all the accordion or else needs to close all accordion even though it was opened/closed previously.

Links:

This is the link of the file https://codesandbox.io/s/react-accordion-forked-sm5fw?file=/src/GetAccordion.js where the props are actually gets passed down.

Edit React-accordion (forked)

Edit:

If I use {this.props.children} then every accordion gets opened.. No issues.

But if I open any accordion manually on click over particular item then If i click expand all then its expanded(expected) but If I click back Collapse all option then not all the accordions are closed.. The ones which we opened previously are still in open state.. But expected behavior here is that everything should gets closed.

3

3 Answers

0
votes

In your file text.js

at line number 9. please replace the previous code by: {this.props.children}

Tried in the sandbox and worked for me.

/// cant add a comment so editing the answer itself. Accordian.js contains your hook expandAll and the heading boolean is already happening GetAccordian.js. I suggest moving the expand all to GetAccordian.js so that you can control both values.

0
votes

in this case this.props.render is not a function and this.props.text is undefined, try replacing this line

  <div className={`content open`}>
    {this.props.render && this.props.render(this.props.text)}
  </div>

by this:

  <div className={`content open`}>
   {this.props.children}
  </div>

EDIT: // Other solution is to pass the expandAll property to the Accordion component

  <Accordion expandAll={expandAll}>
    <Heading>
      <div className="heading-box">
        <h1 className="heading">{item.heading}</h1>
      </div>
    </Heading>
    <Text>
      <p className="text">{item.text}</p>
    </Text>
  </Accordion>

then in getAccordion.js

onShow = (i) => {
      this.setState({
        active: this.props.expandAll ? -1: i,
        reserve: this.props.expandAll ? -1: i
      });

      if (this.state.reserve === i) {
        this.setState({
          active: -1,
          reserve: -1
        });
      }
    };

    render() {
      const children = React.Children.map(this.props.children, (child, i) => {
        return React.cloneElement(child, {
          heading: this.props.expandAll || this.state.active === i,
          text: this.props.expandAll || this.state.active + stage === i,
          onShow: () => this.onShow(i)
        });
      });
      return <div className="accordion">{children}</div>;
    }
  };
0
votes

Building off of @lissettdm answer, it's not clear to me why getAccordion and accordion are two separate entities. You might have a very valid reason for the separation, but the fact that the two components' states are interdependent hints that they might be better implemented as one component.

Accordion now controls the state of it's children directly, as before, but without using getAccordion. Toggling expandAll now resets the states of the individual items as well.

const NormalAccordion = () => {
  const accordionArray = [ //... your data ];

  const [state, setState] = useState({
    expandAll: false,
    ...accordionArray.map(item => false),
  });

  const handleExpandAll = () => {
    setState((prevState) => ({
      expandAll: !prevState.expandAll,
      ...accordionArray.map(item => !prevState.expandAll),
    }));
  };

  const handleTextExpand = (id) => {
    setState((prevState) => ({
      ...prevState,
      [id]: !prevState[id]
    }));
  };

  return (
    <>
      <div className="w-full text-right">
        <button onClick={handleExpandAll}>
          {state.expandAll ? `Collapse All` : `Expand All`}
        </button>
      </div>
      <br />
      {accordionArray.map((item, index) => (
        <div key={index}>
          <div className="accordion">
            <Heading handleTextExpand={handleTextExpand} id={index}>
              <div className="heading-box">
                <h1 className="heading">{item.heading}</h1>
              </div>
            </Heading>
            <Text shouldExpand={state[index]}>
              <p className="text">{item.text}</p>
            </Text>
          </div>
        </div>
      ))}
    </>
  );
};

Heading passes back the index so the parent component knows which item to turn off.

class Heading extends React.Component {
  handleExpand = () => {
    this.props.handleTextExpand(this.props.id);
  };


  render() {
    return (
      <div
        style={ //... your styles}
        onClick={this.handleExpand}
      >
        {this.props.children}
      </div>
    );
  }
}

Text only cares about one prop to determine if it should display the expand content.

class Text extends React.Component {
  render() {
    return (
      <div style={{ ...this.props.style }}>
        <div
          className={`content ${this.props.shouldExpand ? "open" : ""}`}
        >
          {this.props.shouldExpand ? this.props.children : ""}
        </div>
      </div>
    );
  }
}