From 78685db3085c24697b80ae335eec382d4d2a3333 Mon Sep 17 00:00:00 2001 From: Markus Hesper Date: Wed, 3 Sep 2025 13:01:56 +0200 Subject: [PATCH 1/3] fix(ejs): create testcase for reproduction but it works fine in the test .. --- test/unit/vanilla-renderer.test.js | 54 ++++++++++++++++++++++++++++++ 1 file changed, 54 insertions(+) diff --git a/test/unit/vanilla-renderer.test.js b/test/unit/vanilla-renderer.test.js index d0cb513..b00561c 100644 --- a/test/unit/vanilla-renderer.test.js +++ b/test/unit/vanilla-renderer.test.js @@ -178,6 +178,37 @@ class OnePropTag extends TemplateElement { } } customElements.define('one-prop-tag', OnePropTag); + +class NestedElementTag extends TemplateElement { + properties() { + return { + label: 'empty', + }; + } + + template() { + return html``; + } +} + +customElements.define('nested-element-tag', NestedElementTag); + +class ArrayRenderingNestedElementsTag extends TemplateElement { + properties() { + return { + list: [1], + }; + } + + template() { + return html`
+ ${this.list.map((index) => html``)} +
`; + } +} + +customElements.define('array-rendering-nested-elements-tag', ArrayRenderingNestedElementsTag); + describe(`template rendering`, () => { it('renders template in light dom by default', async () => { const el = await fixture(`<${lightTag}>`); @@ -369,4 +400,27 @@ describe(`vanilla-renderer`, () => { await nextFrame(); assert.equal(stripCommentMarkers(arrayElement.innerHTML), '
1
2
3
'); }); + + it('Renders a list of nested template elements', async () => { + const arrayElement = await fixture( + ``, + ); + await nextFrame(); + + assert.equal(arrayElement.innerText, 'label1'); + arrayElement.list = [2, 3, 4]; + + await nextFrame(); + await nextFrame(); + assert.equal(arrayElement.innerText, 'label2 label3 label4'); + arrayElement.list = [69]; + await nextFrame(); + await nextFrame(); + assert.equal(arrayElement.innerText, 'label69'); + + await nextFrame(); + await nextFrame(); + + console.log('####ffoooo'); + }); }); From 56f5f73cfde1086e2f6d5c0d095f4af8d1b0bb0f Mon Sep 17 00:00:00 2001 From: Markus Hesper Date: Wed, 3 Sep 2025 16:24:25 +0200 Subject: [PATCH 2/3] fix(ejs): create testcase for proper reproduction adds cleanup routine to flush state, refs and updates on disconnected. guard propertyCreation with getOwnPropertyDescriptor to not redeclare properties for elements that return to the dom after being detached before --- src/BaseElement.js | 24 ++++++++++++++++++++---- test/unit/vanilla-renderer.test.js | 30 ++++++++++++++++++++++++++---- 2 files changed, 46 insertions(+), 8 deletions(-) diff --git a/src/BaseElement.js b/src/BaseElement.js index 2dd8548..b2f18c3 100644 --- a/src/BaseElement.js +++ b/src/BaseElement.js @@ -191,6 +191,18 @@ class BaseElement extends HTMLElement { } }); + // release dom references + this.$refs = {}; + + // clear state + this._state = {}; + + // cancel pending updates + if (this._batchUpdate) { + cancelAnimationFrame(this._batchUpdate); + this._requestedUpdates = []; + } + this.triggerHook('disconnected'); } @@ -297,16 +309,16 @@ class BaseElement extends HTMLElement { * Will trigger update() when a property was changed * @param {string} property * @param {any} value - * @param {boolean} reflectAttribute + * @param {boolean} isAttribute */ - defineProperty(property, value, reflectAttribute = false) { + 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 (!reflectAttribute && this._options.propertyOptions[property]?.reflect) { + if (!isAttribute && this._options.propertyOptions[property]?.reflect) { this.reflectProperty({ property: property, newValue: value }); } @@ -325,6 +337,10 @@ class BaseElement extends HTMLElement { this._state[property].subscribe(this); } + if (Object.getOwnPropertyDescriptor(this, property)) { + // instance already has a defined property + return; + } Object.defineProperty(this, property, { get: () => { return this._state[property]; @@ -341,7 +357,7 @@ class BaseElement extends HTMLElement { newValue.subscribe(this); } - if (reflectAttribute || this._options.propertyOptions[property]?.reflect) { + if (isAttribute || this._options.propertyOptions[property]?.reflect) { this.reflectProperty({ property, newValue, newValueString }); } diff --git a/test/unit/vanilla-renderer.test.js b/test/unit/vanilla-renderer.test.js index b00561c..6425f4c 100644 --- a/test/unit/vanilla-renderer.test.js +++ b/test/unit/vanilla-renderer.test.js @@ -182,7 +182,7 @@ customElements.define('one-prop-tag', OnePropTag); class NestedElementTag extends TemplateElement { properties() { return { - label: 'empty', + label: 'initial', }; } @@ -413,14 +413,36 @@ describe(`vanilla-renderer`, () => { await nextFrame(); await nextFrame(); assert.equal(arrayElement.innerText, 'label2 label3 label4'); - arrayElement.list = [69]; + arrayElement.list = [5]; + + await nextFrame(); + await nextFrame(); + assert.equal(arrayElement.innerText, 'label5'); + }); + + it.only('Renders attribute values even after being reconnected to the DOM', async () => { + const el = await fixture(``); await nextFrame(); + + assert.equal(el.innerText, 'test'); + el.label = 'test2'; + await nextFrame(); - assert.equal(arrayElement.innerText, 'label69'); + assert.equal(el.innerText, 'test2'); + const parentNode = el.parentNode; + parentNode.removeChild(el); + await nextFrame(); + el.setAttribute('label', 'test3'); + // Reattach the element to its parent node await nextFrame(); + parentNode.appendChild(el); await nextFrame(); - console.log('####ffoooo'); + assert.equal( + stripCommentMarkers(el.outerHTML), + '', + ); + assert.equal(el.innerText, 'test3'); }); }); From 8c2f246e6728f3eeeba24a4cad357ca5c7889293 Mon Sep 17 00:00:00 2001 From: Markus Hesper Date: Wed, 3 Sep 2025 16:34:57 +0200 Subject: [PATCH 3/3] fix(ejs): rm only --- test/unit/vanilla-renderer.test.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/unit/vanilla-renderer.test.js b/test/unit/vanilla-renderer.test.js index 6425f4c..d609bb2 100644 --- a/test/unit/vanilla-renderer.test.js +++ b/test/unit/vanilla-renderer.test.js @@ -420,7 +420,7 @@ describe(`vanilla-renderer`, () => { assert.equal(arrayElement.innerText, 'label5'); }); - it.only('Renders attribute values even after being reconnected to the DOM', async () => { + it('Renders attribute values even after being reconnected to the DOM', async () => { const el = await fixture(``); await nextFrame();