diff --git a/src/index.js b/src/index.js index f82fbd5..29f8209 100644 --- a/src/index.js +++ b/src/index.js @@ -3,7 +3,8 @@ import { h, cloneElement, render, hydrate } from 'preact'; /** * @typedef {import('preact').FunctionComponent | import('preact').ComponentClass | import('preact').FunctionalComponent } ComponentDefinition * @typedef {{ shadow: false } | { shadow: true, mode: 'open' | 'closed'}} Options - * @typedef {HTMLElement & { _root: ShadowRoot | HTMLElement, _vdomComponent: ComponentDefinition, _vdom: ReturnType | null }} PreactCustomElement + * @typedef {() => (Promise | void) | null } Callback + * @typedef {HTMLElement & { _root: ShadowRoot | HTMLElement, _vdomComponent: ComponentDefinition, _vdom: ReturnType | null, _callback: Callback }} PreactCustomElement */ /** @@ -12,6 +13,7 @@ import { h, cloneElement, render, hydrate } from 'preact'; * @param {string} [tagName] The HTML element tag-name (must contain a hyphen and be lowercase) * @param {string[]} [propNames] HTML element attributes to observe * @param {Options} [options] Additional element options + * @param {Callback} [callback] Optional callback function which gets executed within `disconnectedCallback` * @example * ```ts * // use custom web-component class @@ -37,7 +39,13 @@ import { h, cloneElement, render, hydrate } from 'preact'; * }); * ``` */ -export default function register(Component, tagName, propNames, options) { +export default function register( + Component, + tagName, + propNames, + options, + callback +) { function PreactElement() { const inst = /** @type {PreactCustomElement} */ ( Reflect.construct(HTMLElement, [], PreactElement) @@ -47,6 +55,7 @@ export default function register(Component, tagName, propNames, options) { options && options.shadow ? inst.attachShadow({ mode: options.mode || 'open' }) : inst; + inst._callback = callback; return inst; } PreactElement.prototype = Object.create(HTMLElement.prototype); @@ -165,6 +174,7 @@ function attributeChangedCallback(name, oldValue, newValue) { * @this {PreactCustomElement} */ function disconnectedCallback() { + void this._callback?.(); render((this._vdom = null), this._root); } diff --git a/src/index.test.jsx b/src/index.test.jsx index f293941..1f41742 100644 --- a/src/index.test.jsx +++ b/src/index.test.jsx @@ -55,6 +55,40 @@ describe('web components', () => { assert.doesNotThrow(() => el.removeAttribute('size')); }); + describe('disconnectedCallback', () => { + it('should call callback when element is removed from dom', () => { + const Bar = () =>
; + let callbackExecuted = false; + const callback = () => { + callbackExecuted = true; + }; + registerElement(Bar, 'x-bar', [], undefined, callback); + const el = document.createElement('x-bar'); + + root.appendChild(el); + assert.equal(root.innerHTML, '
'); + + root.removeChild(el); + assert.equal(callbackExecuted, true); + }); + + it('should call async callback when element is removed from dom', () => { + const Baz = () =>
; + let callbackExecuted = false; + const callback = async () => { + callbackExecuted = true; + }; + registerElement(Baz, 'x-baz', [], undefined, callback); + const el = document.createElement('x-baz'); + + root.appendChild(el); + assert.equal(root.innerHTML, '
'); + + root.removeChild(el); + assert.equal(callbackExecuted, true); + }); + }); + describe('DOM properties', () => { it('passes property changes to props', () => { const el = document.createElement('x-clock');