Or: How to generate dynamic html tags inside lit-html templates?
eval is sometimes mixed up with
evil. We also hear sometimes that there are cases in which it is needed. This is one such case…
Notice: this article is for an old version of
lit. I was notified in the comments by Justin that in the new
litversions there is better support for static expressions.
As I mention in the article, it did not work at the time and I came up with this solution.
This solution is also not performant when compared to other solutions. It did leave our code cleaner and overall performance in the app was not harmed so this is what we used.
Since we are not using
vivid 3.xthis is considered legacy code in our project.
lit project is a framework for working with web components. In vivid 1.x and 2.x we relied heavily on
lit. While we are working hard on vivid 3.x, we still support and add features to
One such feature was to add an H tag around the expansion panel component. Because expansion panels can be used as headers (and mostly they do), we need to set them up semantically as headers.
We’ve decided the default H level to be 3 (
<h3>). That’s not enough because the expansion panel can also be a sub header of any other header level:
<h1>Is there alien life out there?</h1>
<h2><button>What is life?</button></h2>
<h2><button>What is alien?</button></h2>
So you see – setting up
h3 in this case would be wrong. The same is true for expansion panels under
Some would say to use a
role=header. While this would have worked, there’s this quote from MDN (and other places):
Taking all this into account, we wanted to keep the MDN’s best practices while still allowing for dynamic tags.
Table of Contents
Trying to setup a dynamic tag with lit-html
How do lit templates work?
Let’s first explain how templating works in
lit without getting too much into details.
lit-html is a utility function that turns our strings into live templates. The
lit render method then “magically” listens to changes in properties in the template and updates the view accordingly.
So you’ll have a
render function that looks kind of like this:
The panel header would be the content we’d like to designate as a header.
As I mentioned, we wanted to wrap it with
h3 as default, so it might look something like this:
How to not dynamically change a tag name in lit-html?
The second requirement was to have the
h tag dynamic. That means, we’d like to have a user editable property that will change the header’s level. Looking at the template, this might seem an easy task. Just set the variable inside the template string near the
How wonderful it looks! And how spectacularly it fails 🙂
You see –
lit-html and templating system does not work well with static data. I’ll spare you the creepy details, but the error that we get is that it fails to parse the template – and this happens in runtime! (ouch – imagine what would have happened if we didn’t have tests)
Trying to use
You know what they say: “If you can’t fight them – use their methods”. A lit method called
unsafeHTML promises to do the following:
Renders the result as HTML, rather than text. Since the documentation was lacking, I turned to the official documentation – the unit tests:
Applying that to our code looks like:
Ok – build passed. Tests failed (but who looks at tests, right?). Why did they fail? Here’s how it looks like when using this solution:
I won’t go into details why this didn’t work – suffice to say that the way
lit-html renders its templates doesn’t allow for “loose” HTML tags ends.
We’re back to point zero. We tried something that didn’t work… time to move on!
Trying to use
Our third attempt was to use another built in function of
unsafeStatic. Again the documentation isn’t too helpful, so… you know… unit tests:
Seems like our problem is solved. Let’s just implement this in our code:
And then… we get this:
Apparently, while their tests worked with the
unit-tests render, it did not work in the “real world”. Running
render on my own showed me the exact same thing.
unsafeStatic doesn’t work. What’s next?
How to use
Eval in order to generate dynamic tags in lit-html templates
In our former failed attempt we tried to add the
headerLevel dynamically like this:
Let’s try to tackle this differently. Because we can call rendering functions that return
TemplateResult objects, we can create a function that returns an
TemplateResult like this:
Our solution now return a dynamic template with a dynamic tag name for our header:
Let’s bundle it all up
It seems like we succeeded in what we wanted. We have a dynamic tag inside our
lit-html – and we even used
We did find another “small” issue. While running our ui visual regression tests (that are being bundled by webpack) we got the following error:
The same error showed up in our storybook (webpack, again…).
Oh no! Now our components are not usable in bundled applications (which are like… 99% of the applications consuming Vivid).
What to do? What to do? I know – let’s get depressed and give up!
Or… we could come up with another nasty solution!
If we just set the
html as a local variable and use the local variable, it should be available in runtime.
const safeHTML = html; at the top of the file and then using
safeHTML instead of
html solves our problem.
Actually – a variable might be problematic since an uglifier/minifier might change it. We could use a static method on the class itself to make sure it is more resilient. It depends how “aggressive” your uglifier/minifier is…
Not my problem at the moment 🙂
Accessibility is an important matter. We don’t want to leave anyone behind, and make sure our apps are as accessible to as many people as possible.
Using correct semantics in your HTML is a big part of that. This is why the
vivid team is constantly working on adding accessibility features to our Design System’s components.
In our case we wanted to make sure the
expansion panels get the right semantics as headers. Because we wanted to allow the consumers to determine the header’s level, we wanted the
h tag to be dynamic.
lit-html was not really cool with that, so after trying out some ways of doing that, we found that by using
eval we can create a dynamic tag inside
We then found another small problem: when bundling our component inside other apps (in our case, the ui visual regression tests and storybook), we found out that using
eval is tricky. It wouldn’t find the imported
html since the
eval expression is evaluated in runtime and bundlers tend to give imports their own names…
We solved it by creating a local property on the class and it did the trick. We had a dynamic tag that was also working with bundlers.
The operation was a success.