单击其中的元素时,关闭下拉菜单

时间:2018-11-09 14:01:39

标签: javascript reactjs dropdown

我正在使用应用程序中的通知功能(非常类似于Facebook通知)。

当我单击标题导航中的按钮时,下拉列表将打开并显示通知列表。通知中包含Link(来自react-router)。

我需要做的就是每当点击Link时关闭下拉列表。

这里大致是我目前拥有的层次结构:

Header > Navigation > Button > Dropdown > List > Notification > Link

由于下拉功能的使用不止一次,因此我将其行为抽象为使用render prop的HOC:

export default function withDropDown(ClickableElement) {
  return class ClickableDropdown extends PureComponent {
    static propTypes = {
      children: PropTypes.func.isRequired,
      showOnInit: PropTypes.bool,
    };

    static defaultProps = {
      showOnInit: false,
    };

    state = {
      show: !!this.props.showOnInit,
    };

    domRef = createRef();

    componentDidMount() {
      document.addEventListener('mousedown', this.handleGlobalClick);
    }

    toggle = show => {
      this.setState({ show });
    };

    handleClick = () => this.toggle(true);

    handleGlobalClick = event => {
      if (this.domRef.current && !this.domRef.current.contains(event.target)) {
        this.toggle(false);
      }
    };

    render() {
      const { children, ...props } = this.props;
      return (
        <Fragment>
          <ClickableElement {...props} onClick={this.handleClick} />
          {this.state.show && children(this.domRef)}
        </Fragment>
      );
    }
  };
}

上面的HOC封闭了Button组件,所以我有:

const ButtonWithDropdown = withDropdown(Button);

class NotificationsHeaderDropdown extends PureComponent {
  static propTypes = {
    data: PropTypes.arrayOf(notification),
    load: PropTypes.func,
  };

  static defaultProps = {
    data: [],
    load: () => {},
  };

  componentDidMount() {
    this.props.load();
  }

  renderDropdown = ref => (
    <Dropdown ref={ref}>
      {data.length > 0 && <List items={this.props.data} />}
      {data.length === 0 && <EmptyList />}
    </Dropdown>
  );

  render() {
    return (
      <ButtonWithDropdown count={this.props.data.length}>
        {this.renderDropdown}
      </ButtonWithDropdown>
    );
  }
}

ListNotification都是愚蠢的功能组件,因此我不在这里发布它们的代码。 Dropdown几乎相同,只是使用的是ref forwarding

我真正需要的是调用HOC创建的.toggle()中的ClickableDropdown方法,每当我单击列表中的Link时都要调用该方法。

是否有任何方法可以将.toggle()方法传递到Button > Dropdown > List > Notification > Link子树下?

我正在使用redux,但是我不确定这是我要放在商店里的东西。

还是我应该通过从handleGlobalClick更改ClickableDropdown的实现来使用DOM API来强制性地处理此问题?


编辑:

我正在尝试命令式方法,因此我更改了handleGlobalClick方法:

const DISMISS_KEY = 'dropdown';

function contains(current, element) {
  if (!current) {
    return false;
  }

  return current.contains(element);
}

function isDismisser(dismissKey, current, element) {
  if (!element || !contains(current, element)) {
    return false;
  }

  const shouldDismiss = element.dataset.dismiss === dismissKey;

  return shouldDismiss || isDismisser(dismissKey, current, element.parentNode);
}

// Then...
handleGlobalClick = event => {
  const containsEventTarget = contains(this.domRef.current, event.target);
  const shouldDismiss = isDismisser(
    DISMISS_KEY,
    this.domRef.current,
    event.target
  );

  if (!containsEventTarget || shouldDismiss) {
    this.toggle(false);
  }

  return true;
};

然后,我将Link更改为包含data-dismiss属性:

<Link
  to={url}
  data-dismiss="dropdown"
>
  ...
</Link>

现在该下拉列表已关闭,但我不再重定向到所提供的url

我尝试使用this.toggle(false)requestAnimationFrame来推迟执行setTimeout,但是它也不起作用。


解决方案:

根据@streletss bellow的回答,我提出了以下解决方案:

为了尽可能通用,我在shouldHideOnUpdate下拉组件中创建了一个ClickableDropdown道具,其Hindley-Milner-ish签名是:

shouldHideOnUpdate :: Props curr, Props prev => (curr, prev) -> Boolean

这是componentDidUpdate的实现:

componentDidUpdate(prevProps) {
  if (this.props.shouldHideOnUpdate(this.props, prevProps)) {
    this.toggle(false);
  }
}

这样,我不需要直接在withRouter HOC中使用withDropdown HOC。

因此,我取消了为隐藏向调用者隐藏下拉菜单的条件的职责,我的案例是Navigation组件,在该组件中,我做了这样的事情:

const container = compose(withRouter, withDropdown);
const ButtonWithDropdown = container(Button);


function routeStateHasChanged(currentProps, prevProps) {
  return currentProps.location.state !== prevProps.location.state;
}

// ... then

  render() {
    <ButtonWithDropdown shouldHideOnUpdate={routeStateHasChanged}>
      {this.renderDropdown}
    </ButtonWithDropdown>
  }

2 个答案:

答案 0 :(得分:1)

有没有办法在不通过按钮>下拉菜单>列表>通知>链接子树的下面传递.toggle()方法的方法?

在问题中,您提到您正在使用redux。因此,我假设您将showOnInit存储在redux中。我们通常不将函数存储在redux中。在toggle函数中,我认为您应该调度CHANGE_SHOW操作来进行更改在redux中执行showOnInit,然后将show数据而不是函数传递给子组件。然后,在reducer调度后,react将自动更改“ show”。

switch (action.type) {
    case CHANGE_SHOW:
      return Object.assign({}, state, {
        showOnInit: action.text
      })
    ...
    default:
      return state
  }

链接元素和数据传递

使用“链接到”属性,而不是“数据” ...中的属性。

 <Link
  to={{
     pathname: url,
     state:{dismiss:"dropdown"}
     }}
/>

状态属性将在this.props.location中找到。

稍微尝试一下(不推荐)

这可能会导致您的项目陷入不稳定以及其他一些问题。(https://reactjs.org/docs/context.html#classcontexttype

首先,定义上下文

const MyContext = React.createContext(defaultValue);

第二次定义通过值

<MyContext.Provider value={this.toggle}>

然后,在嵌套组件中获取值

<div value={this.context} />

答案 1 :(得分:1)

似乎您可以简单地利用withRouter HOC并检查this.props.location.pathnamecomponentDidUpdate是否已更改:

export default function withDropDown(ClickableElement) {
  class ClickableDropdown extends Component {
    // ...

    componentDidUpdate(prevProps) {
      if (this.props.location.pathname !== prevProps.location.pathname) {
        this.toggle(false);
      }
    }

    // ...
  };

  return withRouter(ClickableDropdown)
}