From 14b7c874b7a3bc6783c165576764c6531f053850 Mon Sep 17 00:00:00 2001 From: Markus Hesper Date: Wed, 8 Oct 2025 16:19:15 +0200 Subject: [PATCH 1/9] fix(attribute vs prop): add rerpodcution as example https://codepen.io/quarkus/pen/MYKpxgJ --- examples/bugs/index.html | 106 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 106 insertions(+) create mode 100644 examples/bugs/index.html diff --git a/examples/bugs/index.html b/examples/bugs/index.html new file mode 100644 index 0000000..2234cd7 --- /dev/null +++ b/examples/bugs/index.html @@ -0,0 +1,106 @@ + + + + + + + + + + + + + From e65e800521baf0571df68f14d0103e44b60867e8 Mon Sep 17 00:00:00 2001 From: Markus Hesper Date: Thu, 9 Oct 2025 19:10:13 +0200 Subject: [PATCH 2/9] fix(attribute vs prop): fix propertyAttribute cache ti mitigate races between assignments and connection this is also not flushed on "disconnected" on purpose as this might be the only way to restore values that were set from outsiede before leaving the dom --- examples/bugs/index.html | 18 +++++++++++------- src/BaseElement.js | 3 ++- src/dom-parts/AttributePart.js | 5 +++++ 3 files changed, 18 insertions(+), 8 deletions(-) diff --git a/examples/bugs/index.html b/examples/bugs/index.html index 2234cd7..c25f0e1 100644 --- a/examples/bugs/index.html +++ b/examples/bugs/index.html @@ -21,7 +21,7 @@ } // WORKS return html` - + `; } @@ -32,7 +32,7 @@ } // DOES NOT WORK return html` - + `; } @@ -54,31 +54,35 @@ properties() { return { items: null, + attr: null, }; } connectedCallback() { super.connectedCallback(); - console.log('inner:connectedCallback', this.getAttribute('id'), this.items); + console.log('inner:connectedCallback', this.getAttribute('id'), this.items, this.ownerDocument); } connected() { - console.log('inner:connected', this.getAttribute('id'), this.items); + console.log('inner:connected', this.getAttribute('id'), this.items, this.ownerDocument); } disconnectedCallback() { super.disconnectedCallback(); - console.log('inner:disconnectedCallback', this.getAttribute('id'), this.items); + console.log('inner:disconnectedCallback', this.getAttribute('id'), this.items, this.ownerDocument); } disconnected() { - console.log('inner:disconnected', this.getAttribute('id'), this.items); + console.log('inner:disconnected', this.getAttribute('id'), this.items, this.ownerDocument); } template() { return html` -
${this.items?.toString()}
+
+ ${this.items?.toString()}
+ ${this.attr?.toString()} +
`; } } diff --git a/src/BaseElement.js b/src/BaseElement.js index 6ec8c46..2f022c6 100644 --- a/src/BaseElement.js +++ b/src/BaseElement.js @@ -62,6 +62,7 @@ class BaseElement extends HTMLElement { super(); this.$refs = {}; this._state = {}; + this._propertyAttributes = this._propertyAttributes ?? {}; this._mutationObserver = null; this._registeredEvents = []; this._batchUpdate = null; @@ -298,7 +299,7 @@ class BaseElement extends HTMLElement { // registered or connected. To avoid such timing issues we check // if a value was set for that specific property on the // prototype before assigning a default value - const value = this[prop] || this.properties()[prop]; + const value = this[prop] || this._propertyAttributes[prop] || this.properties()[prop]; this.defineProperty(prop, value); }); } diff --git a/src/dom-parts/AttributePart.js b/src/dom-parts/AttributePart.js index f7b71e5..ccbca5d 100644 --- a/src/dom-parts/AttributePart.js +++ b/src/dom-parts/AttributePart.js @@ -23,6 +23,11 @@ const processBooleanAttribute = (node, name, oldValue) => { const processPropertyAttribute = (node, name) => { return (value) => { node[name] = value; + if (!node._propertyAttributes) { + // means processing starts before node was constructed + node._propertyAttributes = {}; + } + node._propertyAttributes[name] = value; }; }; From f3ad6b7a7263297da8e63046f91a3ece05d40c59 Mon Sep 17 00:00:00 2001 From: Markus Hesper Date: Fri, 10 Oct 2025 09:49:44 +0200 Subject: [PATCH 3/9] fix(zombie-elements): add rerpoduction case from: https://github.com/webtides/element-js/issues/158 as it is basically the same issue that we deal with at the moment with "propertyAttributes" --- examples/bugs/array.html | 101 +++++++++++++++++++++++++++++++++++++++ src/BaseElement.js | 9 ++-- 2 files changed, 107 insertions(+), 3 deletions(-) create mode 100644 examples/bugs/array.html diff --git a/examples/bugs/array.html b/examples/bugs/array.html new file mode 100644 index 0000000..37fcb74 --- /dev/null +++ b/examples/bugs/array.html @@ -0,0 +1,101 @@ + + + + + + + + + + + + diff --git a/src/BaseElement.js b/src/BaseElement.js index 2f022c6..3f0a8e7 100644 --- a/src/BaseElement.js +++ b/src/BaseElement.js @@ -196,7 +196,8 @@ class BaseElement extends HTMLElement { this.$refs = {}; // clear state - this._state = {}; + // TODO decide on how to deal with zombie state. either overwrite on "reconnect" OR delete on dsiconnect (what we did with 1.2.4) + // this._state = {}; // cancel pending updates if (this._batchUpdate) { @@ -299,7 +300,9 @@ class BaseElement extends HTMLElement { // registered or connected. To avoid such timing issues we check // if a value was set for that specific property on the // prototype before assigning a default value - const value = this[prop] || this._propertyAttributes[prop] || this.properties()[prop]; + // TODO decide on how to deal with zombie state. either overwrite on "reconnect" OR delete on dsiconnect (what we did with 1.2.4) + // const value = this[prop] || this._propertyAttributes[prop] || this.properties()[prop]; + // const value = this[prop] || this.properties()[prop]; this.defineProperty(prop, value); }); } @@ -315,7 +318,7 @@ class BaseElement extends HTMLElement { defineProperty(property, value, isAttribute = false) { if (this._state.hasOwnProperty(property)) { // property has already been defined as an attribute nothing to do here - return; + // return; } // if property did not come from an attribute but has the option to reflect // enabled or custom fn From 832a6fe9350796fe84e40f8ec28f0f0b4f1a7b0d Mon Sep 17 00:00:00 2001 From: Markus Hesper Date: Fri, 10 Oct 2025 09:54:49 +0200 Subject: [PATCH 4/9] fix(zombie-elements): rm comment --- src/BaseElement.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/BaseElement.js b/src/BaseElement.js index 3f0a8e7..7518769 100644 --- a/src/BaseElement.js +++ b/src/BaseElement.js @@ -302,7 +302,7 @@ class BaseElement extends HTMLElement { // prototype before assigning a default value // TODO decide on how to deal with zombie state. either overwrite on "reconnect" OR delete on dsiconnect (what we did with 1.2.4) // const value = this[prop] || this._propertyAttributes[prop] || this.properties()[prop]; - // const value = this[prop] || this.properties()[prop]; + const value = this[prop] || this.properties()[prop]; this.defineProperty(prop, value); }); } From 6f515a02a5cafcce339b7ae519b8504919f65aed Mon Sep 17 00:00:00 2001 From: Markus Hesper Date: Fri, 10 Oct 2025 10:02:22 +0200 Subject: [PATCH 5/9] fix(zombie-elements): add comment to check execution order --- src/BaseElement.js | 1 + 1 file changed, 1 insertion(+) diff --git a/src/BaseElement.js b/src/BaseElement.js index 7518769..b569763 100644 --- a/src/BaseElement.js +++ b/src/BaseElement.js @@ -109,6 +109,7 @@ class BaseElement extends HTMLElement { } } + // TODO check order of overwrites. ideally these lists shoukd be merged anyways attribute values > state > property defaults // define all attributes to "this" as properties this.defineAttributesAsProperties(); From 5c10f6ddd93c8b2cbf5fa18f24ef1318930d7c6a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Eddy=20Lo=CC=88wen?= Date: Fri, 10 Oct 2025 11:28:24 +0200 Subject: [PATCH 6/9] test: add test for attribute to property mapping --- test/unit/attributes-to-properties.test.js | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/test/unit/attributes-to-properties.test.js b/test/unit/attributes-to-properties.test.js index 29301c2..99644d1 100644 --- a/test/unit/attributes-to-properties.test.js +++ b/test/unit/attributes-to-properties.test.js @@ -2,7 +2,15 @@ import { fixture, defineCE, assert } from '@open-wc/testing'; import { BaseElement } from '../../src/BaseElement'; -const tag = defineCE(class extends BaseElement {}); +const tag = defineCE( + class extends BaseElement { + properties() { + return { + firstname: 'John', + }; + } + }, +); describe('attributes-to-properties', () => { it('reflects lowercase attributes as lowercase properties', async () => { @@ -24,4 +32,9 @@ describe('attributes-to-properties', () => { assert.isTrue(el.hasOwnProperty('dashCase')); assert.equal(el.dashCase, 'Hello'); }); + + it('prefers attribute values over the default values from the properties map for default/initial state', async () => { + const el = await fixture(`<${tag} firstname="Jane">`); + assert.equal(el.firstname, 'Jane'); + }); }); From bfb3fbc84973f2a1800ec1c0a834af9060987968 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Eddy=20Lo=CC=88wen?= Date: Fri, 10 Oct 2025 12:18:39 +0200 Subject: [PATCH 7/9] fix(zombie-elements): disables property attribute parking --- src/BaseElement.js | 2 +- src/dom-parts/AttributePart.js | 10 +++++----- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/BaseElement.js b/src/BaseElement.js index b569763..45917d1 100644 --- a/src/BaseElement.js +++ b/src/BaseElement.js @@ -62,7 +62,7 @@ class BaseElement extends HTMLElement { super(); this.$refs = {}; this._state = {}; - this._propertyAttributes = this._propertyAttributes ?? {}; + // this._propertyAttributes = this._propertyAttributes ?? {}; this._mutationObserver = null; this._registeredEvents = []; this._batchUpdate = null; diff --git a/src/dom-parts/AttributePart.js b/src/dom-parts/AttributePart.js index ccbca5d..26c019c 100644 --- a/src/dom-parts/AttributePart.js +++ b/src/dom-parts/AttributePart.js @@ -23,11 +23,11 @@ const processBooleanAttribute = (node, name, oldValue) => { const processPropertyAttribute = (node, name) => { return (value) => { node[name] = value; - if (!node._propertyAttributes) { - // means processing starts before node was constructed - node._propertyAttributes = {}; - } - node._propertyAttributes[name] = value; + // if (!node._propertyAttributes) { + // // means processing starts before node was constructed + // node._propertyAttributes = {}; + // } + // node._propertyAttributes[name] = value; }; }; From 0a0db59ce6f43c2bd18c14d375d3ca27afdc2dab Mon Sep 17 00:00:00 2001 From: Markus Hesper Date: Fri, 10 Oct 2025 18:17:56 +0200 Subject: [PATCH 8/9] fix(zombie-elements): cleanup and rm todos, also changes the "assignement" lookup to Nullish coalescing as this would actually make falsy values going back to default. --- examples/bugs/{ => 158-zombie}/array.html | 2 +- .../{index.html => 158-zombie/properties.html} | 2 +- src/BaseElement.js | 15 +-------------- src/dom-parts/AttributePart.js | 5 ----- 4 files changed, 3 insertions(+), 21 deletions(-) rename examples/bugs/{ => 158-zombie}/array.html (98%) rename examples/bugs/{index.html => 158-zombie/properties.html} (98%) diff --git a/examples/bugs/array.html b/examples/bugs/158-zombie/array.html similarity index 98% rename from examples/bugs/array.html rename to examples/bugs/158-zombie/array.html index 37fcb74..5c45e1a 100644 --- a/examples/bugs/array.html +++ b/examples/bugs/158-zombie/array.html @@ -8,7 +8,7 @@ TemplateElement, defineElement, html - } from "../../index.js"; + } from "../../../index.js"; /*this works > 1.2.3*/ class OuterElement extends TemplateElement { diff --git a/examples/bugs/index.html b/examples/bugs/158-zombie/properties.html similarity index 98% rename from examples/bugs/index.html rename to examples/bugs/158-zombie/properties.html index c25f0e1..3e5f642 100644 --- a/examples/bugs/index.html +++ b/examples/bugs/158-zombie/properties.html @@ -6,7 +6,7 @@ TemplateElement, html, defineElement, - } from '../../index.js'; + } from '../../../index.js'; class MyElement extends TemplateElement { properties() { diff --git a/src/BaseElement.js b/src/BaseElement.js index 45917d1..655f0f6 100644 --- a/src/BaseElement.js +++ b/src/BaseElement.js @@ -62,7 +62,6 @@ class BaseElement extends HTMLElement { super(); this.$refs = {}; this._state = {}; - // this._propertyAttributes = this._propertyAttributes ?? {}; this._mutationObserver = null; this._registeredEvents = []; this._batchUpdate = null; @@ -109,7 +108,6 @@ class BaseElement extends HTMLElement { } } - // TODO check order of overwrites. ideally these lists shoukd be merged anyways attribute values > state > property defaults // define all attributes to "this" as properties this.defineAttributesAsProperties(); @@ -196,10 +194,6 @@ class BaseElement extends HTMLElement { // release dom references this.$refs = {}; - // clear state - // TODO decide on how to deal with zombie state. either overwrite on "reconnect" OR delete on dsiconnect (what we did with 1.2.4) - // this._state = {}; - // cancel pending updates if (this._batchUpdate) { cancelAnimationFrame(this._batchUpdate); @@ -301,9 +295,7 @@ class BaseElement extends HTMLElement { // registered or connected. To avoid such timing issues we check // if a value was set for that specific property on the // prototype before assigning a default value - // TODO decide on how to deal with zombie state. either overwrite on "reconnect" OR delete on dsiconnect (what we did with 1.2.4) - // const value = this[prop] || this._propertyAttributes[prop] || this.properties()[prop]; - const value = this[prop] || this.properties()[prop]; + const value = this[prop] ?? this.properties()[prop]; this.defineProperty(prop, value); }); } @@ -317,11 +309,6 @@ class BaseElement extends HTMLElement { * @param {boolean} isAttribute */ defineProperty(property, value, isAttribute = false) { - if (this._state.hasOwnProperty(property)) { - // property has already been defined as an attribute nothing to do here - // return; - } - // if property did not come from an attribute but has the option to reflect // enabled or custom fn if (!isAttribute && this._options.propertyOptions[property]?.reflect) { this.reflectProperty({ property: property, newValue: value }); diff --git a/src/dom-parts/AttributePart.js b/src/dom-parts/AttributePart.js index 26c019c..f7b71e5 100644 --- a/src/dom-parts/AttributePart.js +++ b/src/dom-parts/AttributePart.js @@ -23,11 +23,6 @@ const processBooleanAttribute = (node, name, oldValue) => { const processPropertyAttribute = (node, name) => { return (value) => { node[name] = value; - // if (!node._propertyAttributes) { - // // means processing starts before node was constructed - // node._propertyAttributes = {}; - // } - // node._propertyAttributes[name] = value; }; }; From e4224edc7cc5303d37d70f9e037fad31f90f49c8 Mon Sep 17 00:00:00 2001 From: Markus Hesper Date: Mon, 27 Oct 2025 15:48:23 +0100 Subject: [PATCH 9/9] chore(element-js): npm audit fix --- package-lock.json | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/package-lock.json b/package-lock.json index 90431cb..c590960 100644 --- a/package-lock.json +++ b/package-lock.json @@ -5320,9 +5320,9 @@ } }, "node_modules/koa": { - "version": "2.16.2", - "resolved": "https://registry.npmjs.org/koa/-/koa-2.16.2.tgz", - "integrity": "sha512-+CCssgnrWKx9aI3OeZwroa/ckG4JICxvIFnSiOUyl2Uv+UTI+xIw0FfFrWS7cQFpoePpr9o8csss7KzsTzNL8Q==", + "version": "2.16.3", + "resolved": "https://registry.npmjs.org/koa/-/koa-2.16.3.tgz", + "integrity": "sha512-zPPuIt+ku1iCpFBRwseMcPYQ1cJL8l60rSmKeOuGfOXyE6YnTBmf2aEFNL2HQGrD0cPcLO/t+v9RTgC+fwEh/g==", "dev": true, "license": "MIT", "dependencies": { @@ -7474,9 +7474,9 @@ } }, "node_modules/tar-fs": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-3.1.0.tgz", - "integrity": "sha512-5Mty5y/sOF1YWj1J6GiBodjlDc05CUR8PKXrsnFAiSG0xA+GHeWLovaZPYUDXkH/1iKRf2+M5+OrRgzC7O9b7w==", + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-3.1.1.tgz", + "integrity": "sha512-LZA0oaPOc2fVo82Txf3gw+AkEd38szODlptMYejQUhndHMLQ9M059uXR+AfS7DNo0NpINvSqDsvyaCrBVkptWg==", "dev": true, "license": "MIT", "dependencies": { @@ -11903,9 +11903,9 @@ } }, "koa": { - "version": "2.16.2", - "resolved": "https://registry.npmjs.org/koa/-/koa-2.16.2.tgz", - "integrity": "sha512-+CCssgnrWKx9aI3OeZwroa/ckG4JICxvIFnSiOUyl2Uv+UTI+xIw0FfFrWS7cQFpoePpr9o8csss7KzsTzNL8Q==", + "version": "2.16.3", + "resolved": "https://registry.npmjs.org/koa/-/koa-2.16.3.tgz", + "integrity": "sha512-zPPuIt+ku1iCpFBRwseMcPYQ1cJL8l60rSmKeOuGfOXyE6YnTBmf2aEFNL2HQGrD0cPcLO/t+v9RTgC+fwEh/g==", "dev": true, "requires": { "accepts": "^1.3.5", @@ -13426,9 +13426,9 @@ } }, "tar-fs": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-3.1.0.tgz", - "integrity": "sha512-5Mty5y/sOF1YWj1J6GiBodjlDc05CUR8PKXrsnFAiSG0xA+GHeWLovaZPYUDXkH/1iKRf2+M5+OrRgzC7O9b7w==", + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-3.1.1.tgz", + "integrity": "sha512-LZA0oaPOc2fVo82Txf3gw+AkEd38szODlptMYejQUhndHMLQ9M059uXR+AfS7DNo0NpINvSqDsvyaCrBVkptWg==", "dev": true, "requires": { "bare-fs": "^4.0.1",