Lit-html: Allow injection of static content into templates

Created on 29 Aug 2017  路  36Comments  路  Source: Polymer/lit-html

As discussed in this thread, it would be useful to have some way of injecting static content into the templates that would be used at compile time of the templates, but not change per-render.

Potential syntax that come out of that thread would be using a utility function to demarcate static content:

import { html, unsafeStatic } from 'lit-html'
import SomeElement from 'some-element';

const elementName = unsafeStatic(SomeElement.is);
let dynamicTagTemplate = html`<${elementName}>Hello</${elementName}>`

/cc @justinfagnani let me know if I misinterpreted that syntax you suggested

High Enhancement Enable new use cases lit-next

Most helpful comment

Recently in PR #568 I suggested a new approach, "extensions". It assumes that you can decorate your html function and process your strings and values arrays before they comes to html. Initially it was made for creating static values, but I believe the potential of this approach is beyond just this purpose.

What benefites this approach brings in my opinion? Well, that's kinda runtime preprocessor for html function that can adjust template and variables send to it to solve some basic inabilities that html possess due to its origin. Since html uses templates under the hood, we are unable to use values that change the HTML tags and attributes because templates are HTML and they will become invalid if we do this. That's why static values are not implemented yet. But using prepocessor we can do a lot of things we cannot do by default.

There are some possible usage cases that could be solved by using preprocessors:

  • Static values. No additional description is necessary.
  • Dynamic static values. With current implementation I propose in #568 we are unable to use React-style approach to change tag's name depending on some condition, because they simply would ignore. Take a look:
// This is what we would like to have
html`<${isHeader ? 'header' : 'section'}>Some heading</${isHeader ? 'header' : 'section'}>`;

// but now we're forced to use this approach
isHeader ? html`<header>Some heading</header>` : html`<section>Some heading</section>`;

I believe with extensions we could solve this problem as well as we can work with static values.

  • Processing some values in some places. E.g. we have a LitElement with static is attribute which contains its element name. Specific extension combined with unsafeStatic would allow us to use following code:
const lhtml = withLitElement(withUnsafeStatic(html));

lhtml`<${MyElement}></${MyElement}>`;

Very simple and clean.

I believe there is a lot of use cases. E.g. it may be possible to even use pug to create proper lit-html templates.

There is one blocker that still exist. Even if Microsoft/ChakraCore#5201 is merged, it is not necessarily included in Edge 18. And it is a major blocker because caching that is one of the most important part of "extension" won't work in this browser. However, it is a matter of time, and then the idea could be implemented.

What do you think about this approach?

All 36 comments

The syntax is right.

The idea here is that static values would be wrapped in some sentinel object that's scanned for when building the template HTML, and skipped for updates. This would allow for values in positions like tag names and attribute names, and useful for content in situations where is should never change like building up static CSS. Since static values should never change, we can check them on updates and issue a warning if they do change.

just quickly looking at the source. For this to work we would need to pass values into Template then during _getHtml walk values and strings in order to determine where to merge the static value before the browser parses the template via innerHTML.

Any movement on this one. If we are going to have tooling that loads our styles into a string constant for example, I believe we will need something like this to be able to embed this styling into our component template.

const styles = `.host { display: block; border: 1px solid red }`;
const styleBlock = html`<style>${styles}</style>`;

Problem: the template will contain extra quotes that break the style block.

@nborelli that's a valid point. I think a static() directive as discussed in #217 might solve both your use case and @bedeoverend's original issue. We've had to work around it like this in the Skate website. I'm thinking with static(), one could just do <style>${static(css)}</style>.

@treshugart FYI I have a PR for this now at #274 including a test that it works for parameterizing tag names.

For tag names I'd recommend wrapping unsafeStatic to validate that the value is a valid tag name.

Update:

This feature is a lot easier with the Template Literal Revision in place: https://github.com/tc39/ecma262/pull/890

All compilers I've tested, and all browsers except Edge implement the new correct behavior. The issue for Chakra Core is here: https://github.com/Microsoft/ChakraCore/issues/5201

I suggest we wait until we can rely on the per-site caching behavior to implement static.

@justinfagnani Will it be possible to implement those semantics on browsers that don't implement the new behavior? These browser versions will be with us for a while.

@LarsDenBakker compiling out template literals will work. I hope Edge gets the update in the next release, and then maybe it's about one more release until we can ship it.

All compilers I've tested, and all browsers except Edge implement the new correct behavior. The issue for Chakra Core is here: Microsoft/ChakraCore#5201

I suggest we wait until we can rely on the per-site caching behavior to implement static.

Looks like that PR was merged a few days ago, would it be a good moment now to implement static ?

Recently in PR #568 I suggested a new approach, "extensions". It assumes that you can decorate your html function and process your strings and values arrays before they comes to html. Initially it was made for creating static values, but I believe the potential of this approach is beyond just this purpose.

What benefites this approach brings in my opinion? Well, that's kinda runtime preprocessor for html function that can adjust template and variables send to it to solve some basic inabilities that html possess due to its origin. Since html uses templates under the hood, we are unable to use values that change the HTML tags and attributes because templates are HTML and they will become invalid if we do this. That's why static values are not implemented yet. But using prepocessor we can do a lot of things we cannot do by default.

There are some possible usage cases that could be solved by using preprocessors:

  • Static values. No additional description is necessary.
  • Dynamic static values. With current implementation I propose in #568 we are unable to use React-style approach to change tag's name depending on some condition, because they simply would ignore. Take a look:
// This is what we would like to have
html`<${isHeader ? 'header' : 'section'}>Some heading</${isHeader ? 'header' : 'section'}>`;

// but now we're forced to use this approach
isHeader ? html`<header>Some heading</header>` : html`<section>Some heading</section>`;

I believe with extensions we could solve this problem as well as we can work with static values.

  • Processing some values in some places. E.g. we have a LitElement with static is attribute which contains its element name. Specific extension combined with unsafeStatic would allow us to use following code:
const lhtml = withLitElement(withUnsafeStatic(html));

lhtml`<${MyElement}></${MyElement}>`;

Very simple and clean.

I believe there is a lot of use cases. E.g. it may be possible to even use pug to create proper lit-html templates.

There is one blocker that still exist. Even if Microsoft/ChakraCore#5201 is merged, it is not necessarily included in Edge 18. And it is a major blocker because caching that is one of the most important part of "extension" won't work in this browser. However, it is a matter of time, and then the idea could be implemented.

What do you think about this approach?

If this could extend to support SVG, it would be very helpful for SVG icons that are not going to be changed once rendered.

@motss this isn't needed for SVG - just include the SVG in the template. Parts of templates that don't change are already optimized.

I'm your random lit user which stumbled upon this issue and really needs it. Any idea when ? where?

@qballer, I've imlemented the extensions idea I suggested above in a @corpuscule/lit-html-renderer, withCustomElement section package. It still is a temporary solution, but works pretty well IMO.

any way to consume it as an es6 module ? I am not using a bundler.
https://unpkg.com/@corpuscule/element@0.13.0/lib/index.js

@qballer, sure, just use https://unpkg.com/@corpuscule/lit[email protected]/lib/withCustomElement

UPD Also, you may want to use any Custom Element (either third-party one or yours) as an unsafe static automatically. For that, you also have to include https://unpkg.com/@corpuscule/lit[email protected]/lib/init as the first import for the whole application, before importing any other dependency.

Any update here? Curious to know if we should plan to use @Lodin's module or if lit intends to add this extension.

Yeah, I'm curious as well. If it's out of scope for lit-html for now, I would like to publish it as a separate package to simplify access as @daKmoR suggested in #568, because my temporary solution became a bit permanent 馃檮

According to the above comments, the blocker was related to Edge, and there is going to be stable Edgium out soon

According to the above comments, the blocker was related to Edge, and there is going to be stable Edgium out soon

I believe the release date for stable Edge Chromium is January 15, 2020
https://www.zdnet.com/article/microsofts-chromium-based-edge-browser-to-be-generally-available-january-15-2020/

@justinfagnani could you clarify the policy regarding supported Edge versions from 2020 onwards?

@LarsDenBakker another use-case for a lit-helpers package 馃槵

@web-padawan we'll have to see what the numbers look like once Edge/Chromium comes out. We definitely won't be able to drop Edge/EdgeHTML support on Jan 15th, but maybe we will relatively soon after that.

https://github.com/babel/preset-modules has a specific transform for edge which I think fixes the tagged template literal issues.

@daKmoR it's interesting, but I'm a bit concerned about losing XSS safety. I'm wondering if we can scope this somehow to only tagnames.

@LarsDenBakker do you see where it fixes template literals? I don't see that mentioned.

Edit: Found the test: https://github.com/babel/preset-modules/blob/master/test/browser/index.js#L39

Ok, maybe we could recommend this for EdgeHTML

but I'm a bit concerned about losing XSS safety

This is a huge concern for us. We cannot relax XSS protections, which is why this would have to be put into an unsafeStatic directive. For context, we don't import the unsafeHTML directive into Google's internal repository, and we would do the same with unsafeStatic as part of our security policies.

https://github.com/babel/preset-modules has a specific transform for edge which I think fixes the tagged template literal issues.

@daKmoR it's interesting, but I'm a bit concerned about losing XSS safety. I'm wondering if we can scope this somehow to only tagnames.

In a library I wrote I limited it with a regex to letters and dash.

private getTemplate(component: string, attributesObject?: object): TemplateResult {
      const tagName = component.replace(/[^A-Za-z0-9-]/, '');
      let attributes = '';

      if (attributesObject) {
        attributes = Object.keys(attributesObject)
          .map((param: string): string => ` ${param}="${this.params[param]}"`)
          .join('');
      }

      const template = `<${tagName}${attributes}></${tagName}>`;

      return html`${unsafeHTML(template)}`;
    }

@daKmoR it's interesting, but I'm a bit concerned about losing XSS safety. I'm wondering if we can scope this somehow to only tagnames.

yes it would definitely need to be used via a setup function like staticTag or unsafeStaticTag or ... ? e.g. it should only allow valid html tag names 馃憤

possible usage

import { html as originalHtml } from 'lit-html';
import { html, staticTag } from '@open-wc/lit-helpers';

const tag = staticTag('my-tag');
const withStaticTag = html`<${tag}></${tag}>`;
const original = originalHtml`<my-tag></my-tag>`

withStaticTag === original
// or better render of withStaticTag === render of original

and maybe optionally unsafeStatic if really need be... don't have a use case from the top of my head 馃檲

That's really interesting, we can use this as a sort of early version of scoped custom element registries.

One related thing is lit-analyzer support, so we need to somehow map these injected tags, that can be associated with the extensions of the certain base class (i.e. in case of self defined elements) to that class so we get properties type checking etc.

BTW, I'm thinking about some babel plugin that would replace <${tag}></${tag}> with the proper element name at the transpilation stage. It would get the correct importing/exporting during the development and wouldn't affect performance as all current solutions are going to. However, for now, my only idea is to create a long sheet of class names associated with custom element names, which is possible but troublesome for the user. Probably, someone has other ideas? E.g., via the custom-element.json?

Hello,
I currently have something like that in an app :

switch(page) {
 case 'app1':
    return html`<app1 param1=${this.param1} param2=${this.param2}`
 case 'app2':
    return html`<app2 param1=${this.param1} param2=${this.param2}`
}

(with a lot more apps and a lot more params)
The idea is that all these "apps" have the same parameters, but different behaviors.

I would like to reduce duplication and do a loop instead of copying the code 10 times in a switch case.

I tried the solutions presented here but nothing works, for example https://unpkg.com/@corpuscule/lit[email protected]/lib/withCustomElement doesn't work in this case : the cache logic seems to be displaying always the same thing in my case.

Am I missing something or is it currently a problem impossible to solve with lit html ?

I am open to any new/other pattern if someone has some idea.

Edit: after looking at all the related issues, I found that https://github.com/Polymer/lit-html/issues/292#issuecomment-372733131 solves my issue (even if the solution is not very clean, but it does work)

@rom1504 A declarative template is kind of opposed to dynamic code like this. lit-html does not well support replacing tags/properties/attributes in a dynamic way. If using custom elements it would be possible to create the DOM node in JS, and add it to the template.

function tagName(page) {
  switch (page) {
    case 'app1'
      return 'x-app1';
    // ...
}

  render(page) {
    const tag = tagName();
    const el = document.createElement(tag);
    el.param1 = this.param1;
    // ...
    return html`${el}`;
  }

The above may not be pretty, but will likely work, as lit-html supports inserting DOM nodes into templates. You can decide for yourself if you want to assign to properties (el.param1 = ...) or attributes (el.setAttribute('param1', ...)).

not sure if anyone is interested, but static-params, which is library agnostic, seems to cover all use cases of this thread 馃憢

example

import {render, html} from '//unpkg.com/uhtml?module';
import {asStatic, asTag} from '//unpkg.com/static-params?module';

const shtml = asTag(html);
const elements = [
  { tag: asStatic("div"), content: "Hello" },
  { tag: asStatic("h1"), content: "World" }
];

render(
  document.body,
  html`${
    elements.map(
      ({tag, content}) =>
        shtml`<${tag}>${content}</${tag}>`
    )
  }`
);

I came across the same issue, and I wanted to programmatically select the tag name and pass properties to the element. I developed a directive which doesn't require any external dependencies: https://gist.github.com/andyvanee/0c2e379f3c976eb8ffcb9d2bfeacee64

And I've put together a little demo on codepen.

Basically, this lets you create elements in your template from a string tag name and an object for the properties:

html`${tagProps("tag-name", {props})}`

Not sure if this helps anyone here, but it solved the problem for me!

From reading the docs on directives, I'm sure it could be extended to support rendering templates for the content of the tag as well. I didn't need any of that so I haven't gone down that rabbit hole yet...

Was this page helpful?
0 / 5 - 0 ratings