0

I have two dropdowns being passed in as props to components. I can control both with seperate states but I think this can all be done with one state?

Header

import React from 'react';
import DarkLabel from './DarkLabel';
import HeaderDropdown from './HeaderDropdown';

export default class Header extends React.Component {

    componentWillMount() {
        this.setState({
            listOpen: false
        })
    }

    render() {
        ...
        return (
            <div className="row header">
                <div className="col-xs-10">
                    <DarkLabel classExtra="dark-label-lg" icon="/images/system-icons/document_empty.png"
                               content={taskCode}/>
                    <DarkLabel classExtra="dark-label-2x dark-label-lg" icon="/images/system-icons/building.png"
                               dropdown=<HeaderDropdown data={this.props.enquiry.entity ? this.props.enquiry.entity : null}/>
                               content={this.props.enquiry.entity ? this.props.enquiry.entity.name : 'ERROR'}
                               right_icon="/images/system-icons/bullet_arrow_down.png"/>
                    <DarkLabel classExtra="dark-label-md" icon="/images/system-icons/ceo.png"
                               dropdown=<HeaderDropdown data={this.props.enquiry.contact ? this.props.enquiry.contact : null}/>
                               content={this.props.enquiry.contact ? this.props.enquiry.contact.firstName + ' ' + this.props.enquiry.contact.lastName : '-'}
                               right_icon="/images/system-icons/bullet_arrow_down.png"/>
                    <DarkLabel classExtra="flag"
                               content={'/images/flags/large/'+this.props.enquiry.entity.country.countryCode+'.png'}
                               right_icon="/images/system-icons/cog.png" right_icon_callback={this.handleAddressModal.bind(this)}/>
                </div>
            </div>
        )
    }

}

HeaderDropdown

import React from 'react';

export default class HeaderDropdown extends React.Component {

    componentWillMount() {

    }

    render() {

        return (
            <div>
                <div className="dark-label-dropdown">
                    Test
                </div>
            </div>
        );
    }
}

How can I make it so only one can be open at a time and close all others if a different one is clicked? Do I need to store something from 'this' when I bind a click event to HeaderDropdown?


I have missed something from the question. This needs to happen when the user clicks on right_icon in the DarkLabel.

DarkLabel

import React from 'react';

export default class DarkLabel extends React.Component {

    componentWillMount() {

    }

    truncate(limit) {
...
    }

    render() {
        var icon = '', content = '';
        var rightIcon = '';
        ...

        return (
            <div className={'pull-left dark-label-wrapper '+this.props.classExtra}>
                {icon}
                <div>{content}</div>
                {rightIcon}
                {this.props.dropdown}
            </div>
        );
    }
}
imperium2335
  • 23,402
  • 38
  • 111
  • 190

2 Answers2

2

What you could do is add a onClick method to your HeaderDropdown and in your Header handle the state for that. The proper way would be using a store of some kind (redux) but that might be out of scope for this example.

So in short I suggest you alter you Header to this:

import React from 'react';
import DarkLabel from './DarkLabel';
import HeaderDropdown from './HeaderDropdown';

export default class Header extends React.Component {

    componentWillMount() {
        this.setState({
            listOpen: false,
            activeDropdown: 1
        })
    }

    render() {
        const headerDropdownA = (
            <HeaderDropdown 
                data={...} 
                onClick={() => this.setState({activeDropDown: 1})} 
                        isActive={this.state.activeDropdown === 1}
                />
        )

        const headerDropdownA = (
            <HeaderDropdown 
                data={...} 
                onClick={() => this.setState({activeDropDown: 2})} 
                        isActive={this.state.activeDropdown === 2}
                />       
        )
        return (
            <div className="row header">
                <div className="col-xs-10">
                   <DarkLabel  classExtra="..."
                                         dropdown={headerDropdownA}
                               content={...}
                               right_icon="/images/system-icons/bullet_arrow_down.png"/>
                    <DarkLabel classExtra="..."
                               dropdown={headerDropdownB}
                               content={...}
                               right_icon="/images/system-icons/bullet_arrow_down.png"/>
                </div>
            </div>
        )
    }
}

I added a state for the current active dropdown. In your dropdown you can then access the active state with this.props.isActive.

But propably this will not already be sufficient since every click will toggle the dropdown again, also when you click the options? But it might give you a good starting point.

0

You can create a high-order-component that will detect clicks outside of your component e.g.

import React, { Component, PropTypes } from 'react';
import ReactDOM from 'react-dom';

const clickOutsideEvents = [ 'mousedown', 'touchstart' ];
const isDescendant = ( el, target ) => target !== null ? el === target || isDescendant( el, target.parentNode ) : false;

export default class ClickOutside extends Component {

      static propTypes =
      {
            children       : PropTypes.node,
            onClickOutside : PropTypes.any,
      };

      componentDidMount()
      {
            if ( !this.props.onClickOutside ) return;
            clickOutsideEvents.forEach( e => document.addEventListener( e, this.handleClickOutside ) )
      }

      /**
       * Remove the listener in case the props change and there is not ClickAway handler
       * @param  { Object } prevProps
       */
      componentDidUpdate( prevProps )
      {
            if ( prevProps.onClickOutside !== this.props.onClickOutside )
            {
                  clickOutsideEvents.forEach( e => document.removeEventListener( e, this.handleClickOutside ) );

                  if ( this.props.onClickOutside )
                  {
                      clickOutsideEvents.forEach( e => document.addEventListener( e, this.handleClickOutside ) )
                  }
            }
      }

      /**
       * Remove listeners when Component unmount
       */
      componentWillUnmount()
      {
           clickOutsideEvents.forEach( e => document.removeEventListener( e, this.handleClickOutside ) );
      }

      /**
       * Call callback on ClickAway and pass the event
       * @param  event
       */
      handleClickOutside = ( e ) =>
      {
            const el = ReactDOM.findDOMNode( this );

            if ( document.documentElement.contains( e.target ) && !isDescendant( el, e.target ) )
            {
                this.props.onClickOutside( e );
            }
      };

      /**
       * Render the Elements that are Wrapped by the ClickAway
       */
      render()
      {
           return this.props.children;
      }
}

Then wrap your dropdowns with this HOC and set the state of the component based on that

e.g.

setDropdownState(){
   this.setState({ listOpen: false });
}


render(){ 
  return(<ClickOutside onClickOutside={ this.setDropdownState.bind( this ) }>
     <HeaderDropdown listOpen={ this.state.listOpen }>
  </ClickOutside>)
}

You can check this implementation https://github.com/AvraamMavridis/react-clickoutside-component

Avraam Mavridis
  • 8,698
  • 19
  • 79
  • 133