Profile
Back to NewsBack
GitHub Trending 10 min
Reader Mode
yippee-fun/morphlex: Optimal DOM morphing, written in TypeScript.

yippee-fun/morphlex: Optimal DOM morphing, written in TypeScript.

22 hours ago

Morphlex

Morphlex is a DOM morphing library that transforms one DOM tree to match another while preserving element state and making minimal changes.

What makes Morphlex different?

  1. No cascading mutations from inserts. Each inserted node is one DOM operation.
  2. No cascading mutations from removes. Each removed node is one DOM operation.
  3. No cascading mutations from partial sorts. Morphlex finds the longest increasing subsequence, so it moves the fewest elements it can.
  4. It uses moveBefore when available, preserving state.
  5. It uses isEqualNode, but in a way that is sensitive to the value of form inputs.
  6. It uses id sets, inspired by Idiomorph, so ids nested deep inside an element can help to identify it.

Installation

npm install morphlex

Or use it directly from a CDN:

<script type="module">
  import { morph } from "https://www.unpkg.com/morphlex@latest/dist/morphlex.min.js"
</script>

Morphlex only touches the DOM when you call it, so it’s safe to import in environments without a DOM, such as during server-side rendering.

Usage

import { morph, morphInner, morphDocument } from "morphlex"

// Morph the element itself, including its attributes morph(currentNode, newNode)

// Morph only the children of the element morphInner(currentNode, newNode)

// Morph the entire document morphDocument(document, newDocument)

Each function also accepts a string of HTML as the target:

morph(currentNode, <div id="profile" class="active">…</div>)
morphInner(currentNode, <ul><li>One</li><li>Two</li></ul>)
morphDocument(document, await response.text())
  • morph(from, to, options?) morphs from into to. The target can be a node, a NodeList or a string. If it has several nodes, the first is morphed into from and the rest are inserted after it. If it has none, from is removed.
  • morphInner(from, to, options?) morphs the children of from into the children of to, leaving the attributes of from alone. Both must be elements with the same tag name and namespace. A string target must contain exactly one element.
  • morphDocument(from, to, options?) morphs the element of one document into another. A string target is parsed with DOMParser.
Morphlex throws if it needs to replace or insert next to a node that has no parent, for example when morphing a detached
into a .
[!WARNING]
When you pass a string, it is parsed as HTML and its nodes are inserted into the live document, where inline event handlers (such as onclick) and resource-loading attributes (such as src) take effect. Don’t pass untrusted HTML without sanitizing it first.

Options

All three functions accept an optional third argument for configuration:

morph(currentNode, newNode, {
  preserveChanges: true,
  beforeNodeAdded: (parent, node, insertionPoint) => {
    console.log("Adding node:", node)
    return true // return false to prevent addition
  },
})
  • preserveChanges: When true, form controls the user has changed keep their values, and the open state of
    and elements is left alone. See Preserving changes. Default: false
  • beforeNodeVisited(fromNode, toNode): Called before a node is visited during morphing. Return false to skip morphing this node. Nodes that already match their target are left alone without being visited, so this isn’t called for them.
  • afterNodeVisited(fromNode, toNode): Called after a node has been visited and morphed.
  • beforeNodeAdded(parent, node, insertionPoint): Called before a new node is added to the DOM. insertionPoint is the node it will be inserted before, or null if it will be appended. Return false to prevent adding the node.
  • afterNodeAdded(node): Called after a node has been added to the DOM.
  • beforeNodeRemoved(node): Called before a node is removed from the DOM. Return false to prevent removal.
  • afterNodeRemoved(node): Called after a node has been removed from the DOM.
  • beforeAttributeUpdated(element, name, newValue): Called before an attribute is added, changed or removed. newValue is null when the attribute is being removed. Return false to prevent the update.
morph(currentNode, newNode, {
  beforeAttributeUpdated: (element, name) => {
    if (element.tagName === "DETAILS" && name === "open") return false
    return true
  },
})

This can be useful for preserving UI state that your backend does not track. preserveChanges already does this for open on

and , so a hook like this is for when you want the same behaviour without preserving form changes, or for other attributes.

  • afterAttributeUpdated(element, name, previousValue): Called after an attribute has been updated on an element. previousValue is null if the attribute didn’t exist before.
  • beforeChildrenVisited(parent): Called before an element’s children are visited during morphing. Return false to skip visiting children.
  • afterChildrenVisited(parent): Called after an element’s children have been visited and morphed.
When a node can’t be morphed in place and has to be replaced, beforeNodeRemoved is called first, then beforeNodeAdded only if the removal was allowed. Returning false from either one leaves the original node where it is, even when the replacement is an element that would move in from elsewhere.

An element with a unique id can move to a new parent during a morph (except and elements, whose selection belongs to their