DEV Community

Cover image for Capturing a click outside a Reason-React Component
Sebastian Kurpiel
Sebastian Kurpiel

Posted on • Edited on

Capturing a click outside a Reason-React Component

I found myself the other day trying to build a dropdown component in Reason that on outside click would close the menu if opened.
Alt Text

The only example I could find of this was in Reason from GlennSL and it was a bit outdated since it was written in 2018, before hooks were fully implemented into reason. The code below is an updated version using hooks, it requires the bs-dependency of bs-webapi so make sure to add that before you use it.

[@genType "OnClickOutside"]
[@react.component]
let make = (~onOutsideClick, ~children) => {
  let outsideContainer = React.useRef(Js.Nullable.null);
  React.useEffect0(() => {
    open Webapi.Dom;
    let onClick = e => {
      let target = MouseEvent.target(e);
      let outsideDiv =
        Belt.Option.getExn(
          Js.Nullable.toOption(React.Ref.current(outsideContainer)),
        );
      let targetElement = EventTarget.unsafeAsElement(target);
      if (!Element.contains(targetElement, outsideDiv)) {
        onOutsideClick();
      };
    };
    Document.addClickEventListener(onClick, document);
    Some(() => Document.removeClickEventListener(onClick, document));
  });
  <div ref={outsideContainer->ReactDOMRe.Ref.domRef}> children </div>;
};
Enter fullscreen mode Exit fullscreen mode

The logic breakdown:

  • wrap the component in a div with a ref
  • add an event listener for onClick
  • in that onClick function check whether the element clicked contains the div with the ref, if it does then the click is inside the div. If not then the click is outside of the div.
  • If the click is outside of the container then do whatever you need it to(in this case above, close the menu)

In the use case above we wrap our select component in the OnClickOutside and onOutsideClick close the dropdown menu container like so;

  let (visible, setVisibility) = React.useState(() => false);
  <OnClickOutside onOutsideClick={_e => setVisibility(_ => false)}>
      <Select.Button
          toggled=visible onClick={_e => setVisibility(_ => !visible)} /> 
      <Select.List name toggled=visible />
  </OnClickOutside>
Enter fullscreen mode Exit fullscreen mode

If you enjoy writing Reason and would like to write more of it, you're in luck. Draftbit is hiring, we're a no-code tool that let's users build cross-platform apps and we're built mostly in reason! Check us out!

UPDATE:

I was experiencing some state issues with the trigger so I added an extra useEffect to fix this.


[@genType "OverlayTrigger"]
[@react.component]
let make = (~onClick, ~children) => {
  let outsideContainer = React.useRef(Js.Nullable.null);
  open Webapi.Dom;
  let onClickHandler = event => {
    let target = MouseEvent.target(event);
    let outsideDiv =
      Belt.Option.getExn(
        Js.Nullable.toOption(React.Ref.current(outsideContainer)),
      );
    let targetElement = EventTarget.unsafeAsElement(target);
    if (!Element.contains(targetElement, outsideDiv)) {
      onClick();
    };
  };

  React.useEffect2(
    () => {
      Document.addMouseDownEventListener(onClickHandler, document);
      Some(
        () => Document.removeMouseDownEventListener(onClickHandler, document),
      );
    },
    (onClick, React.Ref.current(outsideContainer)),
  );

  <div
    className="cursor-pointer w-full"
    ref={outsideContainer->ReactDOMRe.Ref.domRef}>
    children
  </div>;
};
Enter fullscreen mode Exit fullscreen mode

Top comments (0)