DOM elements that should disappear after LiveView update remain

I am observing the weird but reproducible behavior in LiveView that a DOM element which should disappear after an LiveView update still remain. I don’t have a minimal example, yet.

While writing this post I had an idea about a possible workaround, which turned out to be working. So I am posting this in the hope that people who observe the same thing will find it.

Here is just what I observe.

I have a LiveView with as snippet similar to this in the heex. A simple loop over a list of items rendering checkboxes to check or uncheck the items.

<%= for {item, checked} <- @shown_items do %>
    <div class="shown-item">
        <input {idfy("item", item.id)} value={item.id} checked={checked}/>
        <label for={idfy("item", item.id)}>
            <%= item.name %>
        </label>
    </div>
<% end %>

The @shown_items list changes with user interaction. The user types into an input box and only items that match the search string will appear, along with those that are already checked.

Under some circumstances which are reproducible in the browser but not in unit tests one of the <input> tags remains. The html copied from the browser’s debugger looks like this:

<div class="shown-item">
    <input id="item-3" type="checkbox" name="item[]" value="3" checked="">
    <label for="item-3">
        Item-3 name
    </label>
</div>

<div class="shown-item">
    <input id="item-1" type="checkbox" name="item[]" value="1"><input id="item-3" type="checkbox" name="item[]" value="3">
    <label for="item-1">
        Item-1 name
    </label>
</div>

<div class="shown-item">
    <input id="item-10" type="checkbox" name="item[]" value="10">
    <label for="item-10">
        Item-10 name
    </label>
</div>

In the second div.shown-item there is a input#item-3 which should not be there. It belongs in the first div.shown-item where it also correctly shows up. A copy remains in the second.

Workaround / solution

What seems to work around this is to give every div.shown-item a unique DOM id. I changed the heex code to

<%= for {item, checked} <- @shown_items do %>
    <div class="shown-item" id={idfy("shown-item", item.id)}>
        <input {idfy("item", item.id) value={item.id} checked={checked}/>
       <label for={idfy("item", item.id)}>
            <%= item.name %>
        </label>
    </div>
<% end %>

The idfy function looks like

def idfy(string, id), do: string <> "-#{id}"

:wave:

Might be related: LiveComponent Conditional Rendering Issue Using a For Comprehension - #4 by ruslandoga

tl;dr morphdom uses node id for keying the element and if it’s missing, it’s doing non-keyed updates (i.e. instead of moving the elements, it’s adding or removing the difference).