Editable
Inline editing component - click to edit text, blur or Enter to save, Escape to cancel.
Key Features
- Click to Edit - Click on any text to reveal input field
- Auto-save - Automatically saves on blur or Enter key
- Cancel Support - Press Escape to cancel edits
- 3 Input Types - text, textarea, number
- Server Validation - Full support for Rails validations via Turbo
- Semantic Tags - Use
as: :h1,:h2,:p, etc. with automatic typography - Placeholder - Show helpful text when value is empty
- Validation - Required, maxlength, minlength, min, max
- Full Dark Mode - Beautiful in light and dark themes
- Turbo Compatible - Works seamlessly with Turbo Streams
Basic Usage
Click on the text below to edit it. Changes are sent to the server via PATCH request.
This is a real Post from the database. Try editing the title:
View this post →Changes are instantly saved to the database. Visit the post to confirm!
<%= rui_editable @post, :title, url: post_path(@post) %>
<%= rui_editable @post, :subtitle,
url: post_path(@post),
placeholder: "Add a subtitle..." %>
Input Types
Three input types for different content formats.
Text Input (Default)
For single-line text like titles, names, etc.
<%= rui_editable @post, :title,
url: post_path(@post),
input_type: :text %>
Textarea
For multi-line content like descriptions, notes, body text.
<%= rui_editable @post, :body,
url: post_path(@post),
input_type: :textarea,
rows: 4 %>
Number Input
For numeric values like prices, quantities, ratings.
<%= rui_editable @product, :price,
url: product_path(@product),
input_type: :number,
min: 0,
max: 10000 %>
Validation
Client-side validation provides instant feedback before sending to server.
<%= rui_editable @user, :name,
url: user_path(@user),
required: true %>
<%= rui_editable @post, :title,
url: post_path(@post),
minlength: 3,
maxlength: 100 %>
<%= rui_editable @product, :quantity,
url: product_path(@product),
input_type: :number,
min: 0,
max: 999 %>
Controller Setup
Your Rails controller needs to handle PATCH requests for inline updates.
class PostsController < ApplicationController
def update
@post = Post.find(params[:id])
if @post.update(post_params)
respond_to do |format|
format.turbo_stream { head :ok }
format.html { redirect_to @post }
format.json { render json: { success: true } }
end
else
respond_to do |format|
format.turbo_stream {
render turbo_stream: turbo_stream.replace(
"editable_post_#{@post.id}_title",
partial: "posts/title_error",
locals: { post: @post }
)
}
format.html { render :edit }
format.json {
render json: { errors: @post.errors.full_messages },
status: :unprocessable_entity
}
end
end
end
private
def post_params
params.require(:post).permit(:title, :body, :subtitle)
end
end
Turbo Integration
The editable component works seamlessly with Turbo Streams for real-time updates.
# In your controller, return Turbo Stream for error handling:
respond_to do |format|
format.turbo_stream do
if @post.errors.any?
render turbo_stream: turbo_stream.replace(
dom_id(@post, :editable_title),
partial: "posts/editable_title",
locals: { post: @post, error: @post.errors[:title].first }
)
else
head :ok
end
end
end
The component automatically handles Turbo Stream responses. On success, it updates the display text. On error, it shows the error message and keeps the input focused for correction.
Real-World Examples
Editable Card Title
<div class="bg-white dark:bg-zinc-900 rounded-lg shadow p-6">
<%= rui_editable @task, :name,
url: task_path(@task),
as: :h2,
placeholder: "Untitled Task" %>
<%= rui_editable @task, :description,
url: task_path(@task),
as: :p,
input_type: :textarea,
rows: 3,
placeholder: "Add a description..." %>
</div>
Editable Table Cell
<%= rui_table @products do |table| %>
<% table.with_column("Name") do |product| %>
<%= rui_editable product, :name, url: product_path(product) %>
<% end %>
<% table.with_column("Price") do |product| %>
<%= rui_editable product, :price,
url: product_path(product),
input_type: :number,
min: 0 %>
<% end %>
<% table.with_column("Stock") do |product| %>
<%= rui_editable product, :quantity,
url: product_path(product),
input_type: :number,
min: 0 %>
<% end %>
<% end %>
User Profile Settings
<div class="space-y-4">
<div class="flex items-center justify-between">
<span class="text-sm font-medium text-zinc-500">Display Name</span>
<%= rui_editable @user, :display_name,
url: user_path(@user),
required: true,
maxlength: 50 %>
</div>
<div class="flex items-center justify-between">
<span class="text-sm font-medium text-zinc-500">Bio</span>
<%= rui_editable @user, :bio,
url: user_path(@user),
input_type: :textarea,
rows: 3,
maxlength: 500,
placeholder: "Tell us about yourself..." %>
</div>
</div>
All Options Reference
Complete reference showing every available option with copy-paste ready examples.
Minimal Required Usage
The bare minimum to create an editable field.
<%= rui_editable @post, :title, url: post_path(@post) %>
All Options Example
Every available option with annotations.
<%= rui_editable @post, :title,
# Required
url: post_path(@post), # PATCH endpoint URL
# Semantic element (auto-styled for headings)
as: :h1, # Element type (:span, :h1-h6, :p, :div)
id: "post-title-editor", # Custom HTML ID (optional)
# Input configuration
input_type: :text, # :text (default), :textarea, :number
placeholder: "Enter a title...", # Shown when value is empty
# Validation (client-side)
required: true, # Field cannot be empty
minlength: 5, # Minimum characters
maxlength: 100, # Maximum characters
# Number-specific options (when input_type: :number)
min: nil, # Minimum value
max: nil, # Maximum value
# Textarea-specific options (when input_type: :textarea)
rows: 3 # Number of visible rows
%>
Input Type: Text (Default)
<%= rui_editable @user, :name,
url: user_path(@user),
placeholder: "Enter your name",
required: true,
minlength: 2,
maxlength: 50 %>
Input Type: Textarea
<%= rui_editable @post, :body,
url: post_path(@post),
as: :div,
input_type: :textarea,
rows: 6,
placeholder: "Write your content here...",
minlength: 10,
maxlength: 5000 %>
Input Type: Number
<%= rui_editable @product, :price,
url: product_path(@product),
input_type: :number,
placeholder: "0.00",
required: true,
min: 0,
max: 99999 %>
<%= rui_editable @cart_item, :quantity,
url: cart_item_path(@cart_item),
input_type: :number,
min: 1,
max: 100 %>
Custom ID for Targeting
<%= rui_editable @post, :title,
url: post_path(@post),
id: "editable-post-<%%=@post.id%>-title",
as: :h1 %>
<script>
// Find the editable element
const editable = document.getElementById('editable-post-123-title')
// Listen for save events
editable.addEventListener('editable:saved', (e) => {
console.log('Saved:', e.detail.value)
})
</script>
Complete Setup Guide
Everything you need to implement editable fields from scratch.
1. Model
class Post < ApplicationRecord
belongs_to :user
validates :title, presence: true, length: { minimum: 3, maximum: 100 }
validates :body, length: { maximum: 10000 }
validates :priority, numericality: { only_integer: true, in: 1..5 }, allow_nil: true
end
2. Controller
class PostsController < ApplicationController
before_action :set_post, only: [:show, :update]
def show
end
def update
if @post.update(post_params)
respond_to do |format|
# Turbo Stream - return success for inline update
format.turbo_stream { head :ok }
# JSON - for API clients
format.json { render json: { success: true, value: @post.send(post_params.keys.first) } }
# HTML fallback
format.html { redirect_to @post, notice: 'Updated successfully.' }
end
else
respond_to do |format|
# Turbo Stream - replace with error state
format.turbo_stream do
render turbo_stream: turbo_stream.replace(
dom_id(@post, :editable),
partial: 'posts/editable_error',
locals: { post: @post }
), status: :unprocessable_entity
end
# JSON - return errors
format.json do
render json: { success: false, errors: @post.errors.full_messages },
status: :unprocessable_entity
end
# HTML fallback
format.html { render :edit, status: :unprocessable_entity }
end
end
end
private
def set_post
@post = Post.find(params[:id])
end
def post_params
params.require(:post).permit(:title, :body, :priority, :subtitle)
end
end
3. View
<article class="max-w-4xl mx-auto p-6">
<%= rui_editable @post, :title,
url: post_path(@post),
as: :h1,
placeholder: "Untitled Post",
required: true,
minlength: 3,
maxlength: 100 %>
<%= rui_editable @post, :subtitle,
url: post_path(@post),
as: :h2,
placeholder: "Add a subtitle...",
maxlength: 200 %>
<%= rui_editable @post, :body,
url: post_path(@post),
as: :div,
input_type: :textarea,
rows: 10,
placeholder: "Start writing...",
minlength: 10 %>
<div class="mt-8 flex items-center gap-4">
<span class="text-sm text-zinc-500">Priority:</span>
<%= rui_editable @post, :priority,
url: post_path(@post),
input_type: :number,
min: 1,
max: 5,
placeholder: "1-5" %>
</div>
</article>
4. Routes
Rails.application.routes.draw do
resources :posts, only: [:show, :update]
end
Visual Indicators
The component provides clear visual cues to indicate editability and state changes.
Dashed Underline
A subtle dashed border appears below editable text, signaling that the content can be modified. Always visible to provide consistent affordance.
Pencil Icon
A small pencil icon appears next to editable text. At 40% opacity by default, it brightens to 70% on hover, reinforcing the edit action. Hidden during editing mode.
Hover Background
A subtle background color appears on hover, providing clear visual feedback that the element is interactive and ready to be clicked.
Success Flash
After a successful save, a green background briefly flashes and fades out over 1.5 seconds. This provides immediate visual confirmation that changes were saved.
Error State
When validation fails or a network error occurs, the input displays a red ring and an error message appears below. The input stays focused for easy correction.
Saving Indicator
During save, the component shows "Saving..." text and reduces opacity, preventing interaction until the request completes.
Accessibility
The Editable component follows accessibility best practices for inline editing interfaces.
Focus Management
- Input receives focus automatically when editing starts
- Focus is restored to display element after save/cancel
- Clear focus ring indicators for keyboard users
Keyboard Navigation
- Enter saves changes (for text inputs)
- Cmd/Ctrl + Enter saves changes (for textareas)
- Escape cancels editing and reverts to original value
- Blur (clicking outside) triggers auto-save
Visual Feedback
- Hover state indicates clickable/editable area
- Saving indicator shows when request is in progress
- Error messages are displayed inline with the input
JavaScript API
The Editable component exposes Stimulus actions and custom events for programmatic control.
Stimulus Values
Configure the editable behavior via data-*-value attributes:
| Value | Type | Description |
|---|---|---|
url |
String | PATCH endpoint URL |
field |
String | Form field name (e.g., "post[title]") |
current |
String | Current value |
inputType |
String | Input type (text, textarea, number) |
placeholder |
String | Placeholder text |
Stimulus Actions
Use these actions via data-action attributes:
| Action | Description |
|---|---|
editable#edit |
Switch to edit mode, show input field |
editable#save |
Save changes to server via PATCH request |
editable#cancel |
Cancel edits, revert to original value |
<div data-controller="editable"
data-editable-url-value="<%= post_path(@post) %>"
data-editable-field-value="post[title]"
data-editable-current-value="<%= @post.title %>">
<span data-editable-target="display"><%= @post.title %></span>
<button data-action="editable#edit" class="text-sm text-blue-600">
Edit
</button>
</div>
Custom Events
Listen to these events for lifecycle hooks:
| Event | When | Detail |
|---|---|---|
editable:saved |
Value saved successfully | { value, field } |
editable:cancelled |
Edit cancelled by user | { originalValue } |
editable:error |
Save failed with error | { error, field } |
<div data-controller="my-controller"
data-action="editable:saved->my-controller#onSave
editable:cancelled->my-controller#onCancel
editable:error->my-controller#onError">
<%= rui_editable @post, :title, url: post_path(@post) %>
</div>
// my_controller.js
onSave(event) {
const { value, field } = event.detail
console.log(`Saved ${field}: ${value}`)
}
onCancel(event) {
const { originalValue } = event.detail
console.log(`Cancelled, reverted to: ${originalValue}`)
}
Targets
Reference these targets in custom JavaScript:
| Target | Description |
|---|---|
display |
The text display element (click to edit) |
input |
The input/textarea element |
saving |
Saving indicator element |
error |
Error message element |
icon |
Pencil icon element (hidden during editing) |
Programmatic Control
Control the editable from JavaScript using the Stimulus controller:
// Get the editable controller
const editableElement = document.querySelector('[data-controller="editable"]')
const editableController = application.getControllerForElementAndIdentifier(
editableElement,
"editable"
)
// Switch to edit mode programmatically
editableController.edit()
// Save current value
editableController.save()
// Cancel and revert
editableController.cancel()
API Reference
rui_editable(model, attribute, **options)
Inline editable text component
| Parameter | Type | Default | Description |
|---|---|---|---|
| model* | ActiveRecord::Base | — | The model object to edit |
| attribute* | Symbol | — | The attribute name to edit |
| url | String | — | PATCH endpoint URL (required for saving) |
| as | Symbol |
:span
|
Semantic element with auto-typography (:span, :h1-h6, :p, :div) |
| placeholder | String |
"Click to edit"
|
Text shown when value is empty |
| input_type | Symbol |
:text
|
Input type (:text, :textarea, :number) |
| required | Boolean |
false
|
Whether the field is required |
| maxlength | Integer | — | Maximum character length |
| minlength | Integer | — | Minimum character length |
| min | Integer | — | Minimum value (for number type) |
| max | Integer | — | Maximum value (for number type) |
| rows | Integer |
3
|
Number of rows (for textarea type) |
| id | String | — | Custom ID (auto-generated if nil) |
| class | String | — | Custom CSS classes |