diff --git a/package.json b/package.json index efcc3fc..094d8d6 100644 --- a/package.json +++ b/package.json @@ -14,7 +14,7 @@ "scripts": { "prepare": "npx simple-git-hooks", "build": "mkdir -p dist && terser src/index.js -o dist/preact-custom-element.js --config-file terser-config.json", - "test": "npm run test:types & npm run test:browser", + "test": "npm run test:types && npm run test:browser", "test:browser": "wtr test/browser/*.test.{js,jsx}", "test:types": "tsc -p test/types/", "lint": "eslint src/*.js", diff --git a/src/index.d.ts b/src/index.d.ts index b399309..e7b7d62 100644 --- a/src/index.d.ts +++ b/src/index.d.ts @@ -52,4 +52,6 @@ export default function register

( tagName?: string, propNames?: (keyof P)[], options?: Options -): HTMLElement; +): typeof HTMLElement & { + new (): HTMLElement; +}; diff --git a/src/index.js b/src/index.js index e742b24..efc007d 100644 --- a/src/index.js +++ b/src/index.js @@ -8,44 +8,58 @@ import { h, cloneElement, render, hydrate } from 'preact'; * @type {import('./index.d.ts').default} */ export default function register(Component, tagName, propNames, options) { - function PreactElement() { - const inst = /** @type {PreactCustomElement} */ ( - Reflect.construct(HTMLElement, [], PreactElement) - ); - inst._vdomComponent = Component; - - if (options && options.shadow) { - inst._root = inst.attachShadow({ - mode: options.mode || 'open', - serializable: options.serializable ?? false, - }); - - if (options.adoptedStyleSheets) { - inst._root.adoptedStyleSheets = options.adoptedStyleSheets; + class PreactElement extends HTMLElement { + constructor() { + super(); + + this._vdomComponent = Component; + if (options && options.shadow) { + this._root = this.attachShadow({ + mode: options.mode || 'open', + serializable: options.serializable ?? false, + }); + + if (options.adoptedStyleSheets) { + this._root.adoptedStyleSheets = options.adoptedStyleSheets; + } + } else { + this._root = this; } - } else { - inst._root = inst; } - return inst; + connectedCallback() { + connectedCallback.call(this, options); + } + + /** + * Changed whenever an attribute of the HTML element changed + * + * @param {string} name The attribute name + * @param {unknown} oldValue The old value or undefined + * @param {unknown} newValue The new value + */ + attributeChangedCallback(name, oldValue, newValue) { + if (!this._vdom) return; + // Attributes use `null` as an empty value whereas `undefined` is more + // common in pure JS components, especially with default parameters. + // When calling `node.removeAttribute()` we'll receive `null` as the new + // value. See issue #50. + newValue = newValue == null ? undefined : newValue; + const props = {}; + props[name] = newValue; + this._vdom = cloneElement(this._vdom, props); + render(this._vdom, this._root); + } + + disconnectedCallback() { + render((this._vdom = null), this._root); + } } - PreactElement.prototype = Object.create(HTMLElement.prototype); - PreactElement.prototype.constructor = PreactElement; - PreactElement.prototype.connectedCallback = function () { - connectedCallback.call(this, options); - }; - PreactElement.prototype.attributeChangedCallback = attributeChangedCallback; - PreactElement.prototype.disconnectedCallback = disconnectedCallback; - /** - * @type {string[]} - */ propNames = propNames || Component.observedAttributes || []; PreactElement.observedAttributes = propNames; - if (Component.formAssociated) { - PreactElement.formAssociated = true; - } + PreactElement.formAssociated = Component.formAssociated || false; // Keep DOM properties and Preact props in sync propNames.forEach((name) => { @@ -115,33 +129,6 @@ function connectedCallback(options) { (this.hasAttribute('hydrate') ? hydrate : render)(this._vdom, this._root); } -/** - * Changed whenver an attribute of the HTML element changed - * @this {PreactCustomElement} - * @param {string} name The attribute name - * @param {unknown} oldValue The old value or undefined - * @param {unknown} newValue The new value - */ -function attributeChangedCallback(name, oldValue, newValue) { - if (!this._vdom) return; - // Attributes use `null` as an empty value whereas `undefined` is more - // common in pure JS components, especially with default parameters. - // When calling `node.removeAttribute()` we'll receive `null` as the new - // value. See issue #50. - newValue = newValue == null ? undefined : newValue; - const props = {}; - props[name] = newValue; - this._vdom = cloneElement(this._vdom, props); - render(this._vdom, this._root); -} - -/** - * @this {PreactCustomElement} - */ -function disconnectedCallback() { - render((this._vdom = null), this._root); -} - /** * Pass an event listener to each `` that "forwards" the current * context value to the rendered child. The child will trigger a custom diff --git a/test/types/index.test.tsx b/test/types/index.test.tsx index 383a841..cc3feca 100644 --- a/test/types/index.test.tsx +++ b/test/types/index.test.tsx @@ -1,5 +1,5 @@ import { h } from 'preact'; -import registerElement from '../../src/index'; +import registerElement from '../../src/index.js'; interface AppProps { name: string;