Digging into Turbo 8's Morphing Feature in Ruby on Rails
Tags: ruby, rails, tutorial
Published: 2023-12-31 18:28:02

---
title: Digging into Turbo 8's Morphing Feature in Ruby on Rails
published: true
date: 2023-12-22 20:18:06 UTC
tags: ruby, rails, tutorial
canonical_url: https://web-crunch.com/posts/turbo-morphing-ruby-on-rails
---

{% youtube wURcezOECD0 %}

Inspired by Jorge Manrubia’s [talk from Railsworld](https://www.youtube.com/watch?v=m97UsXa6HFg&t=4s) I wanted to try out morphing which is coming to [Turbo 8](https://github.com/hotwired/turbo) very soon.

On October 9th, 2023 Jorge [published a blog post](https://dev.37signals.com/a-happier-happy-path-in-turbo-with-morphing/) that was a precursor to the talk. I recommend giving it a read.

The gist of the blog post and talk is that Turbo frames and streams are useful but often cumbersome to integrate since they are highly focused containers of logic.

They won’t be going away but might be more of a _special-use_ tool coming up with the introduction of morphing which could be a more convenient and useful “default” much like the standard full-page reloads of historical Rails apps.

### Discovering the problem

The Basecamp team has been working on integrating a calendar into their HEY product. In building the new feature, they quickly spotted the constraints of turbo frames and streams. Having to broadcast and update many items on a given page is problematic and overly complex, so they looked for a better approach, one much closer to the default Rails full-page reload conventions.

## What is morphing?

No, this doesn’t relate to Power Rangers, though one could wish!

Morphing is the process of merging one DOM into another without too many side effects. It’s not necessarily natural but the perception our eyes see makes it feel as such.

Morphing isn’t new, but it is to Turbo 8. The Basecamp team chose [idiomorph](https://github.com/bigskysoftware/idiomorph) as a library to help with the new features. It's a JavaScript library for morphing from one DOM tree to another.

**The TL;DR;**

Morphing provides smoother updates everywhere rather than selective updates like turbo streams and turbo frames.

## Putting it to practice

Let’s make a quick blog to put these new features to work. I prefer `esbuild` to `importmaps` so I passed a flag to declare that. Feel free to skip that part.

```
rails new morphblog -j esbuild

```

### Add Tailwind CSS

I’m going to scaffold our models coming up so to make the views pre-styled and save myself some time we’ll leverage the dedicated [tailwindcss-rails](https://github.com/rails/tailwindcss-rails)gem.

```bash
bundle add tailwindcss-rails
```

Then run the installer

```bash
rails tailwindcss:install
```

The app is now accessible at [localhost:5000](localhost:5000)

## Install Turbo

Add the [latest beta of Turbo](https://github.com/hotwired/turbo-rails/releases/tag/v2.0.0-beta.2)(turbo-rails gem) (this will change of course once the official version is released)

```ruby
# Gemfile

gem "turbo-rails", "~> 2.0.0-beta.2"
```

Then run

```bash
bundle install
```

Don’t forget to update the [node version of Turbo](https://github.com/hotwired/turbo/releases) as well

```bash
yarn add @hotwired/turbo@8.0.0-beta.2
```

## Create the blog

Scaffold a `Post` resource. We’ll make it reference a `User` model that I'll create next.

```bash
rails g scaffold Post title content:text user:references
```

Scaffold a `User`

```bash
rails g scaffold User name email
```

**Note:** For the guide I’ll forgo worrying about authentication so we’re keeping this stupid simple for now.

Create a User on the rails command line `rails console`.

```bash
User.create(name: "Andy", email: "andy@webcrunch.com")

  TRANSACTION (0.0ms) begin transaction
  User Create (0.5ms) INSERT INTO "users" ("name", "email", "created_at", "updated_at") VALUES (?, ?, ?, ?) RETURNING "id" [["name", "Andy"], ["email", "andy@webcrunch.com"], ["created_at", "2023-12-21 20:31:06.848693"], ["updated_at", "2023-12-21 20:31:06.848693"]]
  TRANSACTION (0.4ms) commit transaction
=>
#<User:0x00000001090775b0
 id: 1,
 name: "Andy",
 email: "andy@webcrunch.com",
 created_at: Thu, 21 Dec 2023 20:31:06.848693000 UTC +00:00,
 updated_at: Thu, 21 Dec 2023 20:31:06.848693000 UTC +00:00>
irb(main):002>
```

### Update the associations

After generating the resources we can define the associations in the models.

```ruby
# app/models/user.rb

class User < ApplicationRecord
  has_many :posts
end
```

And then in the `Post` model:

```ruby
# app/models/post.rb
class Post < ApplicationRecord
  belongs_to :user
end
```

Standard Rails stuff!

### Update routing

Inside `config/routes.rb` we can uncomment the line beginning with `root`

```ruby
Rails.application.routes.draw do
  resources :users
  resources :posts
  # Define your application routes per the DSL in https://guides.rubyonrails.org/routing.html

  # Reveal health status on /up that returns 200 if the app boots with no exceptions, otherwise 500.
  # Can be used by load balancers and uptime monitors to verify that the app is live.
  get "up" => "rails/health#show", as: :rails_health_check

  # Defines the root path route ("/")
  root "posts#index"
end
```

### Stub out the controller

Normally in the controller, you might define a current user based on some authentication strategy. To make this demo “just work” I’ll stub it out with some crappy code assigning the first `User` directly :)

```ruby
class PostsController < ApplicationController
   def create
    # assign the User record we just made for demo purposes
    @post = Post.new(post_params.merge(user: User.first))

    respond_to do |format|
      if @post.save
        format.html { redirect_to post_url(@post), notice: "Post was successfully created." }
        format.json { render :show, status: :created, location: @post }
      else
        format.html { render :new, status: :unprocessable_entity }
        format.json { render json: @post.errors, status: :unprocessable_entity }
      end
    end
  end
end
```

### Remove user references from the Post form

Because we are doing this in our controller, we don’t need the `user_id` field. Let's remove it so we are left with the following:

```erb
<%= form_with(model: post, class: "contents") do |form| %>
  <% if post.errors.any? %>
    <div id="error_explanation" class="bg-red-50 text-red-500 px-3 py-2 font-medium rounded-lg mt-3">
      <h2><%= pluralize(post.errors.count, "error") %> prohibited this post from being saved:</h2>

      <ul>
        <% post.errors.each do |error| %>
          <li><%= error.full_message %></li>
        <% end %>
      </ul>
    </div>
  <% end %>

  <div class="my-5">
    <%= form.label :title %>
    <%= form.text_field :title, class: "block shadow rounded-md border border-gray-200 outline-none px-3 py-2 mt-2 w-full" %>
  </div>

  <div class="my-5">
    <%= form.label :content %>
    <%= form.text_area :content, rows: 4, class: "block shadow rounded-md border border-gray-200 outline-none px-3 py-2 mt-2 w-full" %>
  </div>

  <div class="inline">
    <%= form.submit class: "rounded-lg py-3 px-5 bg-blue-600 text-white inline-block font-medium cursor-pointer" %>
  </div>
<% end %>
```

### Move the Post form to the index

Let’s make our form super simple and display it above all the posts on the index. Maybe think of it like a makeshift “X” (formerly Twitter) app.

```erb
<div class="w-full max-w-3xl mx-auto">
  <% if notice.present? %>
    <p class="py-2 px-3 bg-green-50 mb-5 text-green-500 font-medium rounded-lg inline-block" id="notice"><%= notice %></p>
  <% end %>

  <h1 class="font-bold text-4xl">Posts</h1>

  <%= render "posts/form", post: Post.new %>

  <div id="posts" class="min-w-full mt-10">
    <%= render @posts %>
  </div>
</div>
```

From here go ahead and create a post or two. You can use the console or go through the UI but we just need a post or two as dummy data to get started.

## Add new morph meta tag

In your `application.html.erb` layout file add the following within the `head` tags.

```erb
<%= turbo_refreshes_with method: :morph, scroll: :preserve %>
```

```erb
<head>
  <title>Morphblog</title>
  <meta name="viewport" content="width=device-width,initial-scale=1">
  <%= csrf_meta_tags %>
  <%= csp_meta_tag %>
  <%= turbo_refreshes_with method: :morph, scroll: :preserve %>

  <%= stylesheet_link_tag "tailwind", "inter-font", "data-turbo-track": "reload" %>

  <%= stylesheet_link_tag "application", "data-turbo-track": "reload" %>
  <%= javascript_include_tag "application", "data-turbo-track": "reload", defer: true %>
</head>
```

This instructs the app to "morph" the page while preserving the scroll position. Previously, Turbo would "replace" the entire page and "reset" the scroll position.

## Update Post model

Next, we need to broadcast the updates. With the new beta version of Turbo, we can add the following to our Posts model.

```ruby
# app/models/post.rb

class Post < ApplicationRecord
  belongs_to :user
  broadcasts_refreshes # add this line
end
```

Then in the posts#show view, we can add the `turbo_stream_from` tag you might already be familiar with.

```erb
<div class="mx-auto md:w-2/3 w-full flex">
  <div class="mx-auto">
    <% if notice.present? %>
      <p class="py-2 px-3 bg-green-50 mb-5 text-green-500 font-medium rounded-lg inline-block" id="notice"><%= notice %></p>
    <% end %>

    <%= turbo_stream_from @post %>

    <%= render @post %>

    <%= link_to "Edit this post", edit_post_path(@post), class: "mt-2 rounded-lg py-3 px-5 bg-gray-100 inline-block font-medium" %>
    <div class="inline-block ml-2">
      <%= button_to "Destroy this post", post_path(@post), method: :delete, class: "mt-2 rounded-lg py-3 px-5 bg-gray-100 font-medium" %>
    </div>
    <%= link_to "Back to posts", posts_path, class: "ml-2 rounded-lg py-3 px-5 bg-gray-100 inline-block font-medium" %>
  </div>
</div>
```

Watch [a quick example](https://share.cleanshot.com/dR4jzvhg) of the “streamed” updates in action. I opened two windows, one being incognito. It kinda “just works” which is way cooler than tons of turbo frame tags and whatnot.

## Updating collections

This is one area I’m not 100% sold on. It still works but there’s a big assumption with a collection you’ll have an association tied to it. That’s probably true most of the time but it feels a bit rigid. I might be overthinking it of course.

Using the new morph features to make something like the posts index stream updates we need an association tied to it. So instead of just rendering all the posts, we’ll render the user’s posts and update our models.

Here’s the `Post` model

```ruby
# app/models/post.rb
class Post < ApplicationRecord
  # updates the timestamp when a post-commit (create, update, destroy) occurs
  belongs_to :user, touch: true
  broadcasts_refreshes
end
```

The `touch: true` addition instructs the timestamp of the associated user when it's manipulated in some way.

On the `User` model we need to add the new method

```ruby
# app/models/user.rb
class User < ApplicationRecord
  has_many :posts
  broadcasts_refreshes
end
```

Then in the index view, we’ll stub out what might normally be a `current_user` with the associated user we defined for demonstration purposes.

```ruby
# app/controllers/posts_controller.rb
class PostsController < ApplicationController
  before_action :set_post, only: %i[show edit update destroy]

  # GET /posts or /posts.json
  def index
    @user = User.first
    @posts = @user.posts # update to user posts
  end

  # more stuff below
end
```

Then we need to stream updates from that User specifically.

```erb
<div class="w-full max-w-3xl mx-auto">
  <% if notice.present? %>
    <p class="py-2 px-3 bg-green-50 mb-5 text-green-500 font-medium rounded-lg inline-block" id="notice"><%= notice %></p>
  <% end %>

  <h1 class="font-bold text-4xl">Posts</h1>

  <%= render "posts/form", post: Post.new %>

  <%= turbo_stream_from @user %>
  <div id="posts" class="min-w-full mt-10">
    <%= render @posts %>
  </div>
</div>
```

[Here it is in action](https://share.cleanshot.com/HDSgNCsQ). Pretty darn slick!

## Wrapping up

Having dabbled with the beta I’m super excited about this update!

It makes reaching for turbo frame tags more intentional versus necessary. There will no doubt be times when you need more sophistication but I think this is a huge improvement and one I’m excited to transition into existing apps leveraging a ton of turbo frames. The fact that I need to add about 3 lines of code is probably my favorite part. Cheers to the future of Rails!