Example: inline edit (show ↔ edit)
The classic “click to edit a field in place” pattern. In plain Hotwire this is a
Stimulus controller plus three routes (inline_edit, inline_update,
inline_cancel) and partials. Here it’s one component with two actions.
class Fields::InlineEdit < ApplicationComponent
include Phlex::Reactive::Streamable
include Phlex::Reactive::Component
reactive_record :record
reactive_state :attribute, :editing # which field, and the mode
action :edit
action :cancel
action :save, params: { value: :string }
def initialize(record:, attribute:, editing: false)
@record = record
@attribute = attribute.to_sym
@editing = editing
end
def id = dom_id(@record, "inline_#{@attribute}")
def edit = @editing = true
def cancel = @editing = false
def save(value:)
authorize! @record, :update?
@record.update!(@attribute => value)
@editing = false
end
def view_template
span(id:, **reactive_attrs) do
if @editing
input(name: "value", value: current_value, autocomplete: "off")
button(**on(:save)) { "Save" }
button(**on(:cancel)) { "Cancel" }
else
span(**on(:edit), class: "editable") { current_value.presence || "—" }
end
end
end
private
def current_value = @record.public_send(@attribute)
end
Render it for any field:
render Fields::InlineEdit.new(record: @user, attribute: :name)
render Fields::InlineEdit.new(record: @user, attribute: :email)
Notes
- Two pieces of identity:
reactive_record :record(the row, re-found via GlobalID) andreactive_state :attribute, :editing(which field, what mode). Both are signed; the client can’t switch@attributeto a column it shouldn’t edit because the value is signed into the token. - Mode is server state.
edit/cancel/saveflip@editingand re-render the same element — no separate “edit” route or partial. - Authorize on save.
edit/cancelare harmless view toggles;savemutates, so it authorizes. - The
span(**on(:edit))turns the display text into the click target. Addtabindex/ keyboard handling if you need a11y on non-button triggers.
Want it to update other viewers too?
Broadcast on save:
def save(value:)
authorize! @record, :update?
@record.update!(@attribute => value)
@editing = false
Fields::InlineEdit.broadcast_replace_to(
@record, model: @record, attribute: @attribute
)
end
Now everyone viewing that record sees the new value land in place.