15

I am looking to create a custom lookup component using Lightning Web Components. I want to be able to detect when the user is clicking outside of the element so that I can close the dialog and/or clear the text if nothing is selected. I can't seem to use standard Javascript since template.contains(e.target) and DOM traversal doesn't seem to detect whether the clicked element is inside of the component or not. Here is a sample of what I have attempted.

  1. DOM traversal
let parent = this.template.querySelector('.slds-form-element__row');//parent element
let targetElement = e.target; // clicked element

do {
  if (targetElement == parent) {
    window.console.log('clicked inside');
    return;
  }
  // Go up the DOM
  targetElement = targetElement.parentNode;
} while (targetElement);

window.console.log('clicked outside');
  1. template.contains(e.target)
if (this.template.querySelector('.slds-form-element__row').contains(e.target)) {
  window.console.log('clicked inside');
} else {
  window.console.log('clicked outside');
}

In both of these cases, clicked outside is the only thing I will see since e.target doesn't seem to exist within the DOM element, but I do know my click event (which I've attached to document) is firing since I do see the console.log statement in my developer console.

3 Answers 3

15

You can't navigate outside your own template. That's one of the security features of LWC's shadow DOM. Instead, create a transparent, fixed div that covers the entire screen area, then react to that, instead:

<template>
  <div class="backdrop" onclick={closeModal}></div>
  <div class="content">... your content here ...</div>
</template>

.backdrop {
  position: fixed;
  z-index: 999;
  top: 0;
  left: 0;
  right: 0;
  bottom: 0;
}
.content {
  z-index: 1000;
  /* anything else you need here */
}

For a complete example, look at Lightning Strike's Lookup component (written in Aura, but still applicable).

10
  • Instead of doing that he should detect a click outside. He can create a reusable module to do something like suggested here: stackoverflow.com/a/3028037/627569 Mar 28, 2019 at 15:31
  • @citizenconn That's essentially what they're trying to do, but you can't access the DOM outside your component in LWC. This is because of a feature known as the Shadow DOM. Of course, you could encapsulate the backdrop as a custom component of itself, but that would only save a couple of lines of code.
    – sfdcfox
    Mar 28, 2019 at 15:37
  • To clarify, this would definitely work, but I think looking back at my question it wasn't entirely clear. I am not trying to access anything outside of my component. .slds-form-element__row exists within my template, but I am thinking that maybe the locker service wrapper around elements is what is causing my comparison checks to fail, since it is a proxy instead of the element itself. I will review the Strike component and hopefully that will give me some ideas. Thanks! Mar 28, 2019 at 17:40
  • That Strike component gave me a great idea. I left everything the way I had it, but added event.stopPropagation() to all of my click events within my component to prevent the click from bubbling up and left the window.addEventListener() really simple to just close the popup if a click event ever gets that far, which solved my issues with comparison checks. Probably should have thought of that first, but thanks either way! Mar 28, 2019 at 17:57
  • 1
    @BiscuitKnees Ah, I presumed window.addEventListener directly was prohibited. I guess I should have read the code more closely. If you want, you can add your own answer (we totally encourage that) so others can benefit.
    – sfdcfox
    Mar 28, 2019 at 18:00
12

Although I wasn't able to solve my above problem exactly as I posted it, I was able to solve it with a different approach.

I believe the issue I was having was locker service related. This is just a theory, but I believe since e.target in my scenario is a proxy, it can't be used to compare against other nodes within my DOM, even if they are within the same component.

I was able to work around this issue by restructuring my code a little bit so that I am not relying on DOM node comparison. Here is an example of the final code.

onNodeClick = () => {
  this._isOpen = true;

  //Timeout is to prevent the click from continuing and closing the element right away.
  //Just this slight delay of 0 is enough to stop propagation
  window.setTimeout(() => window.addEventListener('click', this.handleClose), 0);
}

handleClose = () => {
  this._isOpen = false;
  window.removeEventListener('click', this.handleClose);
}
4
  • Also, important to note that any other click events that should happen within the element would also need to use e.stopPropagation() in order to stop the event from bubbling up further unless you want that click to close the popup. Mar 28, 2019 at 19:16
  • This is a nice trick. I didn't realize with locker on we can add window events safely (see current api spec here developer.salesforce.com/docs/component-library/tools/…)
    – tsalb
    Mar 28, 2019 at 21:12
  • 2
    Great technique! I was able to do away with the setTimeout call also, by calling event.stopImmediatePropagation(). Also note... the callback must be either an arrow function or bound to 'this'... I missed that in the code above and spend half and hour or so sorting it out. Jun 23, 2019 at 3:34
  • Hi, can you share your LWC code for this bit of functionality to close dropdown on outside click but maintain state if clicked inside options?
    – VarunC
    Mar 24, 2020 at 14:52
1

Another tweak I found it working shown below. "onmouseleve" gets called when mouse is taken out of the component where it's mentioned.

 <div onmouseleave={function}> 
    <Your content here>

 </div>
1
  • Is the onmouseleave on lookup component? Can you add bit more detail in your code(html and js).
    – devforce
    Jul 1, 2020 at 7:33

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .