0

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!

  • 2
    I think as long as the div mentioned isn't affected by the observed properties in any way it should not be rerendered and keep whatever changes you made to it manually, if you could provide some minimal reproduction of your problem we can get to see what may be causing the rerender in your case – Alan Dávalos Jan 06 '21 at 09:11
  • As a note on a separte issue that's intertwined -- using Popper with Bootstrap, I've found it helpful to remove all poppers with `$().popper('destroy')` in `update(changedProperties)` and then re-add them un `updated(changedProperties)` to keep the DOM at the time of update in sync w/ what lit-html thinks the DOM is. – Michael Scott Asato Cuthbert Jan 21 '21 at 10:15

3 Answers3

1

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.

Keith
  • 150,284
  • 78
  • 298
  • 434
  • Thanks @Keith. I ended up rewriting the whole editor in Lit. I’m hoping my project will give permission to let me open source this since it’s not a key “secret sauce” over our competitors. – Michael Scott Asato Cuthbert Sep 03 '22 at 08:34
1

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)
danr
  • 2,405
  • 23
  • 24
  • This works for many scenarios, but with DOM reuse, if lit-element/lit-html does not know that the target_div has been altered, it can reuse the
    DOM for unrelated content when the page changes, with its contents now being out of sync with the actual contents. This was the strategy I was using that caused problems.
    – Michael Scott Asato Cuthbert Jan 25 '21 at 18:34
1

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>`;
  }
}
hunterloftis
  • 13,386
  • 5
  • 48
  • 50