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 d0cb513..d609bb2 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: 'initial', + }; + } + + 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,49 @@ 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 = [5]; + + await nextFrame(); + await nextFrame(); + assert.equal(arrayElement.innerText, 'label5'); + }); + + it('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(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(); + + assert.equal( + stripCommentMarkers(el.outerHTML), + '', + ); + assert.equal(el.innerText, 'test3'); + }); });