Zach Mason
Zach Mason

Reputation: 125

is there a simple way to create a nested dropdown in ReactJS without crazy amounts of state?

My code works but I feel like there's a way to do this without declaring a ton of state.

When the nav is clicked, it opens all SectionHeaders, and when one of those SectionHeaders is clicked, it opens the SubSections (only one SubSection allowed to be opened at once)

isFilterOpen

isFilterOpen

Open but subs closed

Open but subs closed

One sub open (only one at a time, they toggle)

One sub open

Right now, my code looks like this:

class MobileFilter extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      isFilterOpen: false,
      isSectionOpen: {
        Business: false,
        Resource: false,
        Need: false,
        Implementation: false,
        Type: false,
        Foundations: false,
        Advantage: false,
        Advanced: false,
        Catalyst: false,
        Team: false,
      },
    };
    this.filterBar = React.createRef();
  }

  handleFilterClick = () => {
    const {
      isFilterOpen
    } = this.state;
    this.setState({
      isFilterOpen: !isFilterOpen,
    });
  };

  handleSectionClick = title => {
    let selectedSection = title;
    if (title.split(' ').length > 1) {
      selectedSection = title.split(' ')[0]; // eslint-disable-line
    }

    this.setState(prevState => {
      const newState = {};
      Object.keys(prevState.isSectionOpen).forEach(key => {
        newState[key] = false;
      });
      newState[selectedSection] = !prevState.isSectionOpen[selectedSection];
      return {
        ...prevState,
        isSectionOpen: {
          ...newState,
        },
      };
    });
  };

 render() {
    const { isFilterOpen } = this.state;
    const {
      need = '',
      implementation = '',
      type = '',
      customerStoriesURL = '',
      vertical,
    } = this.props;
    const filterClasses = isFilterOpen
      ? 'showMobileSections'
      : 'hideMobileSections';
    const wrapperClass = isFilterOpen
      ? 'mobileFilterWrapperActive'
      : 'mobileFilterWrapper';
    const filterData = this.getData(vertical);

    if (vertical === 'services') {
      return (
        <div className="filterBarMobile" ref={this.filterBar}>
          <div className="mobileFilterWrapperContainer">
            <div className={wrapperClass}>
              <button
                type="button"
                onClick={this.handleFilterClick}
                className="filterHead"
              >
                Navigate Hub
              </button>
              <div className={filterClasses}>
                {this.renderSections('Foundations', filterData.Foundations)}
              </div>
              <div className={filterClasses}>
                {this.renderSections('Advantage', filterData.Advantage)}
              </div>
              <div className={filterClasses}>
                {this.renderSections('Advanced', filterData.Advanced)}
              </div>
              <div className={filterClasses}>
                {this.renderSections('Catalyst', filterData.Catalyst)}
              </div>
              <div className={filterClasses}>
                {this.renderSections(
                  'Team Edition',
                  filterData['Team Edition'],
                )}
              </div>
            </div>
          </div>
        </div>
      );
    }
    return (
      <div className="filterBarMobile" ref={this.filterBar}>
        <div className="mobileFilterWrapperContainer">
          <div className={wrapperClass}>
            <button
              type="button"
              onClick={this.handleFilterClick}
              className="filterHead"
            >
              Navigate Hub
            </button>
            <div className={filterClasses}>
              {this.renderSections(need, filterData.need)}
            </div>
            {implementation ? (
              <div className={filterClasses}>
                {this.renderSections(implementation, filterData.implementation)}
              </div>
            ) : null}
            <div className={filterClasses}>
              {this.renderSections(type, filterData.type)}
            </div>
            <div className={filterClasses}>
              <div className="sectionTab">
                <Link className="sectionLabel" to={customerStoriesURL}>
                  Customer Stories
                </Link>
              </div>
            </div>
          </div>
        </div>
      </div>
    );
  }
}
export default MobileFilter;

As you can see, there's way too much state going on -- there as to be a way to make this more founded on the data / props that are coming in and not in a way that requires me listing out all of the SubSections as a nested state.

Any ideas would help. Thanks!

Upvotes: 1

Views: 349

Answers (1)

Zach Mason
Zach Mason

Reputation: 125

i think i've found the solution. i needed to start from scratch. here's what i have:

import React, { Component } from 'react';
import { Link } from 'gatsby';
import Search from '../Search';
import { businessData } from './filterData';
import './newFilter.less';

class NewFilter extends Component {
  constructor(props) {
    super(props);

    this.state = {
      isOpen: false,
      openSubSection: '',
    };
  }

  handleClick = () => {
    const { isOpen } = this.state;
    if (!isOpen) {
      this.setState({
        openSubSection: '',
      });
    }
    this.setState({
      isOpen: !isOpen,
    });
  };

  handleSubClick = (e, title) => {
    const { openSubSection } = this.state;
    if (openSubSection === title) {
      this.setState({
        openSubSection: '',
      });
    } else {
      this.setState({
        openSubSection: title,
      });
    }
  };

  // renderLinks = sublevels => sublevels.map(({ title }) => <div>{title}</div>);
  renderLinks = sublevels =>
    sublevels.map(({ url_slug, title }) => {
      if (!url_slug) {
        return (
          <div className="sectionLabelSub" key={title}>
            {title}
          </div>
        );
      }
      return (
        <Link
          className="mobileSubLinks"
          key={url_slug}
          to={`/${url_slug}/`}
          style={{ display: 'block' }}
        >
          {title}
        </Link>
      );
    });

  renderSection = section => {
    const { isOpen, openSubSection } = this.state;
    const { title, sublevels } = section;

    let sectionClass = 'hideMobileSections';
    let sectionOpen = 'sectionTabClosed';
    let subSectionClass = 'hideMobileContent';
    let arrowClass = 'arrow arrow--active';

    if (isOpen) {
      sectionClass = 'showMobileSections';
    }

    if (openSubSection === title) {
      subSectionClass = 'showMobileContent';
      sectionOpen = 'sectionTabOpen';
      arrowClass = 'arrow';
    }
    // const sectionClass = isOpen ? 'section__open' : 'section__closed';
    return (
      <div className={sectionClass}>
        <button
          onClick={e => this.handleSubClick(e, title)}
          type="button"
          key={title}
          className={sectionOpen}
        >
          <button type="button" className="sectionLabel">
            {title}
          </button>
          <div className={arrowClass} />
        </button>
        <div className={subSectionClass} role="button" tabIndex="0">
          {this.renderLinks(sublevels)}
        </div>
      </div>
    );
  };

  renderSections = sections =>
    sections.map(section => this.renderSection(section));

  render() {
    const { isOpen } = this.state;
    const { navTitle, sections } = businessData;

    let wrapperClass = 'mobileFilterWrapper';

    if (isOpen) {
      wrapperClass = 'mobileFilterWrapperActive';
    }
    return (
      <div className="filterBarMobile" ref={this.filterBar}>
        <Search vertical='business' />
        <div className="mobileFilterWrapperContainer">
          <div className={wrapperClass}>
            <button
              onClick={() => this.handleClick()}
              type="button"
              className="filterHead"
            >
              {navTitle}
            </button>
            {this.renderSections(sections)}
          </div>
        </div>
      </div>
    );
  }
}

export default NewFilter;

basically i let the data inform the components, pass in the title to the button and the click event, and then the class looks to see if the title from the data matches the title (string) attached to the state

Upvotes: 1

Related Questions