Search code examples
javascriptsvelteinnerhtml

Why is Svelte's bind:innerHTML retaining an outdated string?


Content is displaying correctly, bind:innerHTML isn't tracking changes

The content of a div containing two {#each} blocks is bound to a variable via bind:innerHTML. These {#each} blocks are responsive to user input and dynamically render according to variable changes; this is already functional and the component itself displays changes correctly.

However, the variable (svgDivContentString) does not update as the contents of the component change. It simply retains the initial information from when the component was first mounted. No matter how the contents vary, it goes unchanged. Even if it's emptied manually via svgDivContentString = "", it just resets to the default info when the {#each} block triggers again (regardless of what's actually being displayed). The string is needed for conversion to base64 for use as an img.src.

<div
bind:innerHTML={svgDivContentString}
contenteditable
id="kbContainer"
>
    <svg
    id="svgContainer"
    viewBox="0 0 {$width} {height}"
    xmlns="http://www.w3.org/2000/svg"
    xmlns:xlink="http://www.w3.org/1999/xlink"
    >
    {#each allNotes as note, index (index)}
        ...
    {/each}

Attempted solutions/workarounds

I tried emptying the string manually (as seen above) and ensuring that only new information was being introduced, but somehow the same default info ends up in the string every time. I have also tried going a "less Svelte" way and just using document.getElementById on the div, but that returns undefined. It's also "not very Svelte," which is often said in response to attempted JS workarounds for Svelte problems on SO posts.

Would like to reiterate that it is not a problem with the {#each} block. The rendered content is correctly updated every time. The problem is that bind:innerHTML isn't getting that memo for some reason.

Any guidance here or proposals for alternative means to the same end would be greatly appreciated.


Solution

  • If you just want to read the HTML, you don't necessarily need to do it this way.

    The reason why contenteditable is required for the binding, is that all bindings rely on certain events to fire which then trigger the bound variable to be updated. Unless the there is contenteditable there will be no editing and no editing means no input events.

    Generally when you need to access the DOM directly, the first thing to consider is an action, this gives you a reference to the node and it executes on the creation of the element.

    If you need a more persistent reference, you can use bind:this on the element instead. This sounds like an application for that. Then whenever you need to read the HTML of the element you will have a local DOM element reference.

    To trigger your logic, you will need to rely on other events or use reactive statements that depend on whatever is added to that element.

    E.g.

    let container; // bind:this variable
    $: if (container) {
      allNotes; // dependency
      const html = container.innerHTML;
      // [do something with it]
    }