Stop Elm from touching a particular DOM node

I have a content editable node that is occasionally losing the focus when other parts of the DOM are being updated.

I wrapped around the editable node using Html.Keyed which improved things but the focus loss still happens just less often. Which made me think, perhaps I need another Html.Keyed higher up. To be honest, I know roughly what Html.Keyed does, but am not completely certain when it will still allow a node to be deleted and replaced. If the parent of a keyed node is deleted and replaced, will all its keyed children also be deleted?

So now I am thinking, can I make sure that Elm never touches a node? Can I do that with Html.lazy on an Int counter, and only let Elm change the node when the counter is bumped? I am a bit unclear on what things Elm will consider as referentially equal? The same 2 Ints?

onlyWhenCountChanges : Model -> Html Msg
onlyWhenCountChanges model =
    Html.lazy 
        (\_ -> editableDiv model)
        model.count

Of course the focus loss might be some other browser bug/crazy thing and nothing to do with Elm. The above might enable me to rule out Elm virtual DOM as the cause?

In my (very limited) experience and understanding, if you want to make sure the Keyed node stays in the document, it needs to be attached to the root of the mount point (directly or through a stable chain of parents).
Keyed will reuse existing DOM elements when it thinks they’re “the same” (using the key for comparison), but if your view function states the element should not be there at all, it won’t be.

If the parent of a keyed node is deleted and replaced, will all its keyed children also be deleted?
Yes. It’s true of all nodes. The virtual dom can’t move nodes between levels in the DOM.
If any of the parent nodes of a node change tagname or a parent is added or removed Elm will delete the whole branch from the DOM and recreate it.

Html.Lazy isn’t what to want, it won’t help, it doesn’t prevent Elm from deleting and recreating nodes that Elm thinks are different nodes.

Html.Keyed allows you to tell the virtual dom that a node in the new virtual dom is the same as a node in the old virtual dom. This can be helpful when it might not be clear that it’s the same node.
Usually this is useful for making reordering efficient as Elm can just move nodes instead of recreating them. It can also be useful for making sure Elm doesn’t mix up some div tags at the same level.

Html.Keyed is only useful if Elm can identify that all the parent nodes are also the same nodes.

Check that you’re not adding additional parent nodes to your editable node and that you’re not adding additional siblings of those parents without using Html.Keyed on all siblings.
eg. https://ellie-app.com/g85Cv3rQY2ya1

The example might not be clear.
https://ellie-app.com/g85Cv3rQY2ya1
The focus is lost on the input if you type ‘pizza’ because that results in a new div being added as a sibling to the parent of the input. Elm can’t tell if this new div is new, or a change to the old div(that contained the input) so it just recreates both div nodes and any of their children.

Diffing trees is very difficult and computationally expensive. All virtual dom libraries work on the idea that the DOM structure won’t change much and if it does, they just give up and recreate everything. You’ll see the same problem in React(a bit less so because React warns you if you’re not using keyed nodes)

I see - when its parent or sibling nodes are changed. Didn’t think of that!

Html.lazy compares the view function passed in as well as its arguments when deciding whether nor not the view function needs to be reevaluated. In order for it to work, you’ll need to pass in a named function - a function expression like (\_ -> ... ) will be a new value every time.

3 Likes

Thanks for answers, very helpful. Fortunately, Keying all the way up the root looks pretty easy to do on this code, so I will try that.

https://ellie-app.com/g88d9rwz6Vda1

Is seems to work as long as the parent-nodes and the number of previous sibling-nodes keep the same. Its probably a good idea, having functions returning not a list of nodes, but a single node (which could be a div-node including a list). A changing number of child-nodes is more a problem than a deep DOM tree for this diffing algorithm. Html.keyed · An Introduction to Elm

As keyed-nodes supports the diffing, I am wondering, why they are not the default ones.

Presumably because there is no way to auto generate stable unique keys (or is there)? So doing that would force that onto your code?

2 Likes

I had the problem using the quill editor because quill heavily modifies the dom. What I ended up doing was a dirty hack where a custom element would clone all of its HTML and mount quill on the cloned html. I saw no other way.

class quillEditorProxy extends HTMLElement {
  connectedCallback() {
    this.innerHTML = this.innerHTML + this.outerHTML.replace("-proxy", "");
  }
  static get observedAttributes() {
    return ["content_id"];
  }
  attributeChangedCallback(_, newVal, oldVal) {
    // dirty hack to refresh content because I can't find the setters for the custom element
    // and doing .content = "abc" freezes the page.
    try {
      if (newVal == oldVal) {
        return;
      }
      this.querySelector("quill-editor").remove();
      this.innerHTML = this.innerHTML + this.outerHTML.replace("-proxy", "");
    } catch (e) {}
  }
}

customElements.define("quill-editor-proxy", quillEditorProxy);

I have Keyed all the way up to the root, but still having the issue. Its also weird because the node that is content editable only loses its focus when one of its siblings is changed. So I should not really need to Key all the way up to fix it. That leads me to think that perhaps its some other random thing/browser bug that is causing my issue.

Is there a fool-proof way to tell for certain when Elm is deleting a DOM node? I guess just add some debug statements in the virtual DOM code to log to console when it happens.

Not sure if that is what you’re after, but in Firefox there is a way to pause JS on DOM mutations. Could that be helpful in your situation?

1 Like

I think a break point on this line catches it:

So I have things that are Keyed and now I can tell for sure that the diffing algorithm is removing them. Quite hard to read that code, so far I conclude that Keying mostly prevents things being overwritten but it is not a guarantee. I am not really sure what the conditions are yet, which determine if it happens or not.

In this particular application, the sibling nodes can change order in the DOM. They are SVG nodes, and since SVG draws things in order, to change the stacking order of the drawing the nodes must be moved. That is happening whilst editing is going on, and I think that is what sometimes removes the focus.

I should be able to solve it by simply moving the editing node to a different place in the DOM, where it does not have changing siblings. Assuming that works, its an easy fix for my particular issue.

However, I get the impression from the Elm Guide that Keying should work, no matter how you change the order of things:

https://guide.elm-lang.org/optimization/keyed.html

Perhaps that is not true, and only certain changes to the keyed nodes result in no deletions. Like adding one, or removing one etc? Don’t know really, I will have to read the algorithm carefully to understand it.

Elm’s implementation of Keyed is very simplistic – it basically just looks ahead one element, for supporting the common use cases:

  • Removing one thing in a list.
  • Adding one thing in a list.
  • Moving one thing in a list.

It does not do a full diff to guarantee that nodes with the same key aren’t re-created.

I went for a more advanced technique in https://github.com/lydell/elm-safe-virtual-dom: I first try with a fast technique (for the “I use Keyed because of performance!” use case). If that gets stuck, it falls back on a slower technique (for the “I use Keyed because of DOM node re-use!” use case), guaranteeing that nodes aren’t re-created. However, there’s still a chance one has to remove a DOM node and insert it somewhere else among its children – that’s after all how one re-orders things.

So I think that the conditions to guarantee that a node is not deleted and recreated are:

  1. The node is keyed, and all its parent nodes are keyed up to the root of the Elm controlled DOM.
  2. The node (and all its parent chain up to the root) is in a stable position where it will not be moved relative to its siblings.

Probably first amongst the siblings is the simplest to maintain a stable position, if siblings are changing. Doesn’t matter if the siblings are static.

If siblings are static, they also probably do not need to be keyed. So the keying up to the root is only really needed on parts of the DOM that are changing, or likely to change. Keying up to the root is a way of sanity checking that you didn’t miss something though.

The explanation in the guide is wrong, but I guess what you are meant to take away from it is not an exact understanding of what the current keyed implementation does, just that its an optimization that can help when making changes to lists of elements. The implementation could change, and it is not meant as a way of preventing node deletion.

I also found that setting a browser break-point on Element delete from the DOM, is a good way to catch when something is getting deleted-and-replaced by the Elm virtual-dom code. Its “Break On…” → “node removal” in Chrome and Firefox.

2 Likes

This topic was automatically closed 10 days after the last reply. New replies are no longer allowed.