Repository navigation
Conversation
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
Author
There was a problem hiding this comment.
The diff is now marked as outdated, because I optimized this: Instead of void-ing an unnecessary object, we simply mention all the __foo in the comment: 655f019 (this PR)
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
The Elm compiler runs its replacements in comments, too, so no need for creating an unnecessary object. (The compiler does not parse the kernel files as JavaScript, it treats them as plain text.)
To reproduce: 1. Render keyed children `[a, b]`. 2. Remove `a` in the browser devtools. 3. Render `[b, a]` (a swap).
To reproduce:
if modBy 2 model.count == 0 then
Html.div [] [ Html.br [] [] ]
else
Html.section [] []
When rendering the `section`, its `tNode` should not contain the `br`
from the `div`.
It never uses its `x` argument for anything. And it makes sense: Before calling the function we have checked `x === y` or that two lazy nodes are equal.
It never uses its `eventNode` argument. And it makes sense: The function only cares about text nodes (which can’t have event listeners).
When virtualizing HTML attributes that should become Elm virtual-dom properties, we’re calling `_VirtualDom_property` which takes an Elm `Decode.Value` as its value argument, but we had forgotten to actually turn our plain JavaScript value into `Decode.Value` with `__Json_wrap`. This resulted in a bug: 1. Have `<button disabled>` in the HTML. 2. But render just `Html.button []` in Elm (no `disabled`). This resulted in the button staying disabled, while it should become enabled (the HTML attribute should be removed).
The code currently covers updates and replacements, but not removals. To reproduce: 1. Render `Html.div [] [ Html.text "Guten Tag" ]`. 2. Translate the page. 3. Render `Html.div [] []`. Result: The translation of “Guten Tag” stays on the page. With this commit: The translation of “Guten Tag” is removed. Remember how browsers’ page translators differ: - Firefox: Updates the existing text node. So it actually had no bug before. - Safari: Replaces with a new text node. - Chrome: Replaces with a `<font>` element with a new text node inside.
As mentioned in the commit message for commit bdb8913 there is no known case where the Firefox translator splits texts due to different word order and detecting that a removed text node has changed results in a better outcome. This commit removes the extra checks, since it get rids of a (recursive) helper function.
We used to have code like this, but it was removed on this branch:
```js
// remove if not in the new facts
if (!(xKey in y))
{
diff = diff || {};
diff[xKey] =
!category
? (typeof x[xKey] === 'string' ? '' : null)
:
(category === 'a__1_STYLE')
? ''
:
(category === 'a__1_EVENT' || category === 'a__1_ATTR')
? undefined
:
{ __namespace: x[xKey].__namespace, __value: undefined };
continue;
}
```
There you see how we set events to `undefined`, as a way of showing that
they should be removed. This is now diffed against the actual event
callbacks in `_VirtualDom_applyEvents` instead:
```js
for (key in allCallbacks)
{
if (!(key in events))
{
domNode.removeEventListener(key, allCallbacks[key]);
delete allCallbacks[key];
}
}
```
Since we never set any events to a falsy value, the code removed in this
commit was dead.
If a browser extension swaps the order of children and the user is also
using a page translator, the code handling translated text crashes.
Setup:
```elm
Html.div []
[ Html.text (if something then "Guten Tag" else "Guten Abend")
, Html.button [] [ Html.text "1" ]
, Html.button [] [ Html.text "2" ]
]
```
Steps to reproduce (using Google Translate in Chrome, which replaces
text nodes with `<font>` elements):
1. Swap the two buttons in the browser console.
2. Translate the page.
3. Flip `something`.
Before this commit: A crash, due to the old code assuming Elm’s elements
were in the correct order.
With this commit: The new translation appears (no leftover old text),
and the buttons return to their correct order.
As a bonus, the new code is simpler than the old.
It’s really strange, but `setAttributeNS` allows a prefix, as in `xlink:href`, but `removeAttributeNS` does not – you need to pass just `href`.
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 four 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.
The fourth detail can be summarized as class vs className.
property "className"andattribute "class"have never been merged – and it hasn’t really been a problem, since nobody mixes both in practice. But now you might mixHtml.Attributes.class(which uses theclassattribute) withHtml.Styled.Attributes.class(which uses theclassNameproperty, from the elm-css package). But there isn’t really a reason to do so.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: