0

I'm building a site navigation using litelement. It will have a dropdown menu. I am trying to figure out how to add an event so that if a user clicks anywhere outside the dropdown menu itself or even outside of the custom element, the dropdown menu will close. I think that's the natural expectation.

I thought of adding a property to my custom element that can be used as a "state". And then added an event listener to the document through connectedCallback lifecycle method. But, it seems I can't really access the property as I had expected. The property is accessible through any other elements that has an event attached to it.

Below is a very generic codepen. Clicking on anywhere on the document should open up a popup that shows the value of the property is undefined. However, if I click on the button that's inside the custom element, which has an event attached to it, that event handler is able to access the property successfully.

https://codepen.io/aver-mimas/pen/ExjZXMq

What's going wrong in this example?

aver
  • 555
  • 2
  • 7
  • 21
  • Hey, I just added an answer that I think is a bit more accurate than the currently accepted one. Could you check it? – Yulian Dec 09 '21 at 13:05

2 Answers2

1

What is happening in your codepen, is that when you send a member function as parameter to something which will store it and use it later (an event listener will store the function in an internal variable, and call it when the event is fired) in JS, this member function "loose" the this property. It is not linked to lit-element but on the js itself, and can happen in a variety of case, but lit-element create many case where it can become revelant.

You can use

document.addEventListener('click', e=>this.handleDocumentClick())

instead of

document.addEventListener('click', this.handleDocumentClick)

The arrow function will instruct JS to bind the 'this' of the object in which it is declared, and then call the member function with the correct context.

Arfost
  • 506
  • 1
  • 4
  • 16
  • Thanks a lot for your reply and the explanation. That explains it and worked perfectly. I was also trying it out a bit differently. I can also do the binding in the constructor as `this.handleDocumentClick = this.handleDocumentClick.bind(this)` and keep the original way of adding the event listener. – aver Feb 24 '20 at 17:08
  • You can also just transform the method into a class field with an arrow-function value: `class X { handleDocumentClick = (e) => { ... }; }` – Justin Fagnani Feb 25 '20 at 00:11
0

Although wrapping the click handler in an anonymous function works fine when adding the event listener, removing it, which you should generally do, won't work. The reason is that although the two anonymous functions look identical, they have completely different references.

document.addEventListener('click', e => this.handleDocumentClick());
document.removeEventListener('click', e => this.handleDocumentClick()); // ❌ doesn't work

For this reason, in such cases you should use an arrow function inside your class, as stated in the docs:

const handleDocumentClick = () => {
  // handle click event
};

// ..

document.addEventListener('click', this.handleDocumentClick);
document.removeEventListener('click', this.handleDocumentClick); // ✅ works
Yulian
  • 6,262
  • 10
  • 65
  • 92