Skip to content

Commit d7aa65c

Browse files
authored
fix: dispose of Lit parts after changing a renderer (#2325) (#2328)
1 parent 85ac717 commit d7aa65c

File tree

8 files changed

+122
-1
lines changed

8 files changed

+122
-1
lines changed

packages/vaadin-combo-box/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@
4444
"@vaadin/testing-helpers": "^0.2.1",
4545
"@vaadin/vaadin-dialog": "^21.0.0-beta2",
4646
"@vaadin/vaadin-template-renderer": "^21.0.0-beta2",
47+
"lit": "^2.0.0-rc.1",
4748
"sinon": "^9.2.0"
4849
},
4950
"publishConfig": {

packages/vaadin-combo-box/src/vaadin-combo-box-item.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -145,6 +145,10 @@ class ComboBoxItemElement extends ThemableMixin(DirMixin(PolymerElement)) {
145145

146146
if (this._oldRenderer !== renderer) {
147147
this.$.content.innerHTML = '';
148+
// Whenever a Lit-based renderer is used, it assigns a Lit part to the node it was rendered into.
149+
// When clearing the rendered content, this part needs to be manually disposed of.
150+
// Otherwise, using a Lit-based renderer on the same node will throw an exception or render nothing afterward.
151+
delete this.$.content._$litPart$;
148152
}
149153

150154
if (renderer) {

packages/vaadin-combo-box/test/item-renderer.test.js

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -123,4 +123,10 @@ describe('item renderer', () => {
123123

124124
expect(getFirstItem().$.content.textContent).to.equal('foo');
125125
});
126+
127+
it('should clear the old content after assigning a new renderer', () => {
128+
comboBox.opened = true;
129+
comboBox.renderer = () => {};
130+
expect(getFirstItem().$.content.textContent).to.equal('');
131+
});
126132
});
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
import { expect } from '@esm-bundle/chai';
2+
import { fixtureSync } from '@vaadin/testing-helpers';
3+
import { html, render } from 'lit';
4+
import '../vaadin-combo-box.js';
5+
6+
describe('lit', () => {
7+
describe('renderer', () => {
8+
let comboBox;
9+
10+
function getFirstItem() {
11+
return comboBox.$.overlay._selector.querySelector('vaadin-combo-box-item');
12+
}
13+
14+
beforeEach(() => {
15+
comboBox = fixtureSync(`<vaadin-combo-box></vaadin-combo-box>`);
16+
17+
const size = 100;
18+
19+
comboBox.items = new Array(size).fill().map((e, i) => {
20+
return { value: `value-${i}` };
21+
});
22+
23+
comboBox.renderer = (root, _, { index }) => {
24+
render(html`value-${index}`, root);
25+
};
26+
});
27+
28+
it('should render the content', () => {
29+
comboBox.opened = true;
30+
expect(getFirstItem().$.content.textContent).to.equal('value-0');
31+
});
32+
33+
it('should render new content after assigning a new renderer', () => {
34+
comboBox.opened = true;
35+
comboBox.renderer = (root, _, { index }) => {
36+
render(html`new-${index}`, root);
37+
};
38+
expect(getFirstItem().$.content.textContent).to.equal('new-0');
39+
});
40+
});
41+
});

packages/vaadin-virtual-list/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@
3434
"devDependencies": {
3535
"@esm-bundle/chai": "^4.1.5",
3636
"@vaadin/testing-helpers": "^0.2.1",
37+
"lit": "^2.0.0-rc.1",
3738
"sinon": "^9.2.4"
3839
},
3940
"publishConfig": {

packages/vaadin-virtual-list/src/vaadin-virtual-list.js

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -127,14 +127,36 @@ class VirtualListElement extends ElementMixin(ThemableMixin(PolymerElement)) {
127127

128128
/** @private */
129129
__updateElement(el, index) {
130+
if (el.__renderer !== this.renderer) {
131+
el.__renderer = this.renderer;
132+
this.__clearRenderTargetContent(el);
133+
}
134+
130135
if (this.renderer) {
131136
this.renderer(el, this, { item: this.items[index], index });
132137
}
133138
}
134139

140+
/**
141+
* Clears the content of a render target.
142+
* @private
143+
*/
144+
__clearRenderTargetContent(element) {
145+
element.innerHTML = '';
146+
// Whenever a Lit-based renderer is used, it assigns a Lit part to the node it was rendered into.
147+
// When clearing the rendered content, this part needs to be manually disposed of.
148+
// Otherwise, using a Lit-based renderer on the same node will throw an exception or render nothing afterward.
149+
delete element._$litPart$;
150+
}
151+
135152
/** @private */
136153
__itemsOrRendererChanged(items = [], renderer, virtualizer) {
137-
if (renderer && virtualizer) {
154+
// If the renderer is removed but there are elements created by
155+
// a previous renderer, we need to request an update from the virtualizer
156+
// to get the already existing elements properly cleared.
157+
const hasRenderedItems = this.childElementCount > 0;
158+
159+
if ((renderer || hasRenderedItems) && virtualizer) {
138160
if (items.length === virtualizer.size) {
139161
virtualizer.update();
140162
} else {
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
import { expect } from '@esm-bundle/chai';
2+
import { fixtureSync } from '@vaadin/testing-helpers';
3+
import { html, render } from 'lit';
4+
import '../vaadin-virtual-list.js';
5+
6+
describe('lit', () => {
7+
describe('renderer', () => {
8+
let list;
9+
10+
beforeEach(() => {
11+
list = fixtureSync(`<vaadin-virtual-list></vaadin-virtual-list>`);
12+
13+
const size = 100;
14+
15+
list.items = new Array(size).fill().map((e, i) => {
16+
return { value: `value-${i}` };
17+
});
18+
19+
list.renderer = (root, _, { index }) => {
20+
render(html`value-${index}`, root);
21+
};
22+
});
23+
24+
it('should render the content', () => {
25+
expect(list.children[0].textContent.trim()).to.equal('value-0');
26+
});
27+
28+
it('should render new content after assigning a new renderer', () => {
29+
list.renderer = (root, _, { index }) => {
30+
render(html`new-${index}`, root);
31+
};
32+
33+
expect(list.children[0].textContent.trim()).to.equal('new-0');
34+
});
35+
});
36+
});

packages/vaadin-virtual-list/test/virtual-list.test.js

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -100,5 +100,15 @@ describe('virtual-list', () => {
100100
const itemRect = item.getBoundingClientRect();
101101
expect(list.getBoundingClientRect().bottom).to.be.within(itemRect.top, itemRect.bottom);
102102
});
103+
104+
it('should clear the old content after assigning a new renderer', () => {
105+
list.renderer = () => {};
106+
expect(list.children[0].textContent.trim()).to.equal('');
107+
});
108+
109+
it('should clear the old content after removing the renderer', () => {
110+
list.renderer = null;
111+
expect(list.children[0].textContent.trim()).to.equal('');
112+
});
103113
});
104114
});

0 commit comments

Comments
 (0)