Notes to self

Single attribute in-place editing with Rails and Turbo

Turbo can largely simplify our front-end needs to achieve a single-page application feel. If you have ever wondered how to do a single attribute in-place update with Turbo, this post is for you.

I’ll assume you have Turbo (with turbo-rails gem) installed, and you already have a classic model CRUD done. If you don’t, just generate a standard scaffold. I’ll use the User model and the name attribute, but it can be anything.

At this point, you might have a controller for the model looking like this:

class UsersController < ApplicationController
  before_action :set_user, only: %i[ show edit update destroy ]

  ...

  # GET /users/1/edit
  def edit
  end

  # PATCH/PUT /users/1 or /users/1.json
  def update
    respond_to do |format|
      if @user.update(user_params)
        format.html { redirect_to user_path(@user), notice: "User was successfully updated." }
        format.json { render :show, status: :ok, location: user_path(@user) }
      else
        format.html { render :edit, status: :unprocessable_entity }
        format.json { render json: @user.errors, status: :unprocessable_entity }
      end
    end
  end

  private
    # Use callbacks to share common setup or constraints between actions.
    def set_user
      @user = User.find(params[:id])
    end

    # Only allow a list of trusted parameters through.
    def user_params
      params.require(:user).permit(:name)
    end
end

You should also have all the standard views that go with it, namely views/users/show.html.erb, that we’ll modify for in-place editing of the user’s name.

We make a specific page for this change to support editing a specific attribute (here a name).

The controller change is easy. We add edit_name method next to your original edit:

class UsersController < ApplicationController
  before_action :set_user, only: %i[ show edit edit_name update destroy password_reset ]

  # GET /users/1/edit
  def edit
  end

  # GET /users/1/edit_name
  def edit_name
  end

  # PATCH/PUT /users/1 or /users/1.json
  def update
    respond_to do |format|
      if @user.update(user_params)
        format.html { redirect_to user_path(@user), notice: "User was successfully updated." }
        format.json { render :show, status: :ok, location: user_path(@user) }
      else
        format.html { render :edit, status: :unprocessable_entity }
        format.json { render json: @user.errors, status: :unprocessable_entity }
      end
    end
  end

  private
    # Use callbacks to share common setup or constraints between actions.
    def set_user
      @user = User.find(params[:id])
    end

    # Only allow a list of trusted parameters through.
    def user_params
      params.require(:user).permit(:name)
    end
end

Notice that there is no need to change how update works, it can do the job for all the attributes at once.

And let’s not forget to make the new path accessible with a change to routes.rb file:

Rails.application.routes.draw do
  ...

  resources :users do
    member do
      get 'edit_name'
    end
  end

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

Now that we have a new route and controller method to render the form for the name change, we implement the views.

We’ll add a standard view for the edit_name action (views/users/edit_name.html.erb):

<%= form_with model: @user, url: user_path(@user) do |form| %>
  <%= form.text_field :name %>
  <%= form.submit "Save" %>
<% end %>

And then wrap it with turbo_frame_tag call:

<%= turbo_frame_tag :user_name do %>
  <%= form_with model: @user, url: user_path(@user) do |form| %>
    <%= form.text_field :name %>
    <%= form.submit "Save" %>
  <% end %>
<% end %>

Wrapping everything in turbo_frame_tag gives this form a unique identifier and determines the area that gets swapped later.

Notice that we don’t need a specific model ID for turbo_frame_tag (like the examples leveraging dom_id) as we will swap the content on the model’s show page where other user entries don’t exist.

Once prepared, we make another turbo_frame_tag on the show page with the same ID. This tells Turbo that it can swap it with the frame we defined in the previous step:

...
<%= turbo_frame_tag :user_name do %>
  Name: <%= link_to @user.name, edit_name_user_path(@user) %>
<% end %>
...

A link_to pointing to the specific path for editing the name will trigger the action, and Turbo does the rest!

Check out my book
Interested in Ruby on Rails default testing stack? Take Minitest and fixtures for a spin with my latest book.

Get Test Driving Rails while it's in prerelease.

by Josef Strzibny
RSS