{"id":13515169,"url":"https://github.com/bigskysoftware/idiomorph","last_synced_at":"2025-05-14T02:05:29.018Z","repository":{"id":59171541,"uuid":"528515990","full_name":"bigskysoftware/idiomorph","owner":"bigskysoftware","description":"A DOM-merging algorithm","archived":false,"fork":false,"pushed_at":"2025-03-06T03:24:51.000Z","size":14259,"stargazers_count":848,"open_issues_count":15,"forks_count":42,"subscribers_count":9,"default_branch":"main","last_synced_at":"2025-04-11T00:42:43.321Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":null,"language":"HTML","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"other","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/bigskysoftware.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":"ROADMAP.md","authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2022-08-24T16:58:08.000Z","updated_at":"2025-04-09T18:08:00.000Z","dependencies_parsed_at":"2024-01-08T21:53:50.752Z","dependency_job_id":"101bb9cd-42d5-43d4-8133-ab3a28a2bd51","html_url":"https://github.com/bigskysoftware/idiomorph","commit_stats":{"total_commits":155,"total_committers":20,"mean_commits":7.75,"dds":0.4193548387096774,"last_synced_commit":"d118b2b7b62143052a8236f4ae73f0d61a37e61a"},"previous_names":[],"tags_count":14,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/bigskysoftware%2Fidiomorph","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/bigskysoftware%2Fidiomorph/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/bigskysoftware%2Fidiomorph/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/bigskysoftware%2Fidiomorph/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/bigskysoftware","download_url":"https://codeload.github.com/bigskysoftware/idiomorph/tar.gz/refs/heads/main","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":254052692,"owners_count":22006716,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":[],"created_at":"2024-08-01T05:01:07.068Z","updated_at":"2025-05-14T02:05:24.005Z","avatar_url":"https://github.com/bigskysoftware.png","language":"HTML","funding_links":[],"categories":["HTML","JavaScript"],"sub_categories":[],"readme":"\u003ch1 style=\"font-family: Verdana,sans-serif;\"\u003e♻️ Idiomorph\u003c/h1\u003e\n\nIdiomorph is a javascript library for morphing one DOM tree to another.  It is inspired by other libraries that \npioneered this functionality:\n\n* [morphdom](https://github.com/patrick-steele-idem/morphdom) - the original DOM morphing library\n* [nanomorph](https://github.com/choojs/nanomorph) - an updated take on morphdom\n\nBoth morphdom and nanomorph use the `id` property of a node to match up elements within a given set of sibling nodes.  When\nan id match is found, the existing element is not removed from the DOM, but is instead morphed in place to the new content.\nThis preserves the node in the DOM, and allows state (such as focus) to be retained. \n\nHowever, in both these algorithms, the structure of the _children_ of sibling nodes is not considered when morphing two \nnodes: only the ids of the nodes are considered.  This is due to performance: it is not feasible to recurse through all \nthe children of siblings when matching things up. \n\n## id sets\n\nIdiomorph takes a different approach: before node-matching occurs, both the new content and the old content\nare processed to create _id sets_, a mapping of elements to _a set of all ids found within that element_.  That is, the\nset of all ids in all children of the element, plus the element's id, if any.\n\nId sets can be computed relatively efficiently via a query selector + a bottom up algorithm.\n\nGiven an id set, you can now adopt a broader sense of \"matching\" than simply using id matching: if the intersection between\nthe id sets of element 1 and element 2 is non-empty, they match.  This allows Idiomorph to relatively quickly match elements\nbased on structural information from children, who contribute to a parent's id set, which allows for better overall matching\nwhen compared with simple id-based matching.\n\nA testimonial:\n\n\u003e We are indeed using idiomorph and we'll include it officially as part of [Turbo 8](https://turbo.hotwired.dev/). We \n\u003e started with morphdom, but eventually switched to idiomorph as we found it way more suitable. It just worked great \n\u003e with all the tests we threw at it, while morphdom was incredibly picky about \"ids\" to match nodes. Also, we noticed \n\u003e it's at least as fast.\n\u003e \n\u003e -- [Jorge Marubia](https://www.jorgemanrubia.com/) / [37Signals](https://37signals.com/)\n\n## Installing\n\nIdiomorph is a small (3.2k min/gz'd), dependency free JavaScript library.  The `/dist/idiomorph.js` file can be included\ndirectly in a browser:\n\n```html\n\u003cscript src=\"https://unpkg.com/idiomorph@0.7.3\"\u003e\u003c/script\u003e\n```\n\nFor production systems we recommend downloading and vendoring the library.\n\nIf you are using [JavaScript Modules](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Modules), we provide \ntwo additional files:\n\n* `dist/idiomorph.cjs.js` - for [CommonJS-style modules](https://wiki.commonjs.org/wiki/Modules)\n* `dist/idiomorph.esm.js` - for [ESM-style modules](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Modules)\n\nIdiomorph can be installed via NPM or your favorite dependency management system under the `idiomorph` dependency \nname.\n\n```js\nrequire(\"idiomorph\"); // CommonJS\nimport \"idiomorph\"; // ESM\n```\n\n## Usage\n\nIdiomorph has a very simple API:\n\n```js\n  Idiomorph.morph(existingNode, newNode);\n```\n\nThis will morph the existingNode to have the same structure as the newNode.  Note that this is a destructive operation\nwith respect to both the existingNode and the newNode.\n\nYou can also pass string content in as the second argument, and Idiomorph will parse the string into nodes:\n\n```js\n  Idiomorph.morph(existingNode, \"\u003cdiv\u003eNew Content\u003c/div\u003e\");\n```\n\nAnd it will be parsed and merged into the new content.\n\nIf you wish to target the `innerHTML` rather than the `outerHTML` of the content, you can pass in a `morphStyle` \nin a third config argument:\n\n```js\n  Idiomorph.morph(existingNode, \"\u003cdiv\u003eNew Content\u003c/div\u003e\", {morphStyle:'innerHTML'});\n```\n\nThis will replace the _inner_ content of the existing node with the new content.\n\n### Options\n\nIdiomorph supports the following options:\n\n| option (with default)         | meaning                                                                                                    | example                                                                  |\n|-------------------------------|------------------------------------------------------------------------------------------------------------|--------------------------------------------------------------------------|\n| `morphStyle: 'outerHTML'`     | The style of morphing to use, either `outerHTML` or `innerHTML`                                            | `Idiomorph.morph(..., {morphStyle:'innerHTML'})`                         |\n| `ignoreActive: false`         | If `true`, idiomorph will skip the active element                                                          | `Idiomorph.morph(..., {ignoreActive:true})`                              |\n| `ignoreActiveValue: false`    | If `true`, idiomorph will not update the active element's value                                            | `Idiomorph.morph(..., {ignoreActiveValue:true})`                         |\n| `restoreFocus: true`          | If `true`, idiomorph will attempt to restore any lost focus and selection state after the morph.           | `Idiomorph.morph(..., {restoreFocus:true})`                              |\n| `head: {style: 'merge', ...}` | Allows you to control how the `head` tag is merged. See the [head](#the-head-tag) section for more details | `Idiomorph.morph(..., {head:{style:'merge'}})`                           |\n| `callbacks: {...}`            | Allows you to insert callbacks when events occur in the morph lifecycle. See the callback table below      | `Idiomorph.morph(..., {callbacks:{beforeNodeAdded:function(node){...}})` |\n\n#### Callbacks\n\nIdiomorph provides the following callbacks, which can be used to intercept and, for some callbacks, modify the swapping behavior\nof the algorithm.\n\n| callback                                                  | description                                                                                                    | return value meaning                               |\n|-----------------------------------------------------------|----------------------------------------------------------------------------------------------------------------|----------------------------------------------------|\n| beforeNodeAdded(node)                                     | Called before a new node is added to the DOM                                                                   | return false to not add the node                   |\n| afterNodeAdded(node)                                      | Called after a new node is added to the DOM                                                                    | none                                               |\n| beforeNodeMorphed(oldNode, newNode)                       | Called before a node is morphed in the DOM                                                                     | return false to skip morphing the node             |\n| afterNodeMorphed(oldNode, newNode)                        | Called after a node is morphed in the DOM                                                                      | none                                               |\n| beforeNodeRemoved(node)                                   | Called before a node is removed from the DOM                                                                   | return false to not remove the node                |\n| afterNodeRemoved(node)                                    | Called after a node is removed from the DOM                                                                    | none                                               |\n| beforeAttributeUpdated(attributeName, node, mutationType) | Called before an attribute on an element is updated or removed (`mutationType` is either \"update\" or \"remove\") | return false to not update or remove the attribute |\n\n### The `head` tag\n\nThe head tag is treated specially by idiomorph because:\n\n* It typically only has one level of children within it\n* Those children often do not have `id` attributes associated with them\n* It is important to remove as few elements as possible from the head, in order to minimize network requests for things\n  like style sheets\n* The order of elements in the head tag is (usually) not meaningful\n\nBecause of this, by default, idiomorph adopts a `merge` algorithm between two head tags, `old` and `new`:\n\n* Elements that are in both `old` and `new` are ignored\n* Elements that are in `new` but not in `old` are added to `old`\n* Elements that are in `old` but not in `new` are removed from `old`\n\nThus the content of the two head tags will be the same, but the order of those elements will not be.\n\n#### Attribute Based Fine-Grained Head Control\n\nSometimes you may want even more fine-grained control over head merging behavior.  For example, you may want a script\ntag to re-evaluate, even though it is in both `old` and `new`.  To do this, you can add the attribute `im-re-append='true'`\nto the script tag, and idiomorph will re-append the script tag even if it exists in both head tags, forcing re-evaluation\nof the script.\n\nSimilarly, you may wish to preserve an element even if it is not in `new`.  You can use the attribute `im-preserve='true'`\nin this case to retain the element.\n\n#### Additional Configuration\n\nYou are also able to override these behaviors, see the `head` config object in the source code.\n\nYou can set `head.style` to:\n\n* `merge` - the default algorithm outlined above\n* `append` - simply append all content in `new` to `old`\n* `morph` - adopt the normal idiomorph morphing algorithm for the head\n* `none` - ignore the head tag entirely\n\nFor example, if you wanted to merge a whole page using the `morph` algorithm for the head tag, you would do this:\n\n```js\nIdiomorph.morph(document.documentElement, newPageSource, {head:{style: 'morph'}})\n```\n\nThe `head` object also offers callbacks for configuring head merging specifics.\n\n### Setting Defaults\n\nAll the behaviors specified above can be set to a different default by mutating the `Idiomorph.defaults` object, including\nthe `Idiomorph.defaults.callbacks` and `Idiomorph.defaults.head` objects.\n\n### htmx\n\nIdiomorph was created to integrate with [htmx](https://htmx.org) and can be used as a swapping mechanism by including\nthe `dist/idiomorph-ext.js` file in your HTML:\n\n```html\n\u003cscript src=\"https://unpkg.com/idiomorph/dist/idiomorph-ext.min.js\"\u003e\u003c/script\u003e\n\u003cdiv hx-ext=\"morph\"\u003e\n    \n    \u003cbutton hx-get=\"/example\" hx-swap=\"morph:innerHTML\"\u003e\n        Morph My Inner HTML\n    \u003c/button\u003e\n\n    \u003cbutton hx-get=\"/example\" hx-swap=\"morph:outerHTML\"\u003e\n        Morph My Outer HTML\n    \u003c/button\u003e\n    \n    \u003cbutton hx-get=\"/example\" hx-swap=\"morph\"\u003e\n        Morph My Outer HTML\n    \u003c/button\u003e\n    \n\u003c/div\u003e\n```\n\nor by importing the \"idiomorph/htmx\" module:\n\n```html\nimport \"idiomorph/htmx\";\n```\n\nNote that this file includes both Idiomorph and the htmx extension.\n\n#### Configuring Morphing Behavior in htmx\n\nThe Idiomorph extension for htmx supports three different syntaxes for specifying behavior:\n\n* `hx-swap='morph'` - This will perform a morph on the outerHTML of the target\n* `hx-swap='morph:outerHTML'` - This will perform a morph on the outerHTML of the target (explicit)\n* `hx-swap='morph:innerHTML'` - This will perform a morph on the innerHTML of the target (i.e. the children)\n* `hx-swap='morph:\u003cexpr\u003e'` - In this form, `\u003cexpr\u003e` can be any valid JavaScript expression.  The results of the expression\n   will be passed into the `Idiomorph.morph()` method as the configuration.\n\nThe last form gives you access to all the configuration options of Idiomorph.  So, for example, if you wanted to ignore\nthe input value in a given morph, you could use the following swap specification:\n\n```html\n  \u003cbutton hx-get=\"/example\" \n          hx-swap=\"morph:{ignoreActiveValue:true}\"\n          hx-target=\"closest form\"\u003e\n      Morph The Closest Form But Ignore The Active Input Value\n  \u003c/button\u003e\n```\n\n## Performance\n\nIdiomorph is not designed to be as fast as either morphdom or nanomorph.  Rather, its goals are:\n\n* Better DOM tree matching\n* Relatively simple code\n\nPerformance is a consideration, but better matching is the reason Idiomorph was created.  Our benchmarks indicate that\nit is approximately equal to 10% slower than morphdom for large DOM morphs, and equal to or faster than morphdom for \nsmaller morphs. See the [Performance](PERFORMANCE.md) document for more details.\n\n## Example Morph\n\nHere is a simple example of some HTML in which Idiomorph does a better job of matching up than morphdom:\n\n*Initial HTML*\n```html\n\u003cdiv\u003e\n    \u003cdiv\u003e\n        \u003cp id=\"p1\"\u003eA\u003c/p\u003e\n    \u003c/div\u003e\n    \u003cdiv\u003e\n        \u003cp id=\"p2\"\u003eB\u003c/p\u003e\n    \u003c/div\u003e\n\u003c/div\u003e\n```\n\n*Final HTML*\n\n```html\n\u003cdiv\u003e\n    \u003cdiv\u003e\n        \u003cp id=\"p2\"\u003eB\u003c/p\u003e\n    \u003c/div\u003e\n    \u003cdiv\u003e\n        \u003cp id=\"p1\"\u003eA\u003c/p\u003e\n    \u003c/div\u003e\n\u003c/div\u003e\n```\n\nHere we have a common situation: a parent div, with children divs and grand-children divs that have ids on them.  This\nis a common situation when laying out code in HTML: parent divs often do not have ids on them (rather they have classes,\nfor layout reasons) and the \"leaf\" nodes have ids associated with them.\n\nGiven this example, morphdom will detach both #p1 and #p2 from the DOM because, when it is considering the order of the\nchildren, it does not see that the #p2 grandchild is now within the first child.\n\nIdiomorph, on the other hand, has an _id set_ for the (id-less) children, which includes the ids of the grandchildren.\nTherefore, it is able to detect the fact that the #p2 grandchild is now a child of the first id-less child.  Because of\nthis information it is able to only move/detach _one_ grandchild node, #p1.  (This is unavoidable, since they changed order)\n\nSo, you can see, by computing id sets for nodes, idiomorph is able to achieve better DOM matching, with fewer node \ndetachments.\n\n## Demo\n\nYou can see a practical demo of Idiomorph out-performing morphdom (with respect to DOM stability, _not_ performance) \nhere:\n\nhttps://github.com/bigskysoftware/Idiomorph/blob/main/test/demo/video.html\n\nFor both algorithms, this HTML:\n\n```html\n\u003cdiv\u003e\n    \u003cdiv\u003e\n        \u003ch3\u003eAbove...\u003c/h3\u003e\n    \u003c/div\u003e\n    \u003cdiv\u003e\n        \u003ciframe id=\"video\" width=\"422\" height=\"240\" src=\"https://www.youtube.com/embed/dQw4w9WgXcQ\"\n                title=\"Rick Astley - Never Gonna Give You Up (Official Music Video)\" frameborder=\"0\"\n                allow=\"accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture\"\n                allowfullscreen\u003e\u003c/iframe\u003e\n    \u003c/div\u003e\n\u003c/div\u003e\n```\n\nis morphed into this HTML:\n\n```html \n\u003cdiv\u003e\n    \u003cdiv\u003e\n        \u003ciframe id=\"video\" width=\"422\" height=\"240\" src=\"https://www.youtube.com/embed/dQw4w9WgXcQ\"\n                title=\"Rick Astley - Never Gonna Give You Up (Official Music Video)\" frameborder=\"0\"\n                allow=\"accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture\"\n                allowfullscreen\u003e\u003c/iframe\u003e\n    \u003c/div\u003e\n    \u003cdiv\u003e\n        \u003ch3\u003eBelow...\u003c/h3\u003e\n    \u003c/div\u003e\n\u003c/div\u003e\n```\n\nNote that the iframe has an id on it, but the first-level divs do not have ids on them.  This means\nthat morphdom is unable to tell that the video element has moved up, and the first div should be discarded, rather than morphed into, to preserve the video element.  \n\nIdiomorph, however, has an id-set for the top level divs, which includes the id of the embedded child, and can see that the video has moved to be a child of the first element in the top level children, so it correctly discards the first div and merges the video content with the second node.\n\nYou can see visually that idiomorph is able to keep the video running because of this, whereas morphdom is not:\n\n![Rick Roll Demo](https://github.com/bigskysoftware/Idiomorph/raw/main/test/demo/rickroll-idiomorph.gif)\n\nTo keep things stable with morphdom, you would need to add ids to at least one of the top level divs.\n\nHere is a diagram explaining how the two algorithms differ in this case:\n\n![Comparison Diagram](https://github.com/bigskysoftware/Idiomorph/raw/main/img/comparison.png)\n\n## Usage in the wild\n\n* [Datastar](https://data-star.dev) - uses idiomorph as its default merging strategy and embeds a Typescript port as part of its backend integration layer.\n* [Turbo](https://turbo.hotwired.dev/handbook/page_refreshes#morphing) - uses idiomorph to perform full page refreshing. \n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fbigskysoftware%2Fidiomorph","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fbigskysoftware%2Fidiomorph","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fbigskysoftware%2Fidiomorph/lists"}