Prevent DOM reuse within lit-html/lit-element

1.3k views Asked by At

I am looking for a way to NOT reuse DOM elements within lit-html/lit-element (yes, I know, I'm turning off one of the prime features). The particular scenario is moving an existing system to lit-element/lit-html that at certain points embeds the trumbowyg WYSIWYG editor. This editor attaches itself to a <div> tag made within lit-element and modifies its own internal DOM, but of course lit-html does not know that this has happened, so it will often reuse the same <div> tag instead of creating a new one. I am looking for something similar to the vue.js key attribute (e.g., preventing Vue from aggresively reusing dom-elements)

I feel like the live() directive in lit-html should be useful for this, but that guards against reuse based on a given attribute, and I want to prevent reuse even if all attributes are identical. Thanks!

3

There are 3 answers

1
Keith On BEST ANSWER

I have had similar issues with rich text editors and contenteditable - due to how templates update the DOM you don't want that to be part of a template.

You do this by adding a new element with the non-Lit DOM and then adding that to the DOM that Lit does manage:

class TrumbowygEditor
  extends HTMLElement {

  constructor() {
    super();
    const shadow = this.attachShadow({mode: 'open'});
    const div = document.createElement('div');
    shadow.appendChild(div);
    
    const style = document.createElement('style');
    // Add CSS required 
    shadow.appendChild(style);

    $(div).trumbowyg(); //init
  }
}

customElements.define('trumbowyg-editor', TrumbowygEditor);

As this is running in a custom element's shadow DOM Lit won't touch it, you can do:

html`
    <div>Lit managed DOM</div>
    <trumbowyg-editor></trumbowyg-editor>`;

However, you will have to implement properties and events on TrumbowygEditor to add everything you want to pass to or get from the nested jQuery component.

You can add the scripts with import if you can get module versions of jQuery/Trumbowyg (or your build tools support it) or you can add <script> tags to your component, add fallback loading DOM content in the constructor, and then on the load event of the <script> call the $(div).trumbowyg() to init the component.

While messier and more work I'd recommend the latter as both components are large and (thanks to jQuery being built on assumptions that are now 15 years old) need to load synchronously (<script async or <script defer don't work). Especially on slower connections Lit will be ready long before jQuery/Trumbowyg have loaded in, so you want <trumbowyg-editor> to look good (show spinner, layout in the right amount of space etc) while that's happening.

1
danr On

You write that you attach the external library directly to an element managed by lit-html. It sounds like you're doing essentially this:

render(html`<section><div id=target></div></section>`, document.body)
external_lib.render_to(document.querySelector("#target"))

If this is what you do instead try to create your own div, let the external lib render to that div, and finally attach that div to lit-html:

let target_div = document.createElement('div')
render(html`<section>${div}</section>`, document.body)
external_lib.render_to(target_div)
2
hunterloftis On

The most up-to-date answer to this problem is to use Lit's built-in keyed directive. This scenario is exactly what it's for:

https://lit.dev/docs/templates/directives/#keyed

Associates a renderable value with a unique key. When the key changes, the previous DOM is removed and disposed before rendering the next value, even if the value—such as a template—is the same.

@customElement('my-element')
class MyElement extends LitElement {

  @property()
  userId: string = '';

  render() {
    return html`
      <div>
        ${keyed(this.userId, html`<user-card .userId=${this.userId}></user-card>`)}
      </div>`;
  }
}