Skip to content

Lesson 6 — The complete boards controller, destroy, and flash

The complete controller

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
# app/controllers/boards_controller.rb
class BoardsController < ApplicationController
  def index
    render Views::Boards::Index.new(boards: Board.all)
  end

  def show
    render Views::Boards::Show.new(board: board)
  end

  def new
    render Views::Boards::New.new(board: Board.new)
  end

  def create
    @board = Board.new(board_params)
    @board.user = User.first        # temporary until we reach Module 11
    if @board.save
      redirect_to boards_path, notice: "Board created."
    else
      render Views::Boards::New.new(board: @board)
    end
  end

  def edit
    render Views::Boards::Edit.new(board: board)
  end

  def update
    if board.update(board_params)
      redirect_to boards_path, notice: "Board updated."
    else
      render Views::Boards::Edit.new(board: board)
    end
  end

  def destroy
    board.destroy
    redirect_to boards_path, notice: "Board deleted."
  end

  private

  def board
    @board ||= Board.find(params[:id])
  end

  def board_params
    params.expect(board: Views::Boards::BoardForm.permitted)
  end
end

We’ll need an Edit button and a Destroy button. Update your Views::Boards::Show page with the following:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
 def view_template
    Breadcrumb() do |b|
      b.item "Boards", url: boards_path
      b.item @board.name
    end

    div(class: "flex items-center justify-between mt-4 mb-6") do
      h1(class: "text-2xl font-bold text-gray-900") { @board.name }
      Button(label: "← Back to boards", href: boards_path, variant: :outline)
      div(class: "flex items-center gap-3") do
        Button(label: "Edit", href: edit_board_path(@board), variant: :secondary)
        Button(label: "Delete board", href: board_path(@board), variant: :danger,
               data: { turbo_method: :delete, turbo_confirm: "Delete #{@board.name}? This cannot be undone." })
      end
    end
  end

Turbo intercepts the form and shows a native confirmation dialog before sending the DELETE request. No JavaScript needed.

Model validations

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
# app/models/board.rb
class Board < ApplicationRecord
  belongs_to :user, optional: true  # wired up properly in Module 11

  has_many :memberships, dependent: :destroy
  has_many :members, through: :memberships, source: :user
  has_many :columns, -> { order(:position) }, dependent: :destroy

  validates :name, presence: true, length: { maximum: 100 }
end

Flash messages in the layout

Update AppLayout to render flash messages using Alert:

1
2
3
4
5
6
7
8
# app/components/layouts/app_layout.rb
def render_flash
  return unless flash[:notice] || flash[:alert]
  div(class: "px-8 pt-4 space-y-2") do
    Alert(message: flash[:notice], variant: :success) if flash[:notice]
    Alert(message: flash[:alert],  variant: :danger)  if flash[:alert]
  end
end

Call render_flash from view_template between render_nav and main.

Flash messages are functional but have limitations — they persist until navigation and shift the page layout. In Module 8 we replace them with Toast notifications that auto-dismiss and float over the UI without affecting layout.


Module 6 summary

  • form_with works in Phlex via include Phlex::Rails::Helpers::FormWith in Components::Base — CSRF is automatic, HTTP method and URL are inferred from model persistence
  • Use button(type: "submit") rather than f.submit for submit buttons — it accepts a content block and is consistent with the Button component
  • FormField is the base class for all input primitives — it owns the shared props (field, label, hint, error, required) and the label/hint rendering. Subclasses only implement render_input
  • Each primitive owns its label — pass label: "Field name" and the label renders with the correct for attribute automatically. No label: prop means no label rendered
  • All primitives accept error: true for red styling and aria-invalid="true" — error message display is the form’s responsibility, not the primitive’s
  • Three error handling strategies — summary at top, inline per field, or both — choose based on form size and audience
  • BoardForm lives under Views:: not Components:: — it is app-specific, knows about Board, and is not a portable library component
  • Self-permitting forms — PERMITTED on the form class, called via params.expect(board: FormClass.permitted) in the controller — eliminates hand-written permit lists
  • Destroy needs no view — form_with(method: :delete) with data: { turbo_confirm: } handles it with Turbo’s confirmation dialog
  • All Tailwind classes in this module use raw palette values — semantic tokens are introduced in Module 7

Components built this module

  • Components::FormField — base class for all form primitives
  • Components::TextInput
  • Components::Textarea
  • Components::Select
  • Components::Checkbox
  • Components::Radio
  • Components::NumberInput
  • Components::HiddenInput
  • Components::ErrorSummary

Views built this module

  • Views::Boards::BoardForm
  • Views::Boards::New
  • Views::Boards::Edit

KanbanFlow progress

KanbanFlow now has a fully working board management UI — create, view, edit, and delete boards. Validation errors display inline with accessible ARIA attributes. Flash messages appear in the layout via Alert. The controller never hand-writes a permit list.