Coder Social home page Coder Social logo

morphdom's Introduction

morphdom

Download count all time

Build Status

NPM

Lightweight module for morphing an existing DOM node tree to match a target DOM node tree. It's fast and works with the real DOM—no virtual DOM needed!

This module was created to solve the problem of updating the DOM in response to a UI component or page being rerendered. One way to update the DOM is to simply toss away the existing DOM tree and replace it with a new DOM tree (e.g., myContainer.innerHTML = newHTML). While replacing an existing DOM tree with an entirely new DOM tree will actually be very fast, it comes with a cost. The cost is that all of the internal state associated with the existing DOM nodes (scroll positions, input caret positions, CSS transition states, etc.) will be lost. Instead of replacing the existing DOM tree with a new DOM tree we want to transform the existing DOM tree to match the new DOM tree while minimizing the number of changes to the existing DOM tree. This is exactly what the morphdom module does! Give it an existing DOM node tree and a target DOM node tree and it will efficiently transform the existing DOM node tree to exactly match the target DOM node tree with the minimum amount of changes.

morphdom does not rely on any virtual DOM abstractions. Because morphdom is using the real DOM, the DOM that the web browser is maintaining will always be the source of truth. Even if you have code that manually manipulates the DOM things will still work as expected. In addition, morphdom can be used with any templating language that produces an HTML string.

The transformation is done in a single pass of both the original DOM tree and the target DOM tree and is designed to minimize changes to the DOM while still ensuring that the morphed DOM exactly matches the target DOM. In addition, the algorithm used by this module will automatically match up elements that have corresponding IDs and that are found in both the original and target DOM tree.

Support for diffing the real DOM with a virtual DOM was introduced in v2.1.0. Virtual DOM nodes are expected to implement the minimal subset of the real DOM API required by morphdom and virtual DOM nodes are automatically upgraded real DOM nodes if they need to be moved into the real DOM. For more details, please see: docs/virtual-dom.md.

Usage

First install the module into your project:

npm install morphdom --save

NOTE: Published npm packages:

  • dist/morphdom-umd.js
  • dist/morphdom-esm.js

The code below shows how to morph one <div> element to another <div> element.

var morphdom = require('morphdom');

var el1 = document.createElement('div');
el1.className = 'foo';

var el2 = document.createElement('div');
el2.className = 'bar';

morphdom(el1, el2);

expect(el1.className).to.equal('bar');

You can also pass in an HTML string for the second argument:

var morphdom = require('morphdom');

var el1 = document.createElement('div');
el1.className = 'foo';
el1.innerHTML = 'Hello John';

morphdom(el1, '<div class="bar">Hello Frank</div>');

expect(el1.className).to.equal('bar');
expect(el1.innerHTML).to.equal('Hello Frank');

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.

Examples

See: ./examples/

Browser Support

  • IE9+ and any modern browser
  • Proper namespace support added in v1.4.0

API

morphdom(fromNode, toNode, options) : Node

The morphdom(fromNode, toNode, options) function supports the following arguments:

  • fromNode (Node)- The node to morph
  • toNode (Node|String) - The node that the fromNode should be morphed to (or an HTML string)
  • options (Object) - See below for supported options

The returned value will typically be the fromNode. However, in situations where the fromNode is not compatible with the toNode (either different node type or different tag name) then a different DOM node will be returned.

Supported options (all optional):

  • getNodeKey (Function(node)) - Called to get the Node's unique identifier. This is used by morphdom to rearrange elements rather than creating and destroying an element that already exists. This defaults to using the Node's id property. (Note that form fields must not have a name corresponding to forms' DOM properties, e.g. id.)
  • addChild (Function(parentNode, childNode)) - Called when adding a new child to a parent. By default, parentNode.appendChild(childNode) is invoked. Use this callback to customize how a new child is added.
  • onBeforeNodeAdded (Function(node)) - Called before a Node in the to tree is added to the from tree. If this function returns false then the node will not be added. Should return the node to be added.
  • onNodeAdded (Function(node)) - Called after a Node in the to tree has been added to the from tree.
  • onBeforeElUpdated (Function(fromEl, toEl)) - Called before a HTMLElement in the from tree is updated. If this function returns false then the element will not be updated. if this function returns an instance of HTMLElement, it will be used as the new fromEl tree to proceed with morphing for that branch, otherwise the current fromEl tree is used.
  • onElUpdated (Function(el)) - Called after a HTMLElement in the from tree has been updated.
  • onBeforeNodeDiscarded (Function(node)) - Called before a Node in the from tree is discarded. If this function returns false then the node will not be discarded.
  • onNodeDiscarded (Function(node)) - Called after a Node in the from tree has been discarded.
  • onBeforeElChildrenUpdated (Function(fromEl, toEl)) - Called before the children of a HTMLElement in the from tree are updated. If this function returns false then the child nodes will not be updated.
  • childrenOnly (Boolean) - If true then only the children of the fromNode and toNode nodes will be morphed (the containing element will be skipped). Defaults to false.
  • skipFromChildren (Function(fromEl)) - called when indexing a the fromEl tree. False by default. Return true to skip indexing the from tree, which will keep current items in place after patch rather than removing them when not found in the toEl.
var morphdom = require('morphdom');
var morphedNode = morphdom(fromNode, toNode, {
  getNodeKey: function(node) {
    return node.id;
  },
  addChild: function(parentNode, childNode) {
    parentNode.appendChild(childNode);
  },
  onBeforeNodeAdded: function(node) {
    return node;
  },
  onNodeAdded: function(node) {

  },
  onBeforeElUpdated: function(fromEl, toEl) {
    return true;
  },
  onElUpdated: function(el) {

  },
  onBeforeNodeDiscarded: function(node) {
    return true;
  },
  onNodeDiscarded: function(node) {

  },
  onBeforeElChildrenUpdated: function(fromEl, toEl) {
    return true;
  },
  childrenOnly: false,
  skipFromChildren: function(fromEl, toEl) {
    return false;
  }
});

FAQ

Can I make morphdom blaze through the DOM tree even faster? Yes.

morphdom(fromNode, toNode, {
    onBeforeElUpdated: function(fromEl, toEl) {
        // spec - https://dom.spec.whatwg.org/#concept-node-equals
        if (fromEl.isEqualNode(toEl)) {
            return false
        }

        return true
    }
})

This avoids traversing through the entire subtree when you know they are equal. While we haven't added this to the core lib yet due to very minor concerns, this is an easy way to make DOM diffing speeds on par with virtual DOM.

Isn't the DOM slow?

UPDATE: As of v2.1.0, morphdom supports both diffing a real DOM tree with another real DOM tree and diffing a real DOM tree with a virtual DOM tree. See: docs/virtual-dom.md for more details.

No, the DOM data structure is not slow. The DOM is a key part of any web browser so it must be fast. Walking a DOM tree and reading the attributes on DOM nodes is not slow. However, if you attempt to read a computed property on a DOM node that requires a relayout of the page then that will be slow. However, morphdom only cares about the following properties and methods of a DOM node:

  • node.firstChild
  • node.nextSibling
  • node.nodeType
  • node.nodeName
  • node.nodeValue
  • node.attributes
  • node.value
  • node.selected
  • node.disabled
  • actualize(document) (non-standard, used to upgrade a virtual DOM node to a real DOM node)
  • hasAttributeNS(namespaceURI, name)
  • isSameNode(anotherNode)

What about the virtual DOM?

Libraries such as a React and virtual-dom solve a similar problem using a Virtual DOM. That is, at any given time there will be the real DOM (that the browser rendered) and a lightweight and persistent virtual DOM tree that is a mirror of the real DOM tree. Whenever the view needs to update, a new virtual DOM tree is rendered. The new virtual DOM tree is then compared with the old virtual DOM tree using a diffing algorithm. Based on the differences that are found, the real DOM is then "patched" to match the new virtual DOM tree and the new virtual DOM tree is persisted for future diffing.

Both morphdom and virtual DOM based solutions update the real DOM with the minimum number of changes. The only difference is in how the differences are determined. morphdom compares real DOM nodes while virtual-dom and others only compare virtual DOM nodes.

There are some drawbacks to using a virtual DOM-based approach even though the Virtual DOM has improved considerably over the last few years:

  • The real DOM is not the source of truth (the persistent virtual DOM tree is the source of truth)
  • The real DOM cannot be modified behind the scenes (e.g., no jQuery) because the diff is done against the virtual DOM tree
  • A copy of the real DOM must be maintained in memory at all times (albeit a lightweight copy of the real DOM)
  • The virtual DOM is an abstraction layer that introduces code overhead
  • The virtual DOM representations are not standardized and will vary by implementation
  • The virtual DOM can only efficiently be used with code and templating languages that produce a virtual DOM tree

The premise for using a virtual DOM is that the DOM is "slow". While there is slightly more overhead in creating actual DOM nodes instead of lightweight virtual DOM nodes, in practice there isnt much difference. In addition, as web browsers get faster the DOM data structure will also likely continue to get faster so there benefits to avoiding the abstraction layer.

Moreover, we have found that diffing small changes may be faster with actual DOM. As the diffing become larger, the cost of diffs slow down due to IO and virtual dom benefits begin to show.

See the Benchmarks below for a comparison of morphdom with virtual-dom.

Which is better: rendering to an HTML string or rendering virtual DOM nodes?

There are many high performance templating engines that stream out HTML strings with no intermediate virtual DOM nodes being produced. On the server, rendering directly to an HTML string will always be faster than rendering virtual DOM nodes (that then get serialized to an HTML string). In a benchmark where we compared server-side rendering for Marko (with Marko Widgets) and React we found that Marko was able to render pages ten times faster than React with much lower CPU usage (see: Marko vs React: Performance Benchmark)

A good strategy to optimize for performance is to render a template to an HTML string on the server, but to compile the template such that it renders to a DOM/virtual DOM in the browser. This approach offers the best performance for both the server and the browser. In the near future, support for rendering to a virtual DOM will be added to the Marko templating engine.

What projects are using morphdom?

morphdom is being used in the following projects:

  • Phoenix Live View (v0.0.1+) - Rich, real-time user experiences with server-rendered HTML
  • TS LiveView (v0.1.0+) - Build SSR realtime SPA with Typescript
  • Omi.js (v1.0.1+) - Open and modern framework for building user interfaces.
  • Marko Widgets (v5.0.0-beta+) - Marko Widgets is a high performance and lightweight UI components framework that uses the Marko templating engine for rendering UI components. You can see how Marko Widgets compares to React in performance by taking a look at the following benchmark: Marko vs React: Performance Benchmark
  • Catberry.js (v6.0.0+) - Catberry is a framework with Flux architecture, isomorphic web-components and progressive rendering.
  • Composer.js (v1.2.1) - Composer is a set of stackable libraries for building complex single-page apps. It uses morphdom in its rendering engine for efficient and non-destructive updates to the DOM.
  • yo-yo.js (v1.2.2) - A tiny library for building modular UI components using DOM diffing and ES6 tagged template literals. yo-yo powers a tiny, isomorphic framework called choo (v3.3.0), which is designed to be fun.
  • vomit.js (v0.9.19) - A library that uses the power of ES6 template literals to quickly create DOM elements that you can update and compose with Objects, Arrays, other DOM elements, Functions, Promises and even Streams. All with the ease of a function call.
  • CableReady(v4.0+) - Server Rendered SPAs. CableReady provides a standard interface for invoking common client-side DOM operations from the server via ActionCable.
  • Integrated Haskell Platform(all versions) - A complete platform for developing server-rendered web applications in Haskell.

NOTE: If you are using a morphdom in your project please send a PR to add your project here

Benchmarks

Below are the results on running benchmarks on various DOM transformations for both morphdom, nanomorph and virtual-dom. This benchmark uses a high performance timer (i.e., window.performance.now()) if available. For each test the benchmark runner will run 100 iterations. After all of the iterations are completed for one test the average time per iteration is calculated by dividing the total time by the number of iterations.

To run the benchmarks:

npm run benchmark

And then open the generated test-page.html in the browser to view the results:

file:///HOME/path-to-morphdom/test/mocha-headless/generated/test-page.html

The table below shows some sample benchmark results when running the benchmarks on a MacBook Pro (2.3 GHz Intel Core i5, 8 GB 2133 MHz LPDDR3). Remember, as noted above, the larger the diff needed to evaluate, the more vdom will perform better. The average time per iteration for each test is shown in the table below:

  • Total time for morphdom: 820.02ms
  • Total time for virtual-dom: 333.81ms (winner)
  • Total time for nanomorph: 3,177.85ms
morphdom nanomorph virtual-dom
attr-value-empty-string 0.01ms 0.02ms 0.01ms
change-tagname 0.01ms 0.00ms 0.01ms
change-tagname-ids 0.02ms 0.00ms 0.02ms
data-table 0.60ms 1.38ms 0.80ms
data-table2 2.72ms 12.42ms 0.84ms
equal 0.50ms 1.33ms 0.02ms
id-change-tag-name 0.03ms 0.04ms 0.04ms
ids-nested 0.08ms 0.02ms 0.01ms
ids-nested-2 0.03ms 0.02ms 0.05ms
ids-nested-3 0.03ms 0.01ms 0.02ms
ids-nested-4 0.04ms 0.04ms 0.03ms
ids-nested-5 0.04ms 0.08ms 0.06ms
ids-nested-6 0.03ms 0.03ms 0.03ms
ids-nested-7 0.02ms 0.03ms 0.02ms
ids-prepend 0.03ms 0.03ms 0.02ms
input-element 0.02ms 0.04ms 0.00ms
input-element-disabled 0.01ms 0.02ms 0.01ms
input-element-enabled 0.01ms 0.02ms 0.01ms
large 2.88ms 12.11ms 0.35ms
lengthen 0.03ms 0.15ms 0.04ms
one 0.01ms 0.03ms 0.01ms
reverse 0.03ms 0.05ms 0.02ms
reverse-ids 0.05ms 0.07ms 0.02ms
select-element 0.07ms 0.14ms 0.03ms
shorten 0.03ms 0.07ms 0.02ms
simple 0.02ms 0.05ms 0.02ms
simple-ids 0.05ms 0.09ms 0.04ms
simple-text-el 0.03ms 0.04ms 0.03ms
svg 0.03ms 0.05ms 0.01ms
svg-append 0.06ms 0.06ms 0.07ms
svg-append-new 0.02ms 0.02ms 0.18ms
svg-no-default-namespace 0.05ms 0.09ms 0.04ms
svg-xlink 0.01ms 0.05ms 0.00ms
tag-to-text 0.00ms 0.00ms 0.01ms
text-to-tag 0.00ms 0.00ms 0.01ms
text-to-text 0.00ms 0.01ms 0.00ms
textarea 0.01ms 0.02ms 0.01ms
todomvc 0.55ms 3.05ms 0.32ms
todomvc2 0.05ms 0.07ms 0.09ms
two 0.01ms 0.02ms 0.01ms

NOTE: Chrome 72.0.3626.121

Maintainers

Contribute

Pull Requests welcome. Please submit Github issues for any feature enhancements, bugs or documentation problems. Please make sure tests pass:

npm test

License

MIT

morphdom's People

Contributors

ahdinosaur avatar aknuds1 avatar autosponge avatar beenotung avatar bredele avatar cbbfcd avatar cfinucane avatar chrismccord avatar dependabot[bot] avatar dylanpiercey avatar eprev avatar fegu avatar fnd avatar gilesbowkett avatar howardroark avatar inad9300 avatar juliangruber avatar karfcz avatar maadhattah avatar megawac avatar melleb avatar mtyaka avatar okdistribute avatar oppianmatt avatar patrick-steele-idem avatar riim avatar snewcomer avatar steffende avatar williammizuta avatar zastavnitskiy avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

morphdom's Issues

innerHTML

I'm using document.registerElement to create custom elements. I'd like to update the contents of the element without touching the existing parent and without creating a new parent element. In essence, the effect of setting .innerHTML but with morphdom's diffing.

The childrenOnly option gets me half way there. For example, the following does what I want:

<!-- before -->
<my-dashboard>Hello</my-dashboard>
import morphdom from 'morphdom';
import {register} from '../../scripts/util/dom';

export default register('my-dashboard', HTMLElement, {
    createdCallback() {
        // called twice
    },

    attachedCallback() {
        this.render();
    },

    render() {
        morphdom(this, '<my-dashboard>World</my-dashboard>', {
            childrenOnly: true
        });
    }
});
<!-- after -->
<my-dashboard>World</my-dashboard>

However, two instances of <my-dashboard> are created in this case, so the createdCallback is fired twice. I only ever want one created. Ideally, I'd like to do something like this:

import morphdom from 'morphdom';
import {register} from '../../scripts/util/dom';

export default register('my-dashboard', HTMLElement, {
    createdCallback() {
        // called once
    },

    attachedCallback() {
        this.render();
    },

    render() {
        morphdom(this, 'World', {
            innerHTML: true
        });
    }
});

Would you be interested in a PR that adds this? I didn't see a way to implement this using the existing hooks.

Walking through the DOM is slow

You state that walking through the DOM isn't slow, but in reality it really is. Accessing things like firstNode or childNodes is vastly slower than storing a light-weight representation of it (aka virtual DOM).

Furthermore, handling of keyed nodes becomes problematic when using DOM nodes. There simply isn't any performant way of doing this when using the DOM as the source-of-truth.

Have you see this too? http://vdom-benchmark.github.io/vdom-benchmark/

Furthermore, you shouldn't really be comparing yourself to the virtual-dom library and stating that library as the de-facto virtual DOM standard, it's one of the slowest implementations of virtual DOM out there in Safari (it's noticeably faster than morphdom in Chrome according to your own benchmark).

Detect reorder of siblings

Any way to detect the reorder of siblings? Perhaps optionally to avoid a performance penalty for this edge case. I have a list of videos. If video 2 is playing and video 1 is removed or reordered, video 2 is stopped because its source attribute is changes.

Element tagName is not patching by id

I create test:

it('patch tags by id', function() {
    var el1 = document.createElement('div');
    el1.innerHTML = '<span id="boo" class="foo"></span>';

    var el2 = document.createElement('div');
    el2.innerHTML = '<div id="boo"></div>';

    var morphedEl = morphdom(el1, el2);

    expect(el1.innerHTML).to.equal('<div id="boo"></div>');
});

Failed:

   AssertionError: expected '<span id="boo"></span>' to equal '<div id="boo"></div>'
      + expected - actual

      -<span id="boo"></span>
      +<div id="boo"></div>

question: initial use case?

Hey @patrick-steele-idem

I was just wondering what was the initial use case, the one that made you create this library. I was thinking of marko async fragments, but then again marko only gives you missing pieces, not a complete "section" of DOM.

Thanks!

Removed xlink:href atribute in svg on v1.4.1

On version 1.4.1 I have a problem with patching svg.
In DOM:

<svg class="icon icon--pin">
    <use xmlns:xlink="http://www.w3.org/1999/xlink" xlink:href="#icon-pin" class="js-icon"></use>
</svg>

Patching remove xlink:href attribute.

I create test:

    it('should transform svg', function() {
        var el1 = document.createElement('div');
        el1.innerHTML = '<svg xmlns="http://www.w3.org/2000/svg" class="icon icon--search"><use xmlns:xlink="http://www.w3.org/1999/xlink" xlink:href="#icon-search" class="js-icon"></use></svg>';

        var el2 = document.createElement('div');
        el2.innerHTML = '<svg xmlns="http://www.w3.org/2000/svg" class="icon icon--search"><use xmlns:xlink="http://www.w3.org/1999/xlink" xlink:href="#icon-search" class="js-icon"></use></svg>';

        morphdom(el1, el2);
        expect(el1.innerHTML).to.equal('<svg xmlns="http://www.w3.org/2000/svg" class="icon icon--search"><use xmlns:xlink="http://www.w3.org/1999/xlink" xlink:href="#icon-search" class="js-icon"></use></svg>');
    });

Failed:

-<svg xmlns="http://www.w3.org/2000/svg" class="icon icon--search"><use class="js-icon"></use></svg>
+<svg xmlns="http://www.w3.org/2000/svg" class="icon icon--search"><use xmlns:xlink="http://www.w3.org/1999/xlink" xlink:href="#icon-search" class="js-icon"></use></svg>

List of modified nodes

It would be nice if we could get of list of modified elements, so its possible to animate or post-process it.

Potential onBeforeNodeDiscarded

I think there could be a use case for adding an onBeforeNodeDiscarded callback that could return false if the node should not be discarded. You can't use the onNodeDiscarded callback because that could be called on a deep node that isn't possible to retain in the tree. But if it can be retained (i.e. it's going to be removed from a node that we're keeping), then we can use the callback to allow it to be kept in the tree.

The reason I think this would be great is because it adds the ability for third party libraries to insert nodes without morphdom clearing them out.

Example use case:

A third party library I'm working with injects a DOM node when I use it. However, when I create a render a new HTML string, that node is not present in the render (because it was added by the third party lib, and is not a part of my template). Morphdom sees this new node, and notices it isn't present in the new markup, so it removes it.

In this case, you could just add a onBeforeNodeDiscarded callback that returns false on the injected node, thus keeping it in the tree.

300ms morphcom call, followed by 300ms (program) (DOM) call in profiler

https://gist.github.com/anonymous/71cec266f8e04f3bf05f1fc67116c5a1

Here is a snippet of the HTML. If you could, repeat the <tr>s until you reach about 1.5MB.

This acts as #pick_table.

Some very small changes are made (background color, maybe a 0 to 1 in quantity, etc.)

I generate the HTML as a string inside of my app. I then flush it straight to the DOM.

Offending code:

var element = document.createElement('tbody');

element.innerHTML = html;

morphdom(document.querySelector('#pick_table tbody'), element);

Is there any way I can improve the performance? It's 300ms on my speedy MacBook Pro. I have customers complaining (this is code in a production app) that it takes 5s+ :/

I'm sure I'm doing something wrong, so any input would be greatly appreciated! Please let me know if I can provide any further information.

What about using TreeWalker and google/incremental-dom?

Hi i've just seen your project in the github trending projects page,
and it triggered my attention because i was in the middle of starting a very similar one.

While searching about how to implement my own in the last days it i've found this:

to read/cycle the source dom tree (in modern browsers) probably is faster to use this api than manually and recursively cycle the dom tree in js (fallback to this method required for old browsers):
https://developer.mozilla.org/en-US/docs/Web/API/TreeWalker

to apply changes in the destination dom tree,
lately seems google have developed a lib to do exactly that:
https://github.com/google/incremental-dom

seems your code does not use this things,
have you considered this options?
there is something wrong in this approach for you?

Support diffing #document-fragment

Would it be possible to support the following use case:

// Example element, could be a reference to an existing DOM node.
var element = document.createElement('div');
element.innerHTML = '<span>Hello</span>';

// Build the fragment to match the children.
var fragment = document.createDocumentFragment();
fragment.appendChild(document.createElement('span'));
fragment.firstChild.appendChild(document.createTextNode('World'));

// This currently does not error, but does not diff the children.
morphdom(element, fragment);

Example using diffHTML: tbranyen/virtual-list@5500f1c#diff-9ec5f0d45b1fdec47275a0f431b03f62R75

I'd like to make this work with morphdom as well.

Button element loses class when removing name attribute in IE, Edge

Some weird bug occurs in IE only (8-11 and Edge as well). When morphing a button element with a class and a name attribute to a button that only has the class but not the name, the resulting button will have an empty class attribute.

The critical part of code is a call of removeAttributeNode in morphAttrs function:

https://github.com/patrick-steele-idem/morphdom/blob/master/lib/index.js#L190

I don't know the reason for use of removeAttributeNode method, but I tried using removeAttribute instead and it seems to fix the bug.

The same issue can be reproduced with this simple snippet:

var btn = document.createElement('button');
document.body.appendChild(btn);
btn.setAttribute('class', 'btn');
btn.setAttribute('name', 'myButton');
btn.innerHTML = "TEST";
btn.removeAttributeNode(btn.getAttributeNode('name'));
console.log(btn.getAttribute('class') === 'btn'); // => false!

tests fail

this happens after a fresh npm install:

$ npm test

> [email protected] test /Users/julian/dev/juliangruber/morphdom
> npm run test-browser && npm run lint


> [email protected] test-browser /Users/julian/dev/juliangruber/morphdom
> node test/mocha-phantomjs/run.js test

Preparing client-side tests...
Config: { runBenchmarks: false, runTests: true }
/Users/julian/dev/juliangruber/morphdom/test/mocha-phantomjs/test-page.marko.js:13
    lasso_page({
    ^

TypeError: lasso_page is not a function
    at render (/Users/julian/dev/juliangruber/morphdom/test/mocha-phantomjs/test-page.marko.js:13:5)
    at Object.Template.render (/Users/julian/dev/juliangruber/morphdom/node_modules/marko/runtime/marko-runtime.js:192:9)
    at Object.Template.(anonymous function) [as render] (/Users/julian/dev/juliangruber/morphdom/node_modules/marko/hot-reload/index.js:84:34)
    at run (/Users/julian/dev/juliangruber/morphdom/test/mocha-phantomjs/run.js:107:18)
    at Object.<anonymous> (/Users/julian/dev/juliangruber/morphdom/test/mocha-phantomjs/run.js:135:1)
    at Module._compile (module.js:397:26)
    at Object.Module._extensions..js (module.js:404:10)
    at Module.load (module.js:343:32)
    at Function.Module._load (module.js:300:12)
    at Function.Module.runMain (module.js:429:10)

js-repaint-perfs / dbmonster

Hey @patrick-steele-idem ,

Continuing mathieuancelin/js-repaint-perfs#53 so not to clutter the pull req with OT discussion...

I'm seeing nearly identical performance compared to React

React is quite slow (not sure how much faster v15.0 is). Though you could make the argument that it doesn't matter from its popularity.

morphdom is tiny (network overhead is often more important... especially in developing countries)

Well, 7.7k (morphdom) [1] vs 10.9k (domvm) [2] or others [3][4][5] is a negligible difference. Even less so after gzip. Developing countries also have slow, shitty phones :)

While the dbmon benchmark is interesting it is just one use case that is likely not representative of real world usage

True, it's unrealistic for 100%, 50% and even 20% mutations, but is realistic for 0%-5% mutations.

For example, morphdom provides hooks to avoid diffing/patching DOM subtrees (e.g., onBeforeUpdateEl)

To be fair, many (most?) vdom libs also provide this.

morphdom avoids the memory overhead of maintaining a parallel virtual DOM tree in memory

From the example [6] at least, it looks like it actually creates a much more expensive detached DOM to do the diff, no?

morphdom plays nice with vanilla DOM libraries and web components...morphdom can be used with any library that produces HTML or DOM nodes..[doesn't rely on non-standardized vtree represenations, treats DOM as the truth]

This is really the main selling point, which is nice.

morphdom will get faster as browsers optimize the DOM

True, perhaps one day: https://air.mozilla.org/bay-area-rust-meetup-february-2016/#@25m50s

Using morphdom for animation is not recommended (use hardware accelerated CSS transitions or an animation library instead)

I think that using any vdom lib for tick/step based animations is terrible practice.

Disclaimer: I'm the author of domvm [7]...dbmonster [8] (optimized a bit for low mutations yields 145fps @ 1%) or dumb re-diff everything [9] (70fps @ 1%)

[1] https://github.com/patrick-steele-idem/morphdom/blob/master/dist/morphdom-umd.min.js
[2] https://gist.github.com/leeoniya/baa5b971f3421d2a628729e456766b8e
[3] https://github.com/paldepind/snabbdom
[4] https://github.com/joelrich/citojs
[5] https://github.com/developit/preact
[6] https://github.com/patrick-steele-idem/morphdom#usage
[7] https://github.com/leeoniya/domvm
[8] http://leeoniya.github.io/domvm/test/bench/dbmonster/
[9] https://github.com/mathieuancelin/js-repaint-perfs/blob/gh-pages/domvm/app.js

Support for IE7+

Current version makes use of document.createRange and Element.prototype.hasAttribute, which are unavailable on older browsers.

createRange can be skipped because the fallback for creating a fragment works without it.

hasAttribute on the other hand can't be easily replaced, but for the use case of the INPUT handler, I would suggest simply checking the getAttribute response for a truthy value.

As far as I've tested, everything else is working fine.

I will submit a pull request with those changes.

Why create an element with a unique id, which is already available?

<!DOCTYPE html>
<html>
<body>

<script src="node_modules/morphdom/dist/morphdom-umd.js"></script>
<script>

var fromEl = document.createElement('div');
var toEl = document.createElement('div');

fromEl.innerHTML = '<div id="qwerty" class="div1"></div>';
toEl.innerHTML = '<div><div id="qwerty" class="div1"></div></div>';

var div1 = fromEl.querySelector('.div1');

morphdom(fromEl, toEl);

var div1_2 = fromEl.querySelector('.div1');

console.log(div1 == div1_2); // false ???, should be true

</script>

</body>
</html>

Uncaught TypeError: Cannot read property 'replaceChild' of null

I ran into this today. I tracked it down, but it wasn't obvious, so I'm posting to help anyone else who may get the same issue.

The problem was caused by generating duplicate ids. When morphdom creates an internal map it uses the id. When it reaches a second node with the same id, it seems to pull from the map. If that node is already removed from the dom, you'll get this error.

To track it down, render the view in question without running through morphdom or do so in a way that does not remove nodes. Then use a tool like HTML Codesniffer to find problems in your markup.

Cheers.

Strings with Space/Whitespace-Padding won't be parsed correctly

This works:

morphdom($("#out"),
  `<div id="out">
    Foo
  </div>`

Howerever this doesn't:

morphdom($("#out"), `
  <div id="out">
    Foo
  </div>`

(not the position of the opening Backtick-Quote)

This als won't work:

morphdom($("#out"), ` <div id="out">Foo</div>`

(Note the leading Space in the string)

With $ beeing

const $ = document.querySelector.bind(document);

TypeError: 'null' is not an object (evaluating 'morphedNode.nodeType')

We have a setup where we are running tests on marko widgets, and are seeing the issue below :

PhantomJS 1.9.8 (Mac OS X 0.0.0)
TypeError: 'null' is not an object (evaluating 'morphedNode.nodeType') (/path/to/project/.test/lasso/static/node_modules/marko-widgets/node_modules/morphdom/lib/index.js:373)

In addition to this we are also seeing:

ERROR: '(node) warning: possible EventEmitter memory leak detected. %d listeners added. Use emitter.setMaxListeners() to increase limit.', 11

Causing all the tests the fail.

Please create a bower package

It is not currently possible to install this library using bower because

  • it is not registered at the bower registry
  • if installing via the github shorthand, the UMD build is not included

Would be great if it was possible to use bower for installation.

Efficient update lists using the key attribute

https://facebook.github.io/react/docs/multiple-components.html#dynamic-children

Hi!
Sorry, translated with translate.google.ru ((

This update faster. In addition, if the nodes are attached to component instances will be destroyed is not correct instances.
For example here https://gist.github.com/Riim/e47127fc4a88cf1e0ff9 (rista.js is this file https://github.com/Riim/rista/blob/37974a40fc83a7127af96e513a67a1fae080694c/rista.js) second element of the list todos is removed after three seconds, but dispose triggered on the third instance.

Lifecycle of elements

I've been trying to work out a sensible way to handle the lifecycle of elements. It would be useful to know when an element is added to the document, when it's morphed, and when it's discarded. Though this functionality exists (aside from creation), it's not very well set up to allow you to respond to these changes on a per-element basis.

var button = document.createElement('button')
button.on('update', function () {
  button.focus()
})

Some other inspiration...
https://github.com/substack/virtual-hyperscript-mount
https://github.com/substack/virtual-hyperscript-hook
virtual-dom: https://github.com/Matt-Esch/virtual-dom/blob/master/docs/hooks.md
React: http://facebook.github.io/react/docs/component-specs.html

What do you think? I would like to put a library together that wraps morphdom to provide this functionality, but I'm not sure if I've got the wrong idea.

Incorrect premise in readme

Applaud your work, and looks like a great project!

I just want to pick a little hole in your premise for why people use things like react - it's categorically not because the DOM is slow, but more to do with the logic around handling competing updates to a nested tree structure.

Expanding on this example - a set of nested views. A call to render higher up the tree should (probably) not cause the child views to re-render. It is of course possible to compute this logic, store references, and replace them or not depending on this, but the code to do so is often fragile and has its own series of abstractions. I use react/reactish library x because I don't have to worry about any of this. At no point have I ever considered the DOM to be slow - in fact, I accept the compromise that react is in general slower than direct DOM manipulation because of the lighter mental overhead and stronger abstractions when building complex stuff.

Does this make sense..?

DOM is altered unnecessarily in the presence of comments

I noticed that morphdom was updating my DOM way more than I expected, and narrowed it down to the presence of comments. Here's a simple test I added that fails (pull request with naive fix to follow).

 it('should not alter the DOM unnecessarily in the presence of comments', function() {
     var el1 = document.createElement('div');
     el1.innerHTML = '<!-- comment --><div class="div1">a</div>';

     var el2 = el1.cloneNode(true);

     var addedNodes = [];
     var discardedNodes = [];
     morphdom(el1, el2, {
         onNodeAdded: function(el) { addedNodes.push(el) },
         onNodeDiscarded: function(el) { discardedNodes.push(el) }
     });

     // This was a no-op update, and should have made no changes
     expect(addedNodes).to.be.empty;
     expect(discardedNodes).to.be.empty;
 });

Input lose focus position after morphing

Please watch this video:
http://youtu.be/X33mMzkzR-A

HTML:
    <input type="text"/>

Javascript:
   var input = document.querySelector('input');
   input.addEventListener('input', function (e) {
       var newNode = document.createElement('input');
       newNode.value = e.currentTarget.value;
       morphdom(e.currentTarget, newNode);
  });

I know it's hard correct handle same cases, but it's very useful feature, because you can easy make two-way data-binding and don't care about "dirty hacks".

Textarea problems

Textarea updates doesn't work after first input text.

   <textarea></textarea>

    <script>
    document.querySelector('textarea').addEventListener('input', function(e) {
      var textarea = document.createElement('textarea');
      textarea.innerHTML = 'Hello World';
      console.log(textarea, e.currentTarget);
      morphdom(e.currentTarget, textarea);
    }, false);
   </script>

After first input
2015-08-20 18 18 23

After second input
2015-08-20 18 18 15

Look diffirence between shadow-dom and innerHTML
2015-08-20 18 17 06

I hope it help you understand issue.

2015-08-20 18 22 07

Willing to add bower file and use the UMD technique?

First of all... thanks so much for making this!!!!!

Since learning about virtual dom I though it was such an ingenious idea for solving the problems you mention with replacing innerHTML as your state changes. The issue is that all the stuff out there requires a paradigm shift in terms of how I currently approach things. I just like the idea of templates more JSX. Then I found this!

When building browser apps I like to just use bower and browser "globals" vs trying to pack everything into an isolated context. I tend to feel the window object is a straight forward context for keeping your 3rd party modules. Also makes working with jQuery stuff a lot more simple. I still keep the core app in it's own isolated context and pass an API to the window... Maybe I'm stuck in the old school.

I was wondering if you would be up for adding a bower file to this project and set it up to use the "UMD" trick to offer core methods as "globals" as well as module exports.

Thanks!!!

Enhancement: support diffing the real DOM with a virtual DOM

In theory, the new target DOM could be a virtual DOM. As long as the virtual DOM exposes an interface that implements the required subset of the real DOM API (node.firstChild, node.tagName, node.nextSibling, node.attributes, etc.) then things will work for the most part. The only difference is that if a virtual DOM node is to be inserted into the real DOM then it will need to be upgraded into a real DOM node. This would add very little overhead to morphdom while also making it more flexible.

If an application is able to efficiently produce a virtual DOM then that application will likely see better performance.

Pull Requests welcome.

Better <head> element support

Morphing <head> element should have a different algorithm because we have a set of immutable elements such as: <style>, <script> and <link rel="stylesheet">. Changing these element's attributes or re-creating of these elements causes style flickering and script reloading which breaks any app.

My proposal would be:

  • Identify such nodes using its content and, additionally, src value for <script> and href value for <link rel="stylesheet">. We need this for handling attributes re-ordering and changing of those attributes which are irrelevant for script or style reloading.
  • Accumulate <script>, <style> and <link rel="stylesheet"> nodes inside <head> element if morphing to a new state requires to remove them.

For example,

We have a head element:

<head>
    <title>First title</title>
    <style type="text/css">some styles1</style>
    <script type="application/javascript">some scripts1</script>
    <script type="application/javascript" src="someScriptSrc1"></script>
    <link rel="stylesheet" href="someStyleLink1">
    <link rel="author" href="Author">
    <meta name="name1" content="value1">
</head>

New state is:

<head>
    <title>Second title</title>
    <style type="text/css">some styles2</style>
    <script type="application/javascript">some scripts2</script>
    <script type="application/javascript" src="someScriptSrc2"></script>
    <link rel="stylesheet" href="someStyleLink2">
    <link rel="author" href="New Author">
    <meta name="name1" content="value2">
</head>

After morphing we have:

<head>
    <title>Second title</title>
    <style type="text/css">some styles1</style>
    <style type="text/css">some styles2</style>
    <script type="application/javascript">some scripts1</script>
    <script type="application/javascript">some scripts2</script>
    <script type="application/javascript" src="someScriptSrc1"></script>
    <script type="application/javascript" src="someScriptSrc2"></script>
    <link rel="stylesheet" href="someStyleLink1">
    <link rel="stylesheet" href="someStyleLink2">
    <link rel="author" href="New Author">
    <meta name="name1" content="value2">
</head>

As far as I use morphdom inside Catberry.js, I have to implement a separate algorithm for solving this problem, you can see it here. But I believe you would come up with a better solution using morphdom approach.

Events

How do you handle events using morphdom?

use informations of native dom

Native node has more informations that a virtual node.
What do you think about using outerHTML and cssText String for a comparison between 2 nodes first of iterate attributes and childs for know if something is changed?

SVG / NS Support

Hello. Is it possible for morphdom to support creating elements with namespace? I've tried to morph my SVG and I had svg element on the page, but it had namespaceURI which does not correspond to SVG, so element is present but not working.

Full page morph.

Currently using jsdom for testing which could be part of the issue, but the following diff detaches the document element and fails.

morphdom(document.documentElement, `
    <!DOCTYPE html>
    <html>
        <head>
            <title>Test</title>
        </head>
        <body>content</body>
    </html>
`);

Am I doing something wrong? Even tried removing the doctype.


Edit:

Also tried doing this directly in Chrome Version 51.0.2680.0 canary (64-bit) and received the following error.

VM414:479 Uncaught DOMException: Failed to execute 'replaceChild' on 'Node': Nodes of type '#text' may not be inserted inside nodes of type '#document'

From:
https://github.com/patrick-steele-idem/morphdom/blob/master/lib/index.js#L478


Edit2:

Looks like the issue stems from https://github.com/patrick-steele-idem/morphdom/blob/master/lib/index.js#L38 and https://github.com/patrick-steele-idem/morphdom/blob/master/lib/index.js#L41 in my case, but it looks like #toElement just doesn't support anything outside of body.

Form selects resets to first option while morphing

Not sure whether this is bug or correct behavior - if I'm morphing DOM tree containing SELECT element, first option (or option with selected attribute) is always set while morphing that tree, event if this particular element nor his children are changed.

HTML comments breaks morph

Hi all, at first great work!

I came across with something that seems to be a issue,
consider the following:

var el = document.createElement('div');
el.className = 'foo';
el.innerHTML = 'bar';
document.body.appendChild(el); 

var updates = '<!-- some comment --> <div class="foo">bar</div>';
morphdom(el, updates);

the el variable behaves exactly as it should be:

expect(el.className).to.equal('foo'); // true

but in the dom what I actually got is this:

image

I am not sure if this is a bug or an expected behavior,
any insights?

update deprecated deps

getting a couple warnings on a fresh npm install:

npm WARN deprecated [email protected]: Package not maintained. Recent browserify uses https://github.com/feross/buffer
npm WARN engine [email protected]: wanted: {"node":"~0.10.0"} (current: {"node":"5.4.1","npm":"3.3.12"})
npm WARN deprecated [email protected]: Jade has been renamed to pug, please install the latest version of pug instead of jade
npm WARN deprecated [email protected]: graceful-fs v3.0.0 and before will fail on node releases >= v7.0. Please update to graceful-fs@^4.0.0 as soon as possible. Use 'npm ls graceful-fs' to find it in the tree.
npm WARN deprecated [email protected]: this package has been reintegrated into npm and is now out of date with respect to npm

isEqualNode waiting for polyfill?

I noticed the following commented code:

    // XXX optimization: if the nodes are equal, don't morph them
    /*
    if (fromNode.isEqualNode(toNode)) {
      return fromNode;
    }
    */

This seems entirely reasonable. If this is waiting on a polyfill for <IE9, I suggest just returning false. It won't be any slower or faster in IE but it could speed up other browsers. If there's another reason, then by all means, explain :)

Enhancement: Option to keep values

When updating the dom sometimes we want to keep existing input values. Would be handy to have an option that kept the existing 'value' attribute and only override it if a new one is in the new dom.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.