├── .gitignore ├── CONCEPT.md ├── LICENSE ├── README.md ├── dist ├── idiomorph-ext.js ├── idiomorph-ext.min.js ├── idiomorph-htmx.js ├── idiomorph.js ├── idiomorph.min.js └── idiomorph.min.js.gz ├── img └── comparison.png ├── package-lock.json ├── package.json ├── src ├── idiomorph-htmx.js └── idiomorph.js └── test ├── bootstrap.js ├── core.js ├── demo ├── demo.html ├── fullmorph.html ├── fullmorph2.html ├── ignoreActiveIdiomorph.html ├── rickroll-idiomorph.gif ├── scratch.html └── video.html ├── fidelity.js ├── head.js ├── htmx ├── above.html ├── below.html ├── htmx-demo.html └── htmx-demo2.html ├── index.html ├── lib └── morphdom.js ├── perf.js ├── perf ├── checkboxes.end ├── checkboxes.start ├── perf1.end ├── perf1.start ├── table.end └── table.start └── test-utilities.js /.gitignore: -------------------------------------------------------------------------------- 1 | /node_modules/ 2 | .idea -------------------------------------------------------------------------------- /CONCEPT.md: -------------------------------------------------------------------------------- 1 | # Concept 2 | 3 | This project will create a JavaScript DOM-merging algoritm that, given two nodes, `oldNode` and `newNode`, will merge information from `newNode` and its children into `oldNode` and it's children in a way that tries to minimize the number of nodes disconnected from the DOM. 4 | 5 | The reason we want to minimize node disconnection (either moves or replacements) is that browsers do not keep state well when nodes are moved or replaced in the DOM: focus is lost, video elements stop playing etc. A good merge algorithm should update the DOM to match `newNode` with as few disconnects as possible. 6 | 7 | ## Existing Solutions 8 | 9 | There are two major existing solutions to this problem: 10 | 11 | * Morphdom - https://github.com/patrick-steele-idem/morphdom 12 | * The original solution to this problem, heavily used by other libraries 13 | * Nanomorph 14 | * A simplified version of Morphdom, a bit easier to understand 15 | 16 | ## Overview Of Current Algorithms 17 | 18 | The fundamental DOM merging algorithm is difficult to innovate on because, in general, we want to minimize the number of moves in the DOM. This means we can't use a more general tree merge algorithm: rather than trying to find a best match, we need to match the new structure up with the old structure as much as is possible. 19 | 20 | The basic, high-level algorithm is as follows: 21 | 22 | ``` 23 | if the new node matches the old node 24 | merge the new node attributes onto the old node 25 | otherwise 26 | replace the old node with the new node 27 | for each child of the new node 28 | find the best match in the children of the old nodes and merge 29 | ``` 30 | 31 | The merging of children is the trickiest aspect of this algorithm and is easiest to see in the nanomorph algorithm: 32 | 33 | https://github.com/choojs/nanomorph/blob/b8088d03b1113bddabff8aa0e44bd8db88d023c7/index.js#L77 34 | 35 | Both nanomorph and morphdom attempt to minimize the runtime of their algorithms, which leads to some fast, but difficult to understand code. 36 | 37 | In the nanomorph code you can see a few different cases being handled. An important concept in the notion of "sameness", which nanomorph uses to determine if a new node can be merged into an existing older node. Here is the javascript for this function: 38 | 39 | ```js 40 | function same (a, b) { 41 | if (a.id) return a.id === b.id 42 | if (a.isSameNode) return a.isSameNode(b) 43 | if (a.tagName !== b.tagName) return false 44 | if (a.type === TEXT_NODE) return a.nodeValue === b.nodeValue 45 | return false 46 | } 47 | ``` 48 | 49 | Note that this is mostly a test of id equivalence between elements. 50 | 51 | This notion of sameness is used heavily in the `updateChildren`, and is an area where idiomorph will innovate. 52 | 53 | One last element of the nanomorph algorithm to note is that, if an element _doesn't_ have an ID and a potential match _doesn't_ have an id, the algorithm will attempt to merge or replace the current node: 54 | 55 | https://github.com/choojs/nanomorph/blob/b8088d03b1113bddabff8aa0e44bd8db88d023c7/index.js#L141 56 | 57 | This is another area where we believe idiomorph can improve on the existing behavior. 58 | 59 | ### Improvements 60 | 61 | The first area where we feel that we may be able to improve on the existing algorigthms is in the notion of "sameness". Currently, sameness is tied very closely to to elements having the same ID. However, it is very common for ids to be sparse in a given HTML tree, only inluded on major elements. 62 | 63 | In order to further improve the notion of sameness, we propose the following idea: 64 | 65 | ``` 66 | For each element in the new content 67 | compute the set of all ids contained within that element 68 | ``` 69 | 70 | This can be implemented as an efficient bottom-up algorithm: 71 | 72 | ``` 73 | For all elements with an id in new content 74 | Add the current elements id to its id set 75 | For each parent of the current element up to the root new content node 76 | Add the current elements id to the parents id set 77 | ``` 78 | 79 | With the correct coding, this should give us a map of elements to the id sets associated with those elements. 80 | 81 | Using this map of sets, we can now adopt a broader sense of "sameness" between nodes: two nodes are the same if they have a non-empty intersection of id sets. This allows children nodes to contribute to the sense of the sameness of a node without requiring a depth-first exploration of children while merging nodes. 82 | 83 | Furthermore, we can efficiently ask "does this node match any other nodes" efficiently: 84 | 85 | Given a old node N, we can ask if it might match _any_ child of a parent node P by computing the intersection of the id sets for N and P. If this is non-empty, then there is likely a match for N somewhere in P. This is because P's id set is a superset of all its childrens id sets. 86 | 87 | Given these two enhancements: 88 | 89 | * The ability to ask if two nodes are the same based on child information efficiently 90 | * The abilit to ask if a node has a match within an element efficiently 91 | 92 | We believe we can improve on the fidelity of DOM matching when compared with plain ID-based matching. 93 | 94 | ## Pseudocode 95 | 96 | Below is a rough sketch of the algorithm: 97 | 98 | ``` 99 | 100 | Let oldNode be the existing DOM node 101 | Let newNode be the new DOM to be merged in place of oldNode 102 | 103 | Sync the attributes of newNode onto oldNode 104 | 105 | Let insertionPoint be the first child of the oldNode 106 | while newNode has children 107 | let newChild be the result of shifting the first child from newNodes children 108 | if the newChild is the same as the insertionPoint 109 | recursively merge newChild into insertionPoint 110 | advance the insertionPoint to its next sibling 111 | else if the newChild has a match within the oldNode 112 | scan the insertionPoint forward to the match, discarding children of the old node 113 | recursively merge newChild into insertionPoint 114 | advance the insertionPoint to its next sibling 115 | else if the insertionPoint has a match within the newNode 116 | insert the newChild before the insertionPoint 117 | else if the insertionPoint and the newChild are compatible 118 | recursively merge newChild into insertionPoint 119 | advance the insertionPoint to its next sibling 120 | else 121 | insert the newChild before the insertionPoint 122 | end 123 | 124 | while insertionPoint is not null 125 | remove the insertion point and advance to the next sibling 126 | 127 | ``` -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | BSD 2-Clause License 2 | 3 | Copyright (c) 2022, Big Sky Software 4 | All rights reserved. 5 | 6 | Redistribution and use in source and binary forms, with or without 7 | modification, are permitted provided that the following conditions are met: 8 | 9 | 1. Redistributions of source code must retain the above copyright notice, this 10 | list of conditions and the following disclaimer. 11 | 12 | 2. Redistributions in binary form must reproduce the above copyright notice, 13 | this list of conditions and the following disclaimer in the documentation 14 | and/or other materials provided with the distribution. 15 | 16 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 17 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 18 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 19 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 20 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 21 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 22 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 23 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 24 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 25 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 26 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Idiomorph 2 | 3 | Idiomorph is a javascript library for morphing one DOM tree to another. It is inspired by other libraries that 4 | pioneered this functionality: 5 | 6 | * [morphdom](https://github.com/patrick-steele-idem/morphdom) - the original DOM morphing library 7 | * [nanomorph](https://github.com/choojs/nanomorph) - an updated take on morphdom 8 | 9 | Both morphdom and nanomorph use the `id` property of a node to match up elements within a given set of sibling nodes. When 10 | an id match is found, the existing element is not removed from the DOM, but is instead morphed in place to the new content. 11 | This preserves the node in the DOM, and allows state (such as focus) to be retained. 12 | 13 | However, in both these algorithms, the structure of the _children_ of sibling nodes is not considered when morphing two 14 | nodes: only the ids of the nodes are considered. This is due to performance: it is not feasible to recurse through all 15 | the children of siblings when matching things up. 16 | 17 | ## id sets 18 | 19 | Idiomorph takes a different approach: before node-matching occurs, both the new content and the old content 20 | are processed to create _id sets_, a mapping of elements to _a set of all ids found within that element_. That is, the 21 | set of all ids in all children of the element, plus the element's id, if any. 22 | 23 | Id sets can be computed relatively efficiently via a query selector + a bottom up algorithm. 24 | 25 | Given an id set, you can now adopt a broader sense of "matching" than simply using id matching: if the intersection between 26 | the id sets of element 1 and element 2 is non-empty, they match. This allows Idiomorph to relatively quickly match elements 27 | based on structural information from children, who contribute to a parent's id set, which allows for better overall matching 28 | when compared with simple id-based matching. 29 | 30 | ## Usage 31 | 32 | Idiomorph is a small (1.7k min/gz'd), dependency free JavaScript library, and can be installed via NPM or your favorite 33 | dependency management system under the `Idiomorph` dependency name. You can also include it via a CDN like unpkg to 34 | load it directly in a browser: 35 | 36 | ```html 37 | 38 | ``` 39 | 40 | Or you can download the source to your local project. 41 | 42 | Idiomorph has a very simple usage: 43 | 44 | ```js 45 | Idiomorph.morph(existingNode, newNode); 46 | ``` 47 | 48 | This will morph the existingNode to have the same structure as the newNode. Note that this is a destructive operation 49 | with respect to both the existingNode and the newNode. 50 | 51 | You can also pass string content in: 52 | 53 | ```js 54 | Idiomorph.morph(existingNode, "
A
181 |B
184 |B
194 |A
197 |