Tony Messias

September 1, 2024

Turbo Streams with Morph

I've tagged a new version of Turbo Laravel today which adds some syntactic sugar for generating Turbo Streams that use the morph method instead of the default one.

As of version 8.0.5 of Turbo.js we may set a `[method="morph"]` attribute on Turbo Streams. Since Turbo Laravel already supports custom attributes, you could already use it like this:

turbo_stream()->update(...)->attributes(['method' => 'morph']);

But I've added a new convenient method to simplify this a little:

turbo_stream()->update(...)->morph();

This should work for both update and replace Turbo Stream actions.

What is cool about DOM morphing is that it unlocks CSS transitions for the element changes. For instance, let's consider a progress bar example (the same example as in the Hypermedia Systems book, which I highly recommend, since the ideas also apply to Turbo and Hotwire). In the example below, we randomly generate a value between 1 and 100. When we submit the form, the progress bar is updated using a Turbo Stream update action, here's what that endpoint could look like without morphing:

Route::post('progress', function () {
  return turbo_stream()
    ->update('progress', view('dashboard.partials.progress', [
      'value' => rand(1, 100),
    ]);
})->name('progress.store');

That partial progress view would look like this:

<div
  class="w-0 h-4 bg-orange-600 animate-pulse transition-all rounded-lg"
  style="width: {{ $value }}%;"
  role="progressbar"
  aria-valuenow="{{ $value }}"
></div>

This is the same partial used to render the initial progress bar on the page when it first appears. It renders inside a div element that has the `[id="progress"]` attribute on it, so we're essentially updating the children element (the progress bar).

Here's what the update looks like in the browser:

morph-without.gif


Notice how the progress bar jumps to the new position. That happens because we cannot use CSS transitions here since we're swapping the DOM element (it's a new element, so transitions don't apply).

To change this Turbo Stream to use morphing, you may chain the new `morph()` method when building the Turbo Streams. Our previous endpoint would look like this:

Route::post('progress', function () {
  return turbo_stream()
    ->update('progress', view('dashboard.partials.progress', [
      'value' => rand(1, 100),
    ])
    ->morph(); // Now with morphing!
})->name('progress.store');

We're using the same partial as before. But now, the Turbo Stream that will be generated will be like:

<turbo-stream action="update" method="morph" target="progress">
  <template>...</template>
</turbo-stream>

And this is what it looks like in the browser now with CSS transitions:

morph-with.gif


Much better, right?! You may also chain the `morph()` modifier when you're broadcasting Turbo Streams, like:

$comment->broadcastUpdate()->morph();

Morph was a new addition to Turbo 8 and it enables a much more fluid page update experience.

About Tony Messias