choojs / nanomorph Goto Github PK
View Code? Open in Web Editor NEW๐ - Hyper fast diffing algorithm for real DOM nodes
License: MIT License
๐ - Hyper fast diffing algorithm for real DOM nodes
License: MIT License
As suggested by @yoshuawuyts in #choo, more checks could be added to nanomorph
to prevent unnecessary repainting.
This would likely result in extra lookups, but depending on whether repaints are computationally more expensive, it could be worth it.
It'd be neat to have a benchmark to test our diffing needs โ perf is a feature, and we should be able to catch regressions.
Given this would need to run in the browser, we'd probably need some tooling around this. I was thinking it'd be cool to run a headless Chrome instance through puppeteer, and use the nanobench module to perform the benches. Would be neat if we could run it as a standalone CLI thing, with similar output to TAP.
But by all means โ feel free to pick up this issue, and run with it. Probably worth picking up similar benches as in choojs/choo#492.
Hope this makes sense. Thanks!
This is an edge case i discovered while using nanomorph "in anger".
What is the best way to test issues like these?
// initial state
<form>
<label for=stuff>
<input autofocus="autofocus" name="stuff">
</label>
</form>
// Updated state based on input interaction
<form>
<h1>YOU DID A THING!</h1>
<label for=stuff>
<input autofocus="autofocus" name="stuff">
</label>
</form>
Believe we're not touching the file field which means morphing might go wrong if the whole view is replaced. Bit of an edge case, but something to think about.
Hi!
Sometimes I think "Oh, onanimationend
, fullscreenchange
are missing in event.js" but "Then what is missing? how can we make it all listed?"
Yes I can make a PR to add tho, Do you have a policy to add Events into [event.js] ?(https://github.com/choojs/nanomorph/blob/master/lib/events.js)
Thanks.
This seems to work for me, but I'm a TypeScript beginner and am probably doing a lot of things wrong:
// Modified from https://github.com/patrick-steele-idem/morphdom/pull/113/commits/0b6b2e87b5f1b193c40c4c64f25938846ccd4039
declare module "nanomorph" {
interface NanomorphOptions {
getNodeKey?: (node: Node) => any;
onBeforeNodeAdded?: (node: Node) => Node;
onNodeAdded?: (node: Node) => Node;
onBeforeElUpdated?: (fromEl: HTMLElement, toEl: HTMLElement) => boolean;
onElUpdated?: (el: HTMLElement) => void;
onBeforeNodeDiscarded?: (node: Node) => boolean;
onNodeDiscarded?: (node: Node) => void;
onBeforeElChildrenUpdated?: (
fromEl: HTMLElement,
toEl: HTMLElement
) => boolean;
childrenOnly?: boolean;
}
namespace nanomorph {
}
function nanomorph(
fromNode: Node,
toNode: Node | string,
options?: NanomorphOptions
): void;
export default nanomorph; // Added "default" here.
}
@yoshuawuyts I have also tried to create this exact thing for use in a framework I have been building. It's located here, perhaps it could provide some inspiration since it is a slightly different approach and it also addresses node ordering with data-key
and id
attributes. It also supports namespaces in a pretty clean way.
Anyways this is a non issue but thought I would shoot this your way.
Feel free to close.
So @developit pointed out another optimization we could do is check node reordering. Often times lists get updated; items get appended or removed. Given that lists, forms and links are the staple elements of HTML this is probably worth optimizing for.
The new node comparison algorithm would then become:
I reckon this should speed up the reordering case by quite a bunch. Help welcome! โจ
Hi Yosh,
sometimes I want to do
var newInput = html`<input></input>`
newInput.value = 42
return newInput
rather than
var newInput = html`<input value=${value}></input>`
return newInput
then diff this thing somewhere else
But since you have this if-chain here https://github.com/yoshuawuyts/nanomorph/blob/32635c37f4272e1e225398e76e540de314a7b701/lib/morph.js#L118
it never really updates my value as written in the first case because I added the property but the attribute doesn't exist (I think?) :(
The way morphdom handles this is it checks these things independently: https://github.com/patrick-steele-idem/morphdom/blob/43b808008bd39b571a1dd32d49811c7c22bd2021/src/specialElHandlers.js#L32
I don't know if this should be considered a bug but it would be nice if my first case would be possible to do as sometimes elements get large and it's visually nice to set some stuff from outside.
Does this make sense? ๐ Sorry for putting more things on your plate, would have just sent PR but you might have had your reasons for doing it like this.
edit by yoshuawuyts syntax highlighting
We could turn the DOM into a Merkle tree, but because each Node is already unique (hurray for objects), the tree itself is already a tree of hashes. The goal of this is to provide functionality similar to React's .shouldComponentUpdate()
but without explicitly having to create manual checks, so that any tree of valid DOM nodes can be parsed and diffed efficiently (hurray for the DOM as the lowest common denominator!). A few rules should suffice I reckon:
"edited="
label; this acts as a signal if that part of the tree should be traversed for further updatesoldState
=== newState
return the existing node so [object Object] === [object Object]
will return true
.!==
node in the old tree, update the node in the old tree"edited="
label than the new tree, or no label at all, update the node in the old tree - don't forget to copy over the "edited" label. Then continue traversing the treeTrees are generally only a few layers of nesting, with loads of elements living in parallel. Traversing upwards in a tree should be generally inexpensive, so the "edited="
label can be applied with little cost. Because JS Objects are unique, and the label provides guarantees about the state of the child nodes, we can stop traversing nodes early and essentially gain the result of React's .shouldComponentUpdate()
.
So yeah, that's the idea. morphdom
can already be improved in size and speed a bit by removing the event hooks; but with this we might be able to take things a bit further. Thanks! โจ
Hi there,
I love the direct DOM diffing, and I first experimented wit morphdom. I encountered one problem connected to what in React's terminology are controlled components. In short, when you diff <input value='sth'>
vs <input value='something'>
, the stateful part of the DOM is lost (field focus, cursor position, etc.).
I was wondering if that is something that you guys dealt with in nanomorph?
I haven't modified my project to use any kind of module manager yet. So, is this possible to use nanomorph by using regular script tags only?
what would be required if this cant be done through npm?
I use *-umd files from morpdom.
Heyo!
I have a failing test for copying over an input type:
https://github.com/kristoferjoseph/nanomorph/blob/master/test.js#L45
I made sure that this isn't an issue with bel
already.
Hey!
I am working with morphdom to build a backbone.js view extension. Because all events are discarded each time and I am morphing an element which is many views combined... I have to get a bit creative. I am writing some extra stuff to map all the events defined in each view to a central object to redistribute them after each render.
I was just wondering if you plan to keep that addition?
Thanks!
I just started using nanomorph
and I have a simple test by changing the icon of an SVG icon sprite every second. Works perfectly in latest chrome but not in Firefox - see screencasts below:
[Edit]: I just realised that the xlink:href
attribute isn't updated.
I use it to build Custom-elements with incremental rendering:
axa-ch-webhub-cloud/pattern-library#411
I'd like to patch just the children of two nodes (specifically a custom element and a document fragment). I think this would be possible with nanomorph
if you exposed the updateChildren()
function or provide a childrenOnly
option like morphdom
.
right now the update-dom file allows creating an update loop from a single element that prevents the tree from being dismounted when the type of root node is changed. This removes a lot of boilerplate when building applications. I think the file is rather unfortunately named though, so I reckon we should rename it.
require('nanomorph/loop')
(my preference)require('nanomorph/update')
thoughts? cc/ @kristoferjoseph
Open issues and PRs with no updates to master in 9 months. Wondering if I should switch back to morphdom.
var newAttrs = newNode._attributes
var oldAttrs = oldNode._attributes
var mergedAttrs = xtend(newAttrs, oldAttrs)
The _attributes
value only exists on DOM nodes created with min-document
/ bel
which is fine - but once we use yo-yoify
or native DOM nodes this'll break down.
Do we think we should copy input values ala yo-yo
?
Always felt weird to me that morphdom
doesn't handle this case.
If possible you should try and avoid using childNodes
and attributes
. Both are computed functions that are very expensive to use.
For example, rather than using childNode
, use firstChild
and loop through via nextSibling
as DOM nodes are based on a two-way linked list โ not a graph/array structure.
Also, I did some tests recently and its far more effective to create a lightweight "vdom" object upon inspecting and calculating what exists in a real DOM node and storing this in a WeakMap based on the DOM node itself being the key. You'll never get greater performance from accessing the "vdom" object vs DOM node properties as they are aren't monomorphic and can't be properly optimised by JavaScript engines. So rather than getting an O(1) from doing dom.nodeType
, you're likely to get an O(log n) or worse depending on the JavaScript engine's optimisation path.
The repo where this bug happens is https://github.com/cideM/tinyfinancial on the details
branch (https://github.com/cideM/tinyfinancial/blob/details/src/client/views/Details/index.js). But it requires MongoDB and one would have to remove the facebook authentication. But maybe I'll be able to explain my issue through code examples as well.
I have a controlled <select>
and onchange
a choo state variable is updated, the view function is rerendered and the newly selected <option>
should now be selected
. The actual behavior is very confusing though:
When the function renders the first time the DOM looks like this
<select value="2017">
<option value="2017" selected="selected">2017</option>
<option value="2016">2016</option>
</select>
If the user select 2016, the DOM looks like this
<select value="2016">
<option value="2017" selected="">2017</option>
<option value="2016" selected="selected">2016</option>
</select>
Note the leftover selected=""
.
Now here are a few console.log
statements that I put in updateAttribute
of morphdom
function updateAttribute (newNode, oldNode, name) {
console.log(newNode)
console.log(newNode.selected)
console.log(oldNode)
console.log(oldNode.selected)
When the user chooses 2016, the log statements evaluate to this:
<option value="2017">2017</option>
true
why? :[
<option value="2017" selected="">2017</option>
false
Seems okay I guess
and
<option value="2016" selected="selected">2016</option>
false
O_o
<option value="2016" selected="selected">2016</option>
false
ร_รฒ
It's late and it's been confusing, so maybe this is totally expected, but I don't it. The booleans seem off though. My intuition says something is mutating the nodes in between other actions but that doesn't sound right. And that getAttribute
and properties are not always the same is known but that it would cause such a bug also sounds odd. Lastly, maybe I am just making a grave mistake here somewhere?
I was actually able to fix this by changing updateAttributes
to
function updateAttribute (newNode, oldNode, name) {
if (newNode.getAttribute(name) !== oldNode.getAttribute(name)) {
oldNode.setAttribute(name, newNode.getAttribute(name))
if (newNode.getAttribute(name)) {
oldNode.setAttribute(name, '')
} else {
oldNode.removeAttribute(name, '')
}
}
}
Note that I replaced the calls to node.selected
with node.getAttribute('selected')
. I didn't fork and run tests and I didn't manually check anything else because I really need to sleep now. But please let me know
a) if my bug is known and there's a workaround I couldn't find
b) my "discoveries" are expected behavior and I just don't it
Something like this
let events = []
if (typeof window !== 'undefined' && typeof document !== 'undefined') {
// eslint-disable-next-line
for (const key in document) {
const isEvent = document[key] == null || typeof document[key] === 'function'
if (key.startsWith('on') && isEvent) {
events.push(key)
}
}
} else {
events = [
'onreadystatechange',
'onpointerlockchange',
'onpointerlockerror',
'onbeforecopy',
'onbeforecut',
'onbeforepaste',
'oncopy',
'oncut',
'onpaste',
'onsearch',
'onselectionchange',
'onselectstart',
'onvisibilitychange',
'onabort',
'onblur',
'oncancel',
'oncanplay',
'oncanplaythrough',
'onchange',
'onclick',
'onclose',
'oncontextmenu',
'oncuechange',
'ondblclick',
'ondrag',
'ondragend',
'ondragenter',
'ondragleave',
'ondragover',
'ondragstart',
'ondrop',
'ondurationchange',
'onemptied',
'onended',
'onerror',
'onfocus',
'oninput',
'oninvalid',
'onkeydown',
'onkeypress',
'onkeyup',
'onload',
'onloadeddata',
'onloadedmetadata',
'onloadstart',
'onmousedown',
'onmouseenter',
'onmouseleave',
'onmousemove',
'onmouseout',
'onmouseover',
'onmouseup',
'onmousewheel',
'onpause',
'onplay',
'onplaying',
'onprogress',
'onratechange',
'onreset',
'onresize',
'onscroll',
'onseeked',
'onseeking',
'onselect',
'onstalled',
'onsubmit',
'onsuspend',
'ontimeupdate',
'ontoggle',
'onvolumechange',
'onwaiting',
'onwheel',
'onauxclick',
'ongotpointercapture',
'onlostpointercapture',
'onpointerdown',
'onpointermove',
'onpointerup',
'onpointercancel',
'onpointerover',
'onpointerout',
'onpointerenter',
'onpointerleave',
'onwebkitfullscreenchange',
'onwebkitfullscreenerror',
'onsecuritypolicyviolation',
'onformdata',
'onfullscreenchange',
'onfullscreenerror',
'onfreeze',
'onresume'
]
}
module.exports = events
Also, currently, there are only 45 event names, but as of today it seems they are 95. As, probably no so common or used, but why not. Also it will decrease the bundle sizes, and will use this list only on server.
Hey there! I'm wondering if there are any recommended ways to cache elements. Some of my render loops happen in a requestAnimationFrame(), so I'd like to prevent tons of network requests for the same images.
โฟ โฟ โฟ
I tried declaring my icons once, outside of my render function. E.g.:
let someIcon = html`<img src="icon.png">`
let renderButton = () => {
return html`
<button>${ someIcon } Click me </button>
`
}
let renderAll = () => {
morph(oldButton, renderButton())
}
This prevents additional network requests, but it still causes unnecessary changes to the DOM because the cached img
node can only live in one tree at a time. So when morph
does a comparison, the img
exists in one tree and not the other, and it does a replacement.
Now that I'm writing this, maybe what I need is exactly two copies of each image. One for the real DOM, and one for the comparison DOM?
Would love to hear if/how other people have handled this sort of thing!
See issue #25
Was there any particular reason to use the order newTree, oldTree
in the API? Maybe I'm just used to morphdom/yo-yo but I would have expected oldTree, newTree
since you're morphing the old tree to become the new tree, no?
morphdom
has tree, newTree
- we got newTree, tree
- this is like super confusing; we should probably change
const assert = require('assert')
Not works in browser with some builders (example, stealjs) without install https://www.npmjs.com/package/assert
So we should clean up our tests a little before we refactor & add more tests :D
Hi
Do you think it's a good idea to annotate the source for educational purposes? Just like how the-super-tiny-compiler does. I really like the idea of a tiny module demonstrating hot but hard technologies. If not this module maybe you could make another one for educational purposes. It would also reduce the barrier of contributing your choo module I believe. Thanks!
I just ran into this but I'm not sure if it is my mistake or an issue.
When setting something like
function mainView (state, emit) {
return html`
<body>
<select onchange="${onchange}">
${state.opts.map((opt, i) => {
return html`<option value="${i}">${opt}</option>`
})}
</select>
<button onclick=${onclick}>Run</button>
</body>
`
function onclick () {
console.log('click!')
}
function onchange() {
console.log('change!')
}
}
The click event is normally attached to the button, but the change event isn't attached to the select tag.
I saw in comments that some elements need speacial treatment to set events, but there is no special treatment for select tags, even tho there is in morphdom. Is there something missing? or am I doing something wrong?
When typing into an input search input box, the input is cleared every time, which is a behavioral difference to morphdom.
It seems like nanomorph element iteration is skipping every other element somewhere. I (blindly) tried adding ids, but the result is unchanged.
Version: [email protected]
with [email protected]
Expected behavior:
nanomorph(new, old)
where new tree has ten childrenObserved behavior: DOM is updated, but only contains elements 0, 2, 4, 6, 8
Example: http://codepen.io/rsreusser/pen/KazMMa?editors=0010
Code to reproduce:
const makeHtml = (n) => bel`
<ul>${
new Array(n).fill(0).map((d, i) => bel`<li>${i}</li>`)
}</ul>
`;
const tree = makeHtml(0);
document.body.appendChild(tree);
nanomorph(makeHtml(10), tree);
Thoughts: Aside from a bug, the next most likely issue seems to me that I'm misusing the API. (right after that is that this is a very experimental library ๐ )
Random thought: This isn't that thing that happens when you mutate a list as you're iterating over it, is it? Some things solve that by letting each operation return an updated loop index. Just a random guess without proper debugging.
Have you considered to use document.createTreeWalker
or document.createNodeIterator
for DOM traversal?
I like the API but it is hardly ever used and I just wonder why.
It's late at night, so I'll keep this short.
I'm making a framework that uses nanomorph heavily. I've just been playing with it, and I realized that comments are uncommented, meaning the <!--
and -->
are removed. This is a bit annoying when debugging my app. Not urgent, but just letting you know it's there.
I created tests for the events listed in https://github.com/choojs/nanomorph/blob/master/lib/events.js#L32
They are tested in the as follows:
The following events fail to parse from HTML attribute. I believe this is a problem in Bel. But I am not sure where this problem originates.
The first two are listed as problematic and fail
Note: these event may not work as expected in Chrome, Safari and Opera 15+ using the JavaScript HTML DOM syntax. However, it should work as an HTML attribute and by using the addEventListener() method (See syntax examples below).
onfocusin
https://www.w3schools.com/jsref/event_onfocusin.asp
onfocusout
https://www.w3schools.com/jsref/event_onfocusout.asp
These events are listed as Editor's Draft(Non-stable version), but should work in chrome. They also fail.
ontouchcancel
https://developer.mozilla.org/en-US/docs/Web/API/GlobalEventHandlers/ontouchcancel
https://developer.mozilla.org/en-US/docs/Web/Events/touchcancel
ontouchend
https://developer.mozilla.org/en-US/docs/Web/API/GlobalEventHandlers/ontouchend
https://developer.mozilla.org/en-US/docs/Web/Events/touchend
ontouchmove
https://developer.mozilla.org/en-US/docs/Web/API/GlobalEventHandlers/ontouchmove
https://developer.mozilla.org/en-US/docs/Web/Events/touchmove
ontouchstart
https://developer.mozilla.org/en-US/docs/Web/API/GlobalEventHandlers/ontouchstart
https://developer.mozilla.org/en-US/docs/Web/Events/touchstart
This event should only work on the body tag, but also seems to fail.
onunload
https://www.w3schools.com/jsref/event_onunload.asp
Should only work on the body tag. Testing on the body tag fails.
I will submit a pull request that contains the event tests.
I'm planing to switch from morhdom due to the size of this library. I have read the FAQs already.
I wanted to know, if there is a way to find out if nanomorph supports a browser or not?
Does nanomorph gracefully degrade on unsupported browsers?
I think we could remove https://github.com/yoshuawuyts/nanomorph/blob/master/index.js#L81-L92 and leave this for libraries to pick up. Would need validation, but yeah think that'd be cleaner. Semver major.
Whenever we morph elements we should probs be copying over the events. From yo-yo:
// update-events.js
module.exports = [
// attribute events (can be set with attributes)
'onclick',
'ondblclick',
'onmousedown',
'onmouseup',
'onmouseover',
'onmousemove',
'onmouseout',
'ondragstart',
'ondrag',
'ondragenter',
'ondragleave',
'ondragover',
'ondrop',
'ondragend',
'onkeydown',
'onkeypress',
'onkeyup',
'onunload',
'onabort',
'onerror',
'onresize',
'onscroll',
'onselect',
'onchange',
'onsubmit',
'onreset',
'onfocus',
'onblur',
'oninput',
// other common events
'oncontextmenu',
'onfocusin',
'onfocusout'
]
// copy.js
function copier (f, t) {
// copy events:
var events = opts.events || defaultEvents
for (var i = 0; i < events.length; i++) {
var ev = events[i]
if (t[ev]) { // if new element has a whitelisted attribute
f[ev] = t[ev] // update existing element
} else if (f[ev]) { // if existing element has it and new one doesnt
f[ev] = undefined // remove it from existing element
}
}
// copy values for form elements
if ((f.nodeName === 'INPUT' && f.type !== 'file') || f.nodeName === 'TEXTAREA' || f.nodeName === 'SELECT') {
if (t.getAttribute('value') === null) t.value = f.value
}
}
Heya there! ๐
I'm deeply digging into tests, because i'm experimenting to implement virtual dom diffing, where nanomorph
guided me at most. Diffing vdom, seems pretty smaller and easy than that here.
I believe my implementation looks good, but it's just a toy for the moment. All nanomorph tests pass, except the last one for nested without id
.
But while digging into test/diff.js
i found one test which have opening tag, but the closing tag is missing a slash /
- see it here test/diff.js#L357-L365. Is it intentional or is typo?
Also, the title isn't very clear to me. What should be the result? I think it should be <section><div></div><section>
, but the title says me that it expect to be something like <section><div>'hello'</div><section>
which not make sense to me to be correct result?
One more thing is to include the test suite (that file) in the npm package, so others can tests against it.
Hello, I'm trying to use nanomorph on the entire <body>
. I'm getting an HTML string via ajax (see inject
). The goal is to update the DOM body by updating only the changed nodes (instead of destroying/replacing everything). What am I missing here?
var inject = "string of HTML, e.g. $('body').html()";
var tree = morph(inject, $('body').html());
console.log(tree); // undefined
Thanks in advance
Based off of an issue @rreusser was seeing illustrated in this codepen
If this library becomes a thing we would probably want to keep parity with the nanomorph
diffing algorithm or (yo-yo
if they're not keen). It would be cool if we could create a shared testing library that defines all behavior we expect from a diffing library so we can make sure our implementations adhere to this. I would imagine the API would be along the lines of:
const abstract = require('abstract-dom-morph/test')
const nanomorph = require('nanomorph')
abstract(nanomorph)
This could help with a possible migration of choo
to a tinier diffing implementation and help foster the creation of new DOM-only diffing algorithms. Thoughts?
cc/ @kristoferjoseph
So we have a slight diffing problem โ not only are we fairly slow at updating, reordering nodes is also suboptimal. This means that some reorders can be surprising at the worst case, and in a worst case reorders simply take longer than they should.
In computer science this is known as "diffing", and is a rather well-studied problem. Probably the best known algo for this is the Mayers Diffing algorithm. It'd be neat if we could make use of this for nanomorph
.
When using appendChild to add children from the new tree to the original it removes them from the original.
Should we add a warning about this in the docs?
ref morphdom writes:
NOTE: This module will modify both the original and target DOM node tree during the transformation. It is assumed that the target DOM node tree will be discarded after the original DOM node tree is morphed.
We're testing SVG namespaces and events, but not regular ol' attributes. I've been running into issues on the server with this, so we should probably test this. Would also help enable #37 as we need to read and write attributes.
Specifically it'd be nice if tests could hit el.setAttribute()
and el.getAttribute()
. Thanks!
e.g. we're evaporating them hah
A declarative, efficient, and flexible JavaScript library for building user interfaces.
๐ Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.
TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
An Open Source Machine Learning Framework for Everyone
The Web framework for perfectionists with deadlines.
A PHP framework for web artisans
Bring data to life with SVG, Canvas and HTML. ๐๐๐
JavaScript (JS) is a lightweight interpreted programming language with first-class functions.
Some thing interesting about web. New door for the world.
A server is a program made to process requests and deliver data to clients.
Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.
Some thing interesting about visualization, use data art
Some thing interesting about game, make everyone happy.
We are working to build community through open source technology. NB: members must have two-factor auth.
Open source projects and samples from Microsoft.
Google โค๏ธ Open Source for everyone.
Alibaba Open Source for everyone
Data-Driven Documents codes.
China tencent open source team.