idiomorph

repository·main·Indexed 22 days ago

https://github.com/bigskysoftware/idiomorph

A lightweight, dependency-free JavaScript library for morphing one DOM tree into another. Idiomorph uses 'id sets' to match elements based on their descendants' IDs, providing more robust updates and better DOM stability than traditional morphing algorithms. It supports various module formats (AMD, CJS, ESM), provides a comprehensive lifecycle callback system, and includes a dedicated extension for integration with htmx.

Tokens
4.5K
Snippets
13
Records
23
Agent score
77%

What's inside idiomorph

  1. Idiomorph usage in popular frameworks

    main

    Idiomorph is used as a core merging strategy in several high-profile projects:

    • Datastar: Uses Idiomorph as its default merging strategy and includes a TypeScript port within its backend integration layer.
    • Turbo (Hotwired): Uses Idiomorph to perform full page refreshing.
  2. Understand Idiomorph's ID Set matching concept

    main

    Idiomorph uses id sets to achieve better DOM matching than traditional morphing libraries (like morphdom).

    The Problem: In many HTML layouts, parent containers lack IDs (using classes for layout instead), while 'leaf' nodes (grandchildren) have IDs. When these leaf nodes change order, traditional morphing libraries often fail to realize the parent container is the same and may unnecessarily detach and re-attach the entire subtree.

    The Idiomorph Solution: Idiomorph computes an id set for every node, which includes the IDs of all its descendants. This allows it to recognize that a node with specific grandchildren is the same as a node in the new structure, even if the parent itself has no ID. This results in fewer node detachments and better performance for complex reordering.

  3. How Idiomorph's DOM merging works

    main

    Idiomorph is a JavaScript DOM-merging algorithm designed to synchronize an oldNode with a newNode. Its primary goal is to minimize the number of nodes disconnected from the DOM (moves or replacements).

    Minimizing disconnections is critical because browser state—such as input focus, video playback, or scroll position—is often lost when nodes are removed or replaced. Idiomorph achieves high fidelity by matching the new structure to the existing structure as closely as possible, rather than performing a generic tree merge.

    Key concepts include:

    • Attribute Syncing: Merging attributes from the new node onto the old node.
    • Advanced Sameness: Unlike traditional algorithms (like nanomorph) that rely heavily on strict ID equivalence, Idiomorph uses a broader definition of "sameness" based on the intersection of ID sets. This allows nodes to be recognized as the same even if they don't share a direct ID, provided they contain related IDs in their subtree.
  4. The concept of 'Sameness' in Idiomorph

    main

    In many DOM morphing libraries, "sameness" (determining if a new node can be merged into an existing old node) is strictly tied to whether elements have matching IDs.

    Idiomorph improves this by using an ID Set Intersection approach:

    1. ID Set Computation: For every element in the new content, a set of all IDs contained within its subtree is computed (using an efficient bottom-up approach).
    2. Broad Sameness: Two nodes are considered "the same" if their ID sets have a non-empty intersection. This allows child nodes to contribute to the identity of a parent node, improving matching fidelity even when IDs are sparse.
    3. Efficient Matching: Because a parent's ID set is a superset of all its children's ID sets, the algorithm can efficiently determine if a node has a potential match within a specific parent element by checking for a non-empty intersection of their ID sets.
  5. Configure the `<head>` tag merging behavior

    main

    Idiomorph treats the <head> tag specially to minimize network requests (like re-fetching stylesheets). By default, it uses a merge algorithm: elements in both tags are ignored, new elements are added, and missing elements are removed.

    Head Merging Styles

    You can control this via the head.style option:

    • merge (default): Merges elements based on presence, ignoring order.
    • append: Appends all content from the new head to the old head.
    • morph: Uses the standard Idiomorph morphing algorithm for the head.
    • none: Ignores the head tag entirely.

    Attribute-Based Control

    You can use specific HTML attributes to fine-tune behavior:

    • im-re-append='true': Forces a <script> tag to re-evaluate even if it exists in both heads.
    • im-preserve='true': Prevents an element from being removed even if it is not in the new head.

    Example: Morphing the entire head

    Idiomorph.morph(document.documentElement, newPageSource, {head: {style: 'morph'}});
  6. Compare Idiomorph vs morphdom stability

    main

    Idiomorph provides superior DOM stability compared to morphdom when morphing elements that lack unique IDs.

    While morphdom often fails to recognize when an element (like an <iframe>) has moved within the DOM tree—leading it to discard and recreate the element (which breaks state like video playback)—Idiomorph uses an internal id-set. This id-set includes the IDs of all descendant elements, allowing Idiomorph to track elements even if their parent containers do not have IDs. This enables Idiomorph to correctly identify that an element has moved rather than being replaced.

    <!-- Example of HTML that Idiomorph handles more stably than morphdom -->
    <div class="container">
        <div class="header">
            <h3>Above...</h3>
        </div>
        <div class="content">
            <iframe id="video" src="..."></iframe>
        </div>
    </div>
    
    <!-- When morphed to this, Idiomorph preserves the iframe state -->
    <div class="container">
        <div class="content">
            <iframe id="video" src="..."></iframe>
        </div>
        <div class="header">
            <h3>Below...</h3>
        </div>
    </div>
  7. Add new benchmarks to the suite

    main
    To add a new benchmark to the suite, create two new HTML files in the perf/benchmarks directory using the naming convention [benchmark-name].old.html and [benchmark-name].new.html. The .old.html file should contain the starting HTML state, and the .new.html file should contain the final morphed HTML state.
  8. View code coverage reports

    main

    After running tests, coverage data is generated. You can view the visual report by opening coverage/lcov-report/index.html in a browser. On Ubuntu, use xdg-open to open it.

    # Open coverage report (Ubuntu)
    xdg-open coverage/lcov-report/index.html
  9. Run individual tests

    main

    You can target specific files or individual test cases to speed up development.

    Headless Mode

    To run a specific test file (e.g., test/core.js) headlessly, pass the file path to the npm test command. To isolate a single test within that file, temporarily use it.only(...) in the source code.

    Browser Mode

    For easier debugging, you can run tests directly in a browser using Mocha. Simply open the test/index.html file in your preferred browser. On Ubuntu, use xdg-open.

    # Run a specific test file headlessly
    npm test test/core.js
    
    # Run tests in browser mode (Ubuntu)
    xdg-open test/index.html
  10. Install dependencies for Idiomorph testing

    main

    To set up the testing environment, ensure you have a supported version of Node.js and npm installed. You must install the project dependencies and the Playwright browser binaries.

    npm install
    npx playwright install
  11. Run Idiomorph performance benchmarks

    main

    Idiomorph includes a performance benchmarking suite located in the perf directory to compare the current implementation of src/idiomorph.js against morphdom or previous Idiomorph releases. These benchmarks are intended for manual execution during development to detect performance regressions and are not part of the standard CI or coverage reports.

    npm run perf [versus=morphdom] [benchmarks...]