Lesson 5 — Inline add and edit forms
The approach
Creating and editing cards and columns currently navigates to a separate page. That works, but it breaks board context — the user loses sight of their board to fill in a one-field form.
The fix is simple: render the forms inline in the board, hidden by default, and toggle them with Stimulus. No Turbo Frames, no streams, no navigation. The form is already in the DOM — Stimulus shows and hides it. When the form submits successfully the controller redirects back to the board and morph updates it. Validation errors are caught client-side before the form ever submits — no server round-trip needed for a single required field.
Form components
Each entity has one form component that handles both adding and editing. The form model and button label are derived from whether the record is persisted:
|
|
|
|
The permitted class method and PERMITTED constant stay on each form
for the controllers to use:
|
|
Stimulus controllers
Two explicit controllers — one per entity. They are structurally identical, which is intentional. They serve separate components with separate concerns. A shared abstraction would add coupling without meaningful benefit for two controllers this small.
|
|
|
|
Note that card_form_controller uses link as its display target
(the “+ Add card” button) while column_form_controller uses display
(the column header). Different target names because they’re toggling
different kinds of elements.
form.reset() in hideForm restores inputs to their original page-load
values. This is important for the edit case — if you clear the title,
see the validation error, then cancel, the input must restore to the
original title rather than staying empty.
Client-side validation catches the blank field before submission. The
server-side presence: true validation remains as a safety net but the
422 error path is effectively never reached in normal use.
KanbanCard
The card component now renders both the display state and the edit form,
toggled by card-form controller:
|
|
The drag handle (cursor-grab) is on the display div, not the card
wrapper. This means dragging only works when the display is visible —
you can’t accidentally drag a card while its edit form is showing.
KanbanColumn
The column renders its header with an inline edit form toggle, its card list, the add card toggle, and delete button:
|
|
Note that render_add_card has its own data-controller="card-form"
wrapper — separate from the card edit controllers on each KanbanCard.
Each controller instance is scoped to its own element, so there’s no
conflict between the “Add card” toggle and the individual card edit
toggles.
Controllers
With inline forms on the board, the cards and columns controllers
simplify. There are no longer separate New and Edit views for cards
and columns — everything happens on the board:
|
|
|
|
The controllers always redirect back to the board. Client-side
validation means the blank field case never reaches the server in
normal use. The save and update calls are not guarded with if —
the server-side validation is a safety net, not a code path we design
UI around.
Icons needed
The edit buttons use :pencil. Add it to Components::Icon::ICONS:
|
|
Updating Views::Boards::Show
The “+ Add column” link previously navigated to new_board_column_path(@board). Now that the new action has been removed from ColumnsController, that link would raise a routing error. The add column interaction moves inline — the same toggle pattern as “+ Add card”.
Update render_add_column in Views::Boards::Show to replace the link with an inline toggle:
def render_add_column
div(
class: "w-72 shrink-0",
data: { controller: "column-form" }
) do
button(
type: "button",
class: "flex items-center gap-2 text-sm text-text-muted " \
"hover:text-text bg-surface-alt/50 rounded-lg p-3 " \
"border-2 border-dashed border-border w-full " \
"hover:border-border-strong",
data: {
column_form_target: "display",
action: "click->column-form#showForm"
}
) do
plain "+ Add column"
end
div(
hidden: true,
class: "bg-surface-alt/50 rounded-lg p-3 border-2 border-dashed border-border",
data: { column_form_target: "form" }
) do
render Views::Columns::ColumnForm.new(
column: @board.columns.build,
board: @board
)
end
end
endFiles to remove
The build pass produced several intermediate form files that the final design supersedes. Delete:
app/views/cards/inline_form.rb
app/views/cards/edit_form.rb
app/views/cards/card_inline_form.rb
app/views/cards/new.rb
app/views/cards/edit.rb
app/views/columns/edit_form.rb
app/views/columns/column_inline_form.rb
app/views/columns/new.rb
app/views/columns/edit.rbAlso remove the new and edit actions from both controllers — they’re
no longer needed since forms render inline on the board.
Routes
With new and edit actions removed, tighten the routes:
|
|
What we now have
The board is fully self-contained. Every card and column interaction happens inline:
- Add card — click “+ Add card”, type title, submit. Morph adds the card to the column
- Edit card — click the pencil, update title, submit. Morph updates the card in place
- Delete card — click ×, confirm. Morph removes the card
- Edit column title — click the pencil on the column header, update, submit. Morph updates the header
- Delete column — click × on the column header, confirm with card count. Morph removes the column and all its cards
- Add column — click “+ Add column”, type name, submit. Morph adds the column to the board
- Reorder cards — drag within or between columns. Position persisted via PATCH
- Reorder columns — drag by the column header. Position persisted via PATCH
No page navigations. No Turbo Frames. No Streams. Morph and Stimulus doing exactly what they’re designed to do.