Skip to content

Lesson 1 — form_with in Phlex

The adapter

form_with is a Rails helper that generates a form tag and yields a form builder. To use it in Phlex, include the adapter in Components::Base:

1
2
3
4
5
6
7
# app/components/base.rb
class Components::Base < Phlex::HTML
  include Phlex::Rails::Helpers::Routes
  include Phlex::Rails::Helpers::FormWith
  extend Literal::Properties
  # ...
end

With the adapter included, form_with is available in every component and view without any further setup.

How form_with works in Phlex

form_with yields a form builder object — exactly as in ERB, but the builder’s output methods push HTML directly to the Phlex buffer:

1
2
3
4
5
6
7
def view_template
  form_with(model: @board, class: "space-y-4") do |f|
    f.label     :name, "Board name"
    f.text_field :name, placeholder: "e.g. Marketing Q3"
    f.submit    "Save"
  end
end

CSRF protection is automatic — form_with injects the authenticity token as a hidden field. You never need to handle it manually.

form_with also detects whether the model is persisted:

  • New record → action="/boards", method="post"
  • Persisted record → action="/boards/1", method="post" with a hidden _method=patch field

One form, two behaviours. No conditional logic needed.

Using form_with builder methods vs Phlex components

The form builder (f) and Phlex components can be mixed freely in a form. Builder methods are convenient for one-offs; Phlex components give you typed props, consistent styling, and integrated labels:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
form_with(model: @board) do |f|
  # Builder method — simple, no styling control
  f.text_field :name

  # Phlex component — typed props, consistent styling, integrated label
  TextInput(field: :name, label: "Board name", value: @board.name)

  # Submit via Button component — consistent with the rest of the UI
  Button(label: "Save", type: "submit")
end

In KanbanFlow we use Phlex components for all inputs and the Button component for submit.

A minimal working form

Before building the primitives, confirm form_with works end to end. Add new and create to the boards controller:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
# app/controllers/boards_controller.rb
def new
  render Views::Boards::New.new(board: Board.new)
end

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

private

def board_params
  params.require(:board).permit(:name)
end

Temporary: @board.user = User.first is a placeholder until Module 11 adds authentication. At that point this line is replaced with @board.user = current_user. Make sure you’ve run bin/rails db:seed so a user exists.

And a minimal view using builder methods directly:

 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
# app/views/boards/new.rb
class Views::Boards::New < Views::Base
  def page_title = "New Board"

  def initialize(board:)
    @board = board
  end

  def view_template
    Breadcrumb() do |b|
      b.item "Boards", url: boards_path
      b.item "New Board"
    end

    div(class: "max-w-lg mt-6") do
      h1(class: "text-2xl font-bold text-gray-900 mb-6") { "New Board" }

      form_with(model: @board, class: "space-y-4") do |f|
        div do
          f.label     :name, "Board name",
                      class: "block text-sm font-medium text-gray-700 mb-1"
          f.text_field :name,
                       class:       "w-full rounded-md border border-gray-300 " \
                                    "px-3 py-2 text-sm bg-white text-gray-900",
                       placeholder: "e.g. Marketing Q3"
        end
        button(
          type:  "submit",
          class: "inline-flex items-center justify-center rounded-md " \
                 "font-medium bg-blue-600 text-white hover:bg-blue-700 " \
                 "px-4 py-2 text-sm"
        ) { plain "Create board" }
      end
    end
  end
end

Note that we use a plain button element rather than f.submit for the submit button. f.submit renders an <input type="submit"> which is less flexible for styling. A button(type: "submit") renders a <button> element and accepts a content block — consistent with how we use Button components throughout the app.

Visit http://localhost:3000/boards/new. The form should render and submit correctly. In Lesson 4 we replace the inline builder fields with Phlex primitives.