Mastering Inline editing with Rails Hotwire

Frontend made easy with Rails Hotwire

Recently, the inline editing capabilities demand has increased a lot. It allows users to edit certain fields in a form directly on the form page, without navigating to a separate edit page. No refresh nor reload page is needed.
And why not, since it enhances user experience and saves time!

In the Rails world, thanks to Hotwire’s stack, inline editing is fun and a simple task. In this post, you will know how to implement inline editing in a Rails application using Hotwire Turbo.

scenario

A client asks OR your application requires you to create a Contact object with first_name, last_name, and email attributes. Users are allowed to inline edit first_name and last_name attributes only, without a page reload ‘refresh’.

First, we scaffold the Contact object like so: rails g scaffold contacts first_name last_name email.
In the Contact model, create an EDITABLE_ATTRIBUTES constant that lists the attributes to be edited inline :first_name and :last_name. While you are here, add a validation that requires the first_name and last_name attributes to be present.

# app/models/contact.rb  
class Contact < ApplicationRecord  
    EDITABLE_ATTRIBUTES = [:first_name, :last_name].freeze  
    validates :first_name, :last_name presence: true  
end

Scaffold kindly enough, generated _contact.html.erb partial to render a single contact.

# app/views/contacts/_contact.html.erb  
<div id="<%= dom_id contact %>">  
    <% Contact::EDITABLE_ATTRIBUTES.each do |attribute| %>  
        <%= render "editable_attribute", contact: contact, attribute: attribute %>  
    <% end %>  
</div>

Above, instead of listing each attribute individually, loop through EDITABLE_ATTRIBUTES and render _editable_attribute.html.erb partial to edit each attribute.

# app/views/contacts/_editable_attribute.html.erb  
<%= turbo_frame_tag attribute do %>  
    <%= link_to (contact[attribute].presence || 'Edit field'), [:edit, contact, attribute: attribute] %>  
<% end %>

Notice, the use the turbo_frame_tag helper here to wrap each attribute with the id attribute, which matches the same id within the _editable_attribute_form.html.erb partial below, and updates it using TurboStreams.

# app/views/contacts/_editable_attribute_form.html.erb  
<%= turbo_frame_tag params[:attribute] do %>  
    <%= form_with(model: contact, url: [contact, attribute: attribute], method: :patch) do |form| %>  
        <% if contact.errors.any? %>  
            <div style="color: red">  
                <ul>  
                    <% contact.errors.each do |error| %>  
                        <li><%= error.full_message %></li>  
                    <% end %>  
                </ul>  
            </div>  
        <% end %>  
        <%= form.text_field attribute, onchange: 'this.form.requestSubmit()' %>  
        <%= form.submit %>  
    <% end %>  
<% end %>

Above partial renders a form for editing a single attribute. It uses the form_with helper to create a form that submits a :patch request to update contact. Notice it is using the same id attribute that matches the previous partial.
The form includes a text_field field for a dynamically selected attribute matching name as params[:attribute]. Each field has an onchange event that submits the form when the value of the field changes.

Lastly, update the edit.html.erb template to render the edit page for contact. If the attribute parameter is present in the request, the template renders the ‌_editable_attribute_form.html.erb partial to allow the user to edit a single attribute inline. If the attribute parameter is not present, the template renders the form partial to allow the user to edit all attributes of the contact object at once. Rails form that you are familiar with.

# app/views/contacts/edit.html.erb  
<% if params[:attribute].present? %>  
    <%= render "editable_attribute_form", contact: @contact %>  
<% else %>  
    <%= render "form", contact: @contact %>  
<% end %>

As you see. Rails 7 with Hotwire stack offers the amazing ability for developers to do a fantastic job with little effort and no reliance on external libraries.

Refactor

As all developers do, we like to write less and reduce DRY. You can simplify and refactor your code as follow.

Extracted the logic for rendering _editable_attribute.html.erb and _editable_attribute_form. HTML.erb partials code into a new helper module EditableAttributesHelper.

By doing so, you need to update both _contact.html.erb partial, and edit.html.erb template and call the new helper instead.

# app/views/contacts/_contact.html.erb  
<div id="<%= dom_id contact %>">  
    <% Contact::EDITABLE_ATTRIBUTES.each do |attribute| %>  
        <%= turbo_frame_tag attribute do %>  
            <%= render_editable_attribute contact, attribute %>  
        <% end %>  
    <% end %>  
</div>

Above we call the render_editable_attribute method and pass in the contact object and the attribute as arguments.

# app/views/contacts/edit.html.erb  
<% if params[:attribute].present? %>  
    <%= turbo_frame_tag params[:attribute] do %>  
        <%= render_editable_attribute_form @contact, params[:attribute] %>  
    <% end %>  
<% else %>  
    <%= render "form", contact: @contact %>  
<% end %>

And in the edit.html.erb template, we call the render_editable_attribute_form method and pass in the @contact object and the params[:attribute] as arguments.

Let's check your new helper module.

# app/helpers/editable_attributes_helper.rb
    module EditableAttributesHelper
      def render_editable_attribute(contact, attribute)
        content_tag(:p) do
          concat link_to(contact[attribute].presence || 'Edit', [:edit, contact, attribute: attribute])
        end
      end

        def render_editable_attribute_form(contact, attribute)
          form_with(model: contact, url: [contact, attribute: attribute], method: :patch) do |form|
            content = []

            if contact.errors.any?
              content << content_tag(:div, class: 'flex text-red-500 flex-col bg-pink-100') do
                content_tag(:h2, "#{pluralize(contact.errors.count, "error")} prohibited this contact from being saved:") +
                content_tag(:ul) do
                  contact.errors.each do |error|
                    concat content_tag(:li, error.full_message)
                  end
                end
              end
            end

            content << content_tag(:div) do
              content << form.text_field(attribute, onchange: 'this.form.requestSubmit()', autofocus: true)
              content << form.submit("Update #{attribute.humanize}")
            end
            content.join.html_safe
          end
        end
    end

The render_editable_attribute method renders a link to the edit action with the attribute parameter set to the current attribute. The render_inline_attribute_form method renders a div element with a form containing a text_field tag for the selected attribute. If there are any errors, they are displayed in a div element with TailwindCSS styles.

Nothing drastically changed from previous partials.

By extracting the logic for rendering both partials into a helper class, we have made the code more reusable and easier to maintain. We can now use the render_editable_attribute and render_editable_attribute_form methods in any view in our application to render editable inline attributes and forms.

Notice content.join.html_safe. It takes an array of strings, content, and joins them into a single string, by calling join method on the array contnet. Then it marks the resulting string as safe for output in an HTML context, by calling the html_safe method on it. The html_safe method tells Rails to not escape the string when it's output in an HTML template, which is typically used when you want to output HTML tags.

Final thoughts

Implementing inline editing is a powerful feature that can improve the user experience of your application by allowing users to edit data directly on the page, without having to reload the page or navigate to a separate page.
Thanks to Rails Hotwire Turbo made the process of doing so easy, fun, and straightforward.

And as usual, Happy Coding 😀 💻

Did you find this article valuable?

Support Ahmed Nadar by becoming a sponsor. Any amount is appreciated!