I have a custom element offering an API method sayHello
. I need to "destroy" all references to the custom element in the disconnectedCallback
if the element gets removed from the DOM. How can I achieve this?
class RemoveEl extends HTMLButtonElement {
constructor() {
super();
this.type= 'button';
this.addEventListener('click', () => {
this.parentElement.removeChild(this);
})
}
sayHello() {
console.log('hello');
}
disconnectedCallback() {
if (!document.body.contains(this)) {
console.log('removed');
// here I need something like
// this = null;
}
}
}
customElements.define('remove-el', RemoveEl, { extends: 'button' });
var sayHello = document.getElementById('sayHello');
var removeEl = document.getElementById('removeEl');
sayHello.addEventListener('click', () => {
if (removeEl) {
removeEl.sayHello();
}
})
<div>Test:
<button is="remove-el" id="removeEl">Click to remove</button>
<button id="sayHello" type="button">Say Hello</button>
</div>
From my understanding, as long as you hold a reference to your element on the JavaScript side, the garbage collector won't be able to destroy your element, even if it was removed from the DOM. Your element will still be live and you will be able to call methods on it.
You will have to manage the references yourself. In the disconnectedCallback
of your custom element, set a property to mark it deleted, for example: this.destroyed = true
.
Then you can use that property to safeguard the access, but the element won't be garbage collected:
class RemoveEl extends HTMLButtonElement {
constructor() {
super();
this.type= 'button';
this.addEventListener('click', () => {
this.parentElement.removeChild(this);
})
}
sayHello() {
console.log('hello');
}
disconnectedCallback() {
if (!document.body.contains(this)) {
this.destroyed = true;
console.log('removed');
}
}
}
customElements.define('remove-el', RemoveEl, { extends: 'button' });
const sayHello = document.getElementById('sayHello');
const removeEl = document.getElementById('removeEl');
sayHello.addEventListener('click', () => {
if (removeEl && !removeEl.destroyed) {
removeEl.sayHello();
}
})
<div>Test:
<button is="remove-el" id="removeEl">Click to remove</button>
<button id="sayHello" type="button">Say Hello</button>
</div>
Or create a reference wrapper on which you can apply functions only if the inner reference is valid, still garbage collection won't be able to destroy the reference since there is now a closure over it due to the do
function using el
:
class RemoveEl extends HTMLButtonElement {
constructor() {
super();
this.type= 'button';
this.addEventListener('click', () => {
this.parentElement.removeChild(this);
})
}
sayHello() {
console.log('hello');
}
disconnectedCallback() {
if (!document.body.contains(this)) {
this.destroyed = true;
console.log('removed');
}
}
}
customElements.define('remove-el', RemoveEl, { extends: 'button' });
const ref = el => ({ do: fn => { if (el && !el.destroyed) fn(el); } })
const sayHello = document.getElementById('sayHello');
const removeEl = ref(document.getElementById('removeEl'));
sayHello.addEventListener('click', () => {
removeEl.do(el => el.sayHello());
})
<div>Test:
<button is="remove-el" id="removeEl">Click to remove</button>
<button id="sayHello" type="button">Say Hello</button>
</div>
Or you could use a proxy to manage that reference. As long as destroyed
is false, the methods would be called on the object, but as soon as the proxy detects that destroyed = true
, it would return a default value for the properties and destroy its own reference to the element, which hopefully will let the garbage collector get rid of it.
Something a bit like this:
class RemoveEl extends HTMLButtonElement {
constructor() {
super();
this.type= 'button';
this.addEventListener('click', () => {
this.parentElement.removeChild(this);
})
}
sayHello() {
console.log('hello');
}
disconnectedCallback() {
if (!document.body.contains(this)) {
this.destroyed = true;
console.log('removed');
}
}
}
customElements.define('remove-el', RemoveEl, { extends: 'button' });
const ref = (el, defaultEl) => {
let destroyed = el.destroyed;
const checkEl = () => {
if (!destroyed && el && el.destroyed) {
destroyed = true;
el = null;
}
return destroyed;
}
return new Proxy({}, {
get: (obj, prop) => {
return checkEl() ? defaultEl[prop] : el[prop];
}
});
}
const sayHello = document.getElementById('sayHello');
const removeEl = ref(document.getElementById('removeEl'), { sayHello: () => console.log('bye') });
sayHello.addEventListener('click', () => {
removeEl.sayHello();
})
<div>Test:
<button is="remove-el" id="removeEl">Click to remove</button>
<button id="sayHello" type="button">Say Hello</button>
</div>