Can eval be good?

A good case for Eval in JavaScript

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 lit versions 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 lit anymore in vivid 3.x this is considered legacy code in our project.

The 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 2.x.

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.

The expansion panel before the change. Closed state (top) and open state (bottom). You can see in the green area the button that’s not wrapped in any indication it is a header.

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>
<expansion-panel>
  <h2><button>What is life?</button></h2>
</expansion-panel>
<expansion-panel>
  <h2><button>What is alien?</button></h2>
</expansion-panel>

So you see – setting up h3 in this case would be wrong. The same is true for expansion panels under h3, h4 and h5.

Some would say to use a div with aria-level and role=header. While this would have worked, there’s this quote from MDN (and other places):

A friendly suggestion from MDN regarding the usage (or non-usage) of aria-level

Taking all this into account, we wanted to keep the MDN’s best practices while still allowing for dynamic tags.

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:

A method render on the custom element. It returns a TemplateResult using the html lit function.

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:

Same render function – only this time the header is wrapped with an h3 tag

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 h:

Adding ${this.headerLevel} near the h tag

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 unsafeHTML

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:

Part of the unsafeHTML unit test that shows how to use it. It seems we can actually parse anything as HTML.

Applying that to our code looks like:

Applying unsafeHTML in our code

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:

The DOM after applying unsafeHTML. You can see all it did was generate an orphaned h3 tag instead of wrapping our button with h3 tag. That’s mostly why the tests failed – because they expected the button to be wrapped with h3… post in the comments below if you’d like to see how I tdded this one 🙂

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 unsafeStatic

Our third attempt was to use another built in function of lit called unsafeStatic. Again the documentation isn’t too helpful, so… you know… unit tests:

The test where the unsafeHTML does exactly what we need!

Seems like our problem is solved. Let’s just implement this in our code:

Adding the unsafeStatic to the mix just like in the docs and unit tests of the library.

And then… we get this:

Instead of a beautiful expansion panel, we get something odd that’s yelling object object!
Hey Element – next time you yell object no one will believe you!

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.

So… 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:

<h${this.headerLevel}>
	${this.renderPanelHeader()}
</h${this.headerLevel}>

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 evaled TemplateResult like this:

Our new renderPanelHeader method. It first verifies that our input is correct (we don’t want someone injecting stuff into our code). Then it returns the evaluated value of running the string inside the eval call.

Our solution now return a dynamic template with a dynamic tag name for our header:

On the left side – our greeting panel. On the right side – the HTML structure of the panel with a dynamic header. You can view an animation here:
Animation showing the dynamic change of a tag name in a lit-element

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 eval

We did find another “small” issue. While running our ui visual regression tests (that are being bundled by webpack) we got the following error:

An error seen in runtime when using the imported html function inside an eval

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.

So adding 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 🙂

Summary

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 lit-html.

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.

Thanks a lot to Rachel B. Tannenbaum and Yinon Oved for the inspiration and review of this article.

Sign up to my newsletter to enjoy more content:

0 0 votes
Article Rating
Subscribe
Notify of
guest

2 Comments
Oldest
Newest Most Voted
Inline Feedbacks
View all comments
Justin
2 years ago

I would use static expressions for this instead: https://lit.dev/docs/templates/expressions/#static-expressions