Batch: Support browser extensions, Google Translate, fix Html.map, and more#187
Open
lydell wants to merge 125 commits intoelm:masterfrom
Open
Batch: Support browser extensions, Google Translate, fix Html.map, and more#187lydell wants to merge 125 commits intoelm:masterfrom
lydell wants to merge 125 commits intoelm:masterfrom
Conversation
This was referenced Jun 1, 2025
One might think that if `oldNode === newNode` no changes are needed, but users can mutate properties, for example by typing into text inputs, so we still need to apply properties. This happens when using constants or `lazy`.
|
We hit the Html.map bug (getting unexpected values like @lydell's great work on elm-safe-virtual-dom helped a lot! 🎉 We're now using that to patch the files in elm_home in our build process, and that change will be in production within a few weeks. |
If an element has `[Html.Attributes.disabled True]` and then switches to `[]`, the `disabled` property is supposed to be set to `false`. However, this didn’t work because I had mixed up `props` and `prevProps`. I tried to read `props["disabled"]` which is `undefined` (since `disabled` isn’t set anymore), but should read `prevProps["disabled"]` (which is the value `false`).
lydell
commented
Aug 16, 2025
| // grep --invert-match void src/Elm/Kernel/VirtualDom.js | grep --only --extended-regexp '_{2}[0-9a-z]\w+' | awk '!visited[$0]++' >b.txt | ||
| // # Keep only the double underscore tokens from the reference commit that still exist. | ||
| // grep --fixed-strings --line-regexp --file=b.txt a.txt | ||
| void { __2_TEXT: null, __text: null, __descendantsCount: null, __2_NODE: null, __tag: null, __facts: null, __kids: null, __namespace: null, __2_KEYED_NODE: null, __2_CUSTOM: null, __model: null, __render: null, __diff: null, __2_TAGGER: null, __tagger: null, __node: null, __2_THUNK: null, __refs: null, __thunk: null, __1_EVENT: null, __key: null, __value: null, __1_STYLE: null, __1_PROP: null, __1_ATTR: null, __1_ATTR_NS: null, __handler: null, __eventNode: null }; |
Author
There was a problem hiding this comment.
This backwards compatibility (and some more later in the file) could be replaced by:
- Use _VirtualDom_toTest instead of decoding hacks elm-explorations/test#250, via lydell@d472bad
- elm-pages using
_VirtualDom_toStringvia lydell@3f4648d + lydell@9a55ea3
Stopping propagation implies synchronous rendering. `isSync` is passed along with the message in `sendToApp`. However, when wrapping `sendToApp` with the mapping function from `Html.map`, I forgot to pass it along, causing `isSync` to be `undefined`, which results in the same outcome as `false`.
ahankinson
added a commit
to elm-janitor/virtual-dom
that referenced
this pull request
Nov 20, 2025
fixes elm#187 fixes elm#182 fixes elm/html#44 fixes elm/browser#121 fixes elm/browser#66 fixes elm#62 fixes elm#127 fixes elm#147 fixes elm#159 fixes elm#105 fixes elm#162 fixes elm#171 fixes elm#175 fixes elm#178 fixes elm#180 fixes elm#183 fixes elm#189 fixes elm#166 fixes elm/html#160 fixes elm/html#177 fixes elm/compiler#2069
ahankinson
added a commit
to elm-janitor/browser
that referenced
this pull request
Nov 21, 2025
fixes elm#137 fixes elm#105 fixes elm/virtual-dom#187
ahankinson
added a commit
to elm-janitor/browser
that referenced
this pull request
Nov 21, 2025
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Intro
This PR:
For those who would like to use this PR in their own app – see https://github.com/lydell/elm-safe-virtual-dom.
The above link also explains exactly what is changed, in a way that should be somewhat understandable even if you’re not super into the real and virtual DOM.
Below is a more technical, condensed version of the above link.
Summary of changes and fixed issues
Html.mapbug where messages of the wrong type can appear inupdatefunctions is fixed, by completely changing howHtml.mapworks. The old code was very clever and could theoretically skip more work in some cases, but was also a bit difficult to understand. The new code is instead very simple, leaving little room for errors. Closes elm/virtual-dom#105, closes elm/virtual-dom#162, closes elm/virtual-dom#171, closes elm/virtual-dom#166 (PR), closes elm/html#160, closes elm/compiler#2069Html.mapimprovements removes the need for the hidden_Json_equalityfunction in elm/json, which was buggy. Closes elm/json#13, closes elm/json#15, closes elm/core#904. This PR might still be nice to merge, for those who encountered the bug but don’t have time to upgrade everything at once: elm/json#32 (PR)Html.Keyed. The new algorithm is slightly smarter without losing performance, and uses the new Element.prototype.moveBefore API, if available, which allows moving an element on the page without “resetting” it (scroll position, animations, loaded state for iframes and video, etc.). Element.prototype.moveBefore was added to the specification recently. Closes elm/virtual-dom#175, closes elm/virtual-dom#178, closes elm/virtual-dom#183, closes Use Object.create(null) instead of {} to compute keyed diffs #180_VirtualDom_virtualizeis now completed, making it usable in practice, for example for elm-pages. This means that server-side rendered pages no longer have to redraw the whole page when Elm initializes (which seems to have been the intention for_VirtualDom_virtualizeall along, but hasn’t worked in practice).--primary-color, can now be set withHtml.Attributes.style "--primary-color" "salmon". CSS custom properties are incredibly useful for implementing theming (such as a dark mode). Closes elm/html#177, closes Allow CSS custom properties by using style.setProperty() #127.Svg.Attributes.xlinkHrefno longer mutates the DOM on every single render, which caused flickering in Safari sometimes. This was due to an oversight in the diffing of namespaced attributes. Closes elm/virtual-dom#62, closes Fix diffing for namespaced attributes (Fixes #62) #159lazyno longer changes behavior for<input>. Closes Lazy is too lazy for inputs #189The key changes
Making Elm work well with browser extensions, third-party scripts and page translators required three key changes.
1. Keeping our own tree of DOM nodes
A render in Elm currently works like this:
view._VirtualDom_addDomNodes).It’s step 3 that can crash if a browser extension has changed the DOM.
With this PR, there is no more step 3. Instead, we store the DOM nodes in our own tree. While diffing in step 2, we simply make the changes immediately to the correct DOM node, by reading it from our own tree. Since there’s no walking of the real DOM tree, it doesn’t matter what changes a browser extension makes.
See New algorithm for pairing virtual DOM nodes for all the details.
2. Cooperating with page translators
There’s some new code for cooperating with page translators (Google Translate, as well as the translators built into Firefox and Safari). If the diff tells us to update a text node, but we detect that the text node in question has been translated, we tell the parent element of that text node to delete all text node children and re-render them. The page translator then kicks in and re-translates that element (taking the entire text of the element into account for a more accurate translation).
(Evan, if you remember us talking about this at Elm Camp 2024 – don’t worry, there is no “bug” introduced on purpose regarding
<font>elements – they can still be created by Elm just fine. Google Translate oddly uses<font>tags for translated text, but it turned out to easy to tell the difference between<font>tags created by Elm and by others.)3. Virtualizing more conservatively
When using
Browser.documentandBrowser.application, Elm takes charge of<body>. Third-party scripts and browser extensions put things in<body>too, and crucially they might do so before Elm initializes. Currently, Elm virtualizes all elements in the mount node (<body>) in this case, which most likely results in the extra things in<body>added by third-party scripts and browser extensions being removed.This PR changes the virtualization behavior to only virtualize text nodes, and elements that have the
data-elmattribute, leaving everything else alone. I adddata-elmautomatically to all elements created by Elm, so if you server-side render your page by running your Elm code on the server, you get that for free without having to do anything. You can read more about how this can be used in practice at elm-pages PR 519.Note: This is the reason the PR is only 99.99 % backwards compatible. Read more in the “Breaking” changes section below.
The other changes
So, I’ve talked about the key changes. But the “Summary of changes and fixed issues” section mentions a few more things. Why are they in this PR?
Html.map: I needed to work onHtml.mapanyway to make it work with the new DOM node pairing algorithm.Html.Keyed: Same thing.lazyfix for inputs: It was incredibly easy to fix thanks to the other changes in this PR.Detailed descriptions of changes
“Breaking” changes
I haven’t changed the Elm interface at all (no added functions, no changed functions, no removed functions, or types). All behavior except three details should be equivalent, except less buggy.
The goal was to be 100 % backwards compatible. For some people, it is. For others, there are two changes that are in “breaking change territory”. The first one can be summarized as: Elm no longer empties the mount element. It’s easily fixed by adding the
data-elmattribute to select elements in the HTML.That’s how users will perceive it. In reality, the actual change is which elements are virtualized (to support third-party scripts better, as mentioned in the “Virtualizing more conservatively” section).
The second detail can be summarized as properties are diffed against the real DOM. This can uncover mistakes in Elm code, where the you forgot to set
selectedproperly on options in a<select>element, for example.The third detail can be summarized as setters should have getters on custom elements. Otherwise the setter will unnecessarily run on every render.
Read all about these “Breaking” changes in the safe-virtual-dom documentation.
Performance
O(n)complexity.Related PR:s
<a>click listeners: Allow virtualization to set up link click listeners browser#137requestAnimationFrameerror loop on virtual DOM crashes: Fix animation frames browser#138Code style
I’ve done my best to: