4

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.

5

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).

  • 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 – citizen conn Mar 28 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 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! – BiscuitKnees Mar 28 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! – BiscuitKnees Mar 28 at 17:57
  • @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 at 18:00
6

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);
}
  • 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. – BiscuitKnees Mar 28 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 at 21:12
  • 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. – Caspar Harmer Jun 23 at 3:34

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.