Need to document any difficulties that you may encounter while using selects with a non-static (potentially changing, potentially loaded from an api) list of items.
For all of the examples below, imagine that we have a basic component that loops over an array, someProperty
, to produce a list of <vaadin-items>
class MyElement extends LitElement {
@property someProperty: String[];
render(): TemplateResult {
return html`...`;
}
renderItems: TemplateResult[] {
return someProperty.map(item => {
return html`<vaadin-item>item</vaadin-item>`;
});
}
}
Issue 1 - Binding render FNs
Typically, our goal will be to bind a render FN once and only once in the constructor. Binding to "this" is needed in some cases where render fns must consult / access the host element.
private boundSelectRenderer: Function;
constructor() {
super();
this.boundSelectRenderer = (root) => this.selectRenderer.(root);
}
render(): TemplateResult {
return html`<cpsi-select .renderer="${this.boundSelectRenderer}">`
}
selectRenderer(root: HTMLElement): void {
render(html`<vaadin-list-box>${this.renderItems()}</vaadin-list-box>`, root);
}
However, this can cause the select to "flicker" and force itself to close on the first render
![bind-renderer-once-only](https://user-images.githubusercontent.com/1993389/102401951-ae3a2080-3fa9-11eb-90e2-16ee06a93174.gif)
Instead, at odds with our general goal of binding once, here we have to bind at least each time the properties that impact the render FN are changed. I think we could bind in every render cycle, but that would be wasteful. We can use the lit-html guard directive to only bind the renderer when certain properties that impact rendering are changed
render(): TemplateResult {
return html`
<cpsi-select
.renderer="${guard([this.someProperty], () => (root) => this.selectRenderer(root))}">
`;
}
selectRenderer(root: HTMLElement): void {
render(html`<vaadin-list-box>${this.renderItems()}</vaadin-list-box>`, root);
}
Issue 2 - implementation details of render fn
Bad impl -- simple render into root
Ideally, our renderer would be quite simple... something like this:
private selectRenderer(root: HTMLElement): void {
const listbox: TemplateResult = html`
<vaadin-list-box>${this.renderItems()}</vaadin-list-box>
`;
render(listbox, root);
}
Problem(s) with this impl
- any updates to items / properties relevant to rendering produces an empty list
![render-root-lit](https://user-images.githubusercontent.com/1993389/102402033-d32e9380-3fa9-11eb-9372-0449412ac006.gif)
Bad impl -- conditional render into root or existing listbox
Previously, some research led me to conclude that you must conditionally render into root: if a listbox was previously rendered into root, then you must render into the listbox. So we can modify the example above a bit to have an if-else
private selectRenderer(root: HTMLElement): void {
if (root.firstElementChild) {
// render items into existing listbox
const listbox: HTMLElement = root.firstElementChild as HTMLElement;
render(this.renderItems(), listbox);
} else {
// render listbox and items into root
const listbox: TemplateResult = html`
<vaadin-list-box>${this.renderItems()}</vaadin-list-box>
`;
render(listbox, root);
}
}
Problem(s) with this impl
- flicker on initial opening
- any updates to items / properties relevant to rendering produces an empty list
![conditional-render-root-lit](https://user-images.githubusercontent.com/1993389/102402066-dde92880-3fa9-11eb-95af-f0d9d78d8484.gif)
Good impl
The successful implementation has none of the problems from the previous example, but it is effectively the same. I'm not quite sure why this has none of the issues from the previous example - in theory they should be similar. I expect that some lit-html caching is playing a role. In this example, we always renderItems into the listbox and - if needed, the listbox is appended to root. We re-use previously created listbox instance
private selectRenderer(root: HTMLElement): void {
if (root.firstElementChild) {
const listbox: HTMLElement = root.firstElementChild as HTMLElement;
render(this.renderItems(), listbox);
} else {
const listbox: HTMLElement = document.createElement('vaadin-list-box');
render(this.renderItems(), listbox);
root.appendChild(listbox);
}
}
![no-guard-items](https://user-images.githubusercontent.com/1993389/102402132-f2c5bc00-3fa9-11eb-99ee-63c0823d46a9.gif)
This works, but here too we will make use of the guard directive to avoid mapping items via renderItems when we don't need to.. you can note in the gif below that extra rendering of items have been reduced. lit-html is intelligent about dom updates.. so just because we rendered / mapped items into TemplateResults - that does not mean dom updates will actually be made. This is just a micro-optimization
private selectRenderer(root: HTMLElement): void {
if (root.firstElementChild) {
gaurd([this.someProperty], () => {
const listbox: HTMLElement = root.firstElementChild as HTMLElement;
render(this.renderItems(), listbox);
});
} else {
// initial listbox should always have items rendered into it
const listbox: HTMLElement = document.createElement('vaadin-list-box');
render(this.renderItems(), listbox);
root.appendChild(listbox);
}
}
![guard-items](https://user-images.githubusercontent.com/1993389/102402153-f9543380-3fa9-11eb-9e13-1bee823acb8f.gif)
Summary
Putting it all together --
- Bind renderer in each render cycle - perhaps optimize via
guard
- Selectively render either into
root
or root.firstElementChild
- Do not render into root via lit-html - render into a listbox and use dom apis to append the listbox to root as needed