Skip to content

DOM Morphing ​

DOM morphing is the intelligent process of updating your page's HTML while preserving JavaScript state, event listeners, focus, and user selections. Instead of destroying and recreating elements, Hyper uses Datastar's morphing algorithm to surgically update only what changed, creating a smooth, flicker-free experience.

Why Morphing Matters

Morphing preserves the "life" in your page - things like focus state, scroll positions, CSS transitions, and third-party widget state. Without morphing, every update would feel like a hard page refresh.

The Problem with Replacement ​

Traditional DOM replacement destroys everything and starts fresh:

php
// Server returns new HTML
return hyper()->replace('#list', '<div id="list">New content</div>');

What gets lost:

  • Active input focus
  • Form input values not synced with signals
  • Scroll positions
  • CSS transition states
  • Event listeners from third-party libraries
  • Video/audio playback position

The user experience:

  • Flickers during update
  • Focus jumps unexpectedly
  • Scrolls reset to top
  • Animations restart abruptly

The Solution: Morphing ​

Morphing compares the old and new HTML trees and surgically updates only what changed:

php
// Morphing preserves state while updating content
return hyper()->outer('#list', '<div id="list">New content</div>');

What gets preserved:

  • ✅ Input focus (cursor stays in the same field)
  • ✅ Form values (partially filled forms don't reset)
  • ✅ Scroll positions (list scrolling maintained)
  • ✅ Event listeners (click handlers stay attached)
  • ✅ CSS transitions (smooth animations continue)
  • ✅ Third-party widgets (maps, charts, editors keep state)

How Morphing Works ​

Hyper uses Datastar's idiomorph algorithm, an intelligent DOM diffing strategy:

1. Element Matching ​

The algorithm identifies corresponding elements between old and new trees using:

ID Matching (Strongest):

html
<!-- Old HTML -->
<div id="user-123">John Doe</div>

<!-- New HTML -->
<div id="user-123">John Smith</div>

<!-- Result: Updates text, preserves element -->

Position + Tag Matching:

html
<!-- Old HTML -->
<ul>
    <li>First</li>
    <li>Second</li>
</ul>

<!-- New HTML -->
<ul>
    <li>First Modified</li>
    <li>Second</li>
</ul>

<!-- Result: Updates first li's text, preserves both elements -->

2. Attribute Diffing ​

Only changed attributes are updated:

html
<!-- Old -->
<button class="btn" disabled>Loading...</button>

<!-- New -->
<button class="btn btn-primary">Submit</button>

<!-- Result: Adds btn-primary class, removes disabled attribute -->

3. Content Updating ​

Text content is updated without recreating the element:

html
<!-- Old -->
<span>Loading...</span>

<!-- New -->
<span>Complete!</span>

<!-- Result: Text changes, span element reused -->

4. Structural Changes ​

When structure changes, only affected nodes are recreated:

html
<!-- Old -->
<div>
    <p>Paragraph 1</p>
    <p>Paragraph 2</p>
</div>

<!-- New -->
<div>
    <p>Paragraph 1</p>
    <h2>New Heading</h2>
    <p>Paragraph 2</p>
</div>

<!-- Result: Paragraph elements preserved, h2 inserted between them -->

Morphing Modes ​

Hyper provides different morphing modes for different use cases:

outer (Default) ​

Morphs the entire element including its tag:

php
return hyper()->fragment('posts.list', 'post-list', $data);
// or
return hyper()->outer('#post-list', $html);
html
<!-- Old -->
<div id="post-list" class="loading">
    <p>Loading...</p>
</div>

<!-- New -->
<div id="post-list" class="loaded">
    <ul>...</ul>
</div>

<!-- Result: div attributes and contents morph -->

Use when:

  • Updating a component entirely
  • Element attributes might change
  • Default for fragments

inner ​

Morphs only the element's children, preserving the container:

php
return hyper()->inner('#container', $html);
html
<!-- Old -->
<div id="container" class="wrapper">
    <p>Old content</p>
</div>

<!-- New HTML passed to inner: -->
<p>New content</p>

<!-- Result: Container div preserved, children replaced -->
<div id="container" class="wrapper">
    <p>New content</p>
</div>

Use when:

  • Container element should never change
  • Only updating content, not wrapper
  • Preserving container's event listeners

replace ​

Completely destroys and recreates the element:

php
return hyper()->replace('#widget', $html);

Use when:

  • Morphing is causing issues
  • Need a clean slate (third-party widget re-initialization)
  • Structure changed too drastically for morphing

Performance Cost

replace mode loses all preserved state. Use sparingly.

morph (Explicit Outer) ​

Explicitly use outer morphing mode:

php
return hyper()->outer('#element', $html);

append / prepend ​

Add new content without replacing existing:

php
// Add to end
return hyper()->append('#list', '<li>New item</li>');

// Add to beginning
return hyper()->prepend('#list', '<li>New item</li>');

Use for:

  • Infinite scroll (append new items)
  • Chat messages (prepend new messages)
  • Activity feeds

before / after ​

Insert content as siblings:

php
// Insert before element
return hyper()->before('#target', $html);

// Insert after element
return hyper()->after('#target', $html);

delete ​

Remove the element:

php
return hyper()->remove('#notification');

When Morphing Succeeds ​

Morphing works best when:

1. Element IDs Are Stable ​

blade
<!-- ✅ Good - Stable IDs -->
<div id="user-{{ $user->id }}">
    {{ $user->name }}
</div>

<!-- ❌ Poor - Changing IDs -->
<div id="user-{{ rand() }}">
    {{ $user->name }}
</div>

2. Structure Is Consistent ​

blade
<!-- ✅ Good - Same structure -->
<!-- Before: -->
<ul id="list">
    <li>Item 1</li>
    <li>Item 2</li>
</ul>

<!-- After: -->
<ul id="list">
    <li>Item 1 Updated</li>
    <li>Item 2</li>
    <li>Item 3</li>
</ul>

<!-- ❌ Poor - Radical structure change -->
<!-- Before: -->
<ul id="list"><li>Items</li></ul>

<!-- After: -->
<table id="list"><tr><td>Items</td></tr></table>

3. Using data-for with Keys ​

blade
<!-- ✅ Good - Stable keys help morphing -->
<template data-for="user in $users" data-for__key="id">
    <div id="user-{user.id}">
        <span data-text="user.name"></span>
    </div>
</template>

<!-- ❌ Poor - No keys, index-based matching -->
<template data-for="user in $users">
    <div><span data-text="user.name"></span></div>
</template>

4. Attributes Change Predictably ​

blade
<!-- ✅ Good - Predictable attribute changes -->
<button class="btn" data-attr:class="$loading ? 'btn loading' : 'btn'">
    Submit
</button>

<!-- ⚠️ Watch out - Unpredictable classes -->
<button class="{{ collect(['btn', $classes, $moreClasses])->flatten()->unique()->implode(' ') }}">

When Morphing Struggles ​

Radical Structure Changes ​

html
<!-- Old: List structure -->
<div id="content">
    <ul><li>Item</li></ul>
</div>

<!-- New: Table structure -->
<div id="content">
    <table><tr><td>Item</td></tr></table>
</div>

<!-- Result: May experience flicker or state loss -->

Solution: Use replace mode for radical changes, or keep structure consistent.

Changing Element Types ​

html
<!-- Old -->
<div id="wrapper">
    <span>Text</span>
</div>

<!-- New -->
<div id="wrapper">
    <p>Text</p>
</div>

<!-- Result: span → p requires recreation -->

Solution: Keep element types consistent when possible.

Dynamic IDs ​

blade
<!-- ❌ Poor - ID changes on every render -->
<div id="item-{{ time() }}">

<!-- ✅ Better - Stable ID -->
<div id="item-{{ $item->id }}">

Complex Third-Party Widgets ​

Some widgets modify their DOM internally in ways morphing can't track:

html
<!-- Chart library that manipulates DOM -->
<div id="chart"></div>
<script>
    new Chart('#chart', {...});
</script>

<!-- After morph: Chart might break -->

Solution: Use data-ref and reinitialize after updates, or use replace mode.

Debugging Morphing ​

Visualizing Changes ​

Add this to see what's being updated:

javascript
// In browser console
// Watch for morphing events (if Datastar exposes them)
document.addEventListener('datastar-morph', (e) => {
    console.log('Morphed:', e.detail);
});

Comparing HTML ​

Log before/after HTML to understand what changed:

php
public function update()
{
    $html = view('components.list', $data)->render();

    Log::info('Morphing HTML:', ['html' => $html]);

    return hyper()->html($html, ['selector' => '#list']);
}

Using Browser DevTools ​

  1. Open DevTools → Elements
  2. Trigger a Hyper update
  3. Watch for flash highlighting (shows changed elements)
  4. Inspect element structure before/after

Check for ID Stability ​

blade
@fragment('users')
<div id="users">
    @foreach($users as $user)
        <div id="user-{{ $user->id }}"
             data-debug="{{ json_encode(['id' => $user->id, 'name' => $user->name]) }}">
            {{ $user->name }}
        </div>
    @endforeach
</div>
@endfragment

Common Patterns ​

Preserving Form Input Focus ​

blade
<div id="search-results">
    <input type="text"
           id="search-input"
           data-bind="query"
           data-on:input__debounce.300ms="@get('/search')" />

    <div id="results">
        @foreach($results as $result)
            <div id="result-{{ $result->id }}">{{ $result->title }}</div>
        @endforeach
    </div>
</div>

The #search-input retains focus even as results update because morphing preserves it.

Infinite Scroll with Morphing ​

blade
<div id="items">
    @foreach($items as $item)
        <div id="item-{{ $item->id }}">{{ $item->name }}</div>
    @endforeach
</div>

<div data-on-intersect__once="@get('/load-more')">Load more...</div>
php
public function loadMore()
{
    $moreItems = Item::paginate(20, ['*'], 'page', request('page', 1));

    // Append new items without affecting existing ones
    $html = view('partials.items', ['items' => $moreItems])->render();

    return hyper()->append('#items', $html);
}

Updating Nested Components ​

blade
<div id="dashboard">
    @fragment('stats')
    <div id="stats">
        <div id="stat-users">{{ $stats['users'] }}</div>
        <div id="stat-revenue">${{ $stats['revenue'] }}</div>
    </div>
    @endfragment

    @fragment('chart')
    <div id="chart">
        <canvas id="revenue-chart"></canvas>
    </div>
    @endfragment
</div>
php
// Update only stats without touching chart
return hyper()->fragment('dashboard', 'stats', ['stats' => $newStats]);

The canvas in #chart is untouched, preserving any Chart.js instance.

Form Validation with Preserved State ​

blade
<form id="signup" @signals(['name' => '', 'email' => '', 'errors' => []])
      data-on:submit__prevent="@postx('/signup')">

    <input type="text" id="name-input" data-bind="name" />
    <div data-error="name"></div>

    <input type="email" id="email-input" data-bind="email" />
    <div data-error="email"></div>

    <button type="submit">Sign Up</button>
</form>

When validation fails and returns errors, morphing preserves:

  • Current input focus
  • Partially typed values
  • Cursor position

Performance Considerations ​

Morphing is Fast ​

The idiomorph algorithm is highly optimized:

Typical benchmarks:

  • Small fragment (10 elements): <1ms
  • Medium list (100 items): 2-5ms
  • Large update (500 elements): 10-20ms

These are faster than human perception (~16ms per frame).

When to Use Replace ​

Only use replace mode when:

  1. You measured a performance problem with morphing
  2. Morphing is causing visual glitches
  3. You need to reinitialize third-party widgets

Batch Updates ​

Update multiple fragments in one response:

php
return hyper()->fragments([
    ['view' => 'dashboard', 'fragment' => 'stats', 'data' => $stats],
    ['view' => 'dashboard', 'fragment' => 'activity', 'data' => $activity],
]);

This is more efficient than separate requests.

Comparison with Other Approaches ​

vs Virtual DOM (React, Vue) ​

FeatureMorphing (idiomorph)Virtual DOM
ApproachDirect DOM comparisonIn-memory tree comparison
PerformanceExcellent (native)Good (JS overhead)
State preservationAutomaticRequires keys/refs
Third-party widgetsPreservedOften break
Learning curveNoneRequires framework knowledge

vs innerHTML Replacement ​

FeatureMorphinginnerHTML
State preservation✅ Yes❌ No
Event listeners✅ Preserved❌ Lost
Focus✅ Maintained❌ Lost
Scroll position✅ Kept❌ Reset
PerformanceExcellentGood

Learn More ​