flow-components
flow-components copied to clipboard
[LitRenderer] Provide "import" api or autoimport 'nothing' to allow explicit prevent rendering of attributes or similar if necessary
Describe your motivation
LitRenderer allows us to render content in grid columns with very good performance compared to the ComponentRenderer. Unfortunately we only can provide the template without anything else like imports. The automatic imported Lit functionality and elements is relatively limited, for instance we can use html but not much more.
Using inline expressions for attributes in lit results in most cases in the attribute rendered, regardless of its content.
An expression like <a target=${item.openInNewTab ? "_blank" : ""} ... results in either <a target="_blank" ... or <a target ..., where the latter is theoritcally wrong. Using undefined instead of an empty string results in the same outcome. While it may work in Browsers, it still can lead to issues when having css selectors (e.g a:not([target])).
The official way of Lit to prevent attributes or other parts from being rendererd is the usage of the special element nothing or the ifDefined directive (https://lit.dev/docs/templates/conditionals/). Unfortunately, both are not available for the LitRenderer.
Follow up ticket for https://github.com/vaadin/flow-components/issues/2753
Describe the solution you'd like
Variant 1:
Provide an import mechanism to define imports on the server side. An easy variant would be something like
LitRenderer.of(...)
.importFromLit("nothing") // results in "import {nothing} from 'lit';"
.importDirectiveFromLit("ifDefined") // results in "import {ifDefined} from 'lit/directives/if-defined.js';"
.withProperty();
Not sure if variant 1 is technically doable without bringing in potential security risks. I guess an alternative here would be to have some kind of map in the background or an enum plus interface for the things, that can be imported, e.g.
LitRenderer.of(...)
.import(Lit.NOTHING) // results in "import {nothing} from 'lit';"
.import(Lit.Directive.IF_DEFINED) // results in "import {ifDefined} from 'lit/directives/if-defined.js';"
.withProperty();
In this case the import would take an instance of something like LitRendererImportDefinition, for which we could provide a predefined set of imports via an enum. Devs could also implement that interface and thus provide a way of clearly define custom imports but with some restrictions.
interface LitRendererImportDefinition {
// replace method names with something that makes sense for JS imports, I'm just a Java Dev :D
String getElement();
String getNpmPackage();
}
Flow then takes care of combining import definitions to valid JS imports.
Variant 2:
Provide at least the nothing element, as it has a huge impact on the rendering process and has a high chance of being used often.
Describe alternatives you've considered
Exporting the lit renderer template to a function in a JS module, where I can handle the imports myself. I then assign that to some global window variable and use that function instead in my renderer.
// simplified example
import {nothing, html} from 'lit';
window.Vaadin.Flow._custom_things = {
anchor(item) {
return html`<a target=${item.openInNewTab ? "_blank" : nothing}>...`
}
}
@JsModule("./grid-lit-renderers.js");
...
LitRenderer.of("${window.Vaadin.Flow._custom_things.anchor(item)}");
This solution works without issues, but it might be a bit overhead when having simple templates, that need additional directives or the nothing value.
Additional context
No response
One challenge with Variant 1 is the way the production build needs to know which imports will be used so that it can know what to include in the frontend bundle. This means that the design would have to be based on annotations or something else that can be directly extracted from bytecode. This is doable but also a relatively big design and development effort.
I would thus recommend going with Variant 2 even though that's a more limited approach.