From f945051f4ae63865c2ca31645ccf829105dea954 Mon Sep 17 00:00:00 2001 From: jordi fita mas Date: Mon, 10 Apr 2023 00:05:29 +0200 Subject: [PATCH] Remove document and window event handlers when removing custom elements MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit I realized that the event handlers that i was setting when creating the tags input and the multi-select controls were not removed just because these elements are no longer in the document, and kept firing again and again. I no longer can use an anonymous function, because removeEventListener would not match it with the one passed to addEventListener. I also have to bind the handler to `this` in order to keep having access to the object, and, again, can not do it in the call to addEventListener, or i would get a different function each time. I added the check to see if the element is connected inside the connectedCallback because the documentation warns that this callback “may be called once your element is no longer connected”[0], and i understood it to mean that the connected and disconnected callbacks could be called our of order, thus it would be possible to add event listeners that would not be removed—again. I am not actually sure where i have to do the same for the rest of the “internal” events. [0]: https://developer.mozilla.org/en-US/docs/Web/Web_Components/Using_custom_elements#using_the_lifecycle_callbacks --- web/static/numerus.js | 50 ++++++++++++++++++++++++++++--------------- 1 file changed, 33 insertions(+), 17 deletions(-) diff --git a/web/static/numerus.js b/web/static/numerus.js index 86b7c23..c0ab0cb 100644 --- a/web/static/numerus.js +++ b/web/static/numerus.js @@ -4,9 +4,13 @@ class Multiselect extends HTMLDivElement { this.initialized = false; this.toSearch = ''; this.highlighted = 0; + this.onFocusOutHandler = this.onFocusOut.bind(this); } connectedCallback() { + if (!this.isConnected) { + return; + } if (!this.initialized) { for (const child of this.children) { switch (child.nodeName) { @@ -22,6 +26,19 @@ class Multiselect extends HTMLDivElement { this.init(); } } + window.addEventListener('focusin', this.onFocusOutHandler); + document.addEventListener('click', this.onFocusOutHandler); + } + + disconnectedCallback() { + document.removeEventListener('click', this.onFocusOutHandler); + window.removeEventListener('focusin', this.onFocusOutHandler); + } + + onFocusOut(e) { + if (this.contains(e.target)) return; + if (!e.target.isConnected) return; + this.close(); } init() { @@ -93,17 +110,6 @@ class Multiselect extends HTMLDivElement { this.options.setAttribute('role', 'listbox'); this.close(); - window.addEventListener('focusin', (e) => { - if (this.contains(e.target)) return; - this.close(); - }); - - document.addEventListener('click', (e) => { - if (this.contains(e.target)) return; - if (!e.target.isConnected) return; - this.close(); - }); - this.rebuild(); } @@ -278,9 +284,13 @@ class Tags extends HTMLDivElement { super(); this.initialized = false; this.tags = []; + this.onFocusOutHandler = this.onFocusOut.bind(this); } connectedCallback() { + if (!this.isConnected) { + return; + } if (!this.initialized) { for (const child of this.children) { switch (child.nodeName) { @@ -296,6 +306,18 @@ class Tags extends HTMLDivElement { this.init(); } } + window.addEventListener('focusin', this.onFocusOutHandler); + } + + disconnectedCallback() { + window.removeEventListener('focusin', this.onFocusOutHandler); + } + + onFocusOut(e) { + if (this.contains(e.target)) return; + if (e.target.value && e.target.value.trim() !== '') { + this.createTag(); + } } init() { @@ -339,12 +361,6 @@ class Tags extends HTMLDivElement { break; } }); - window.addEventListener('focusin', (e) => { - if (this.contains(e.target)) return; - if (e.target.value.trim() !== '') { - this.createTag(); - } - }); // Must come after the search input this.tagList.append(this.label);