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 😀 💻