Skip to content

Lesson 2 — Auth views in Phlex

The generator creates ERB views. We replace them with Phlex — consistent with the rest of the app.

Delete the generated ERB files:

1
rm -rf app/views/sessions app/views/passwords

Minimal layout

Auth pages — sign in, sign up, password reset — need a clean, focused layout without the full app nav.

 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
# app/views/layouts/auth_layout.rb
class Views::Layouts::AuthLayout < Components::Base
  include Phlex::Rails::Helpers::CSRFMetaTags
  include Phlex::Rails::Helpers::CSPMetaTag
  include Phlex::Rails::Helpers::StylesheetLinkTag
  include Phlex::Rails::Helpers::JavascriptImportmapTags
  prop :title, String, default: -> { "KanbanFlow" }

  def view_template
    doctype
    html(id: "html-root", lang: "en", data: { turbo_permanent: true }) do
      head do
        meta(charset: "UTF-8")
        meta(name: "viewport", content: "width=device-width,initial-scale=1")
        title { @title }
        csrf_meta_tags
        csp_meta_tag
        meta(name: "turbo-refresh-method", content: "morph")
        meta(name: "turbo-refresh-scroll", content: "preserve")
        stylesheet_link_tag "tailwind",    "data-turbo-track": "reload"
        stylesheet_link_tag "application", "data-turbo-track": "reload"
        javascript_importmap_tags
      end
      body(class: "bg-surface-alt min-h-screen") { yield }
    end
  end
end

Views::Sessions::New — sign in

 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
# app/views/sessions/new.rb
class Views::Sessions::New < Views::Base
  def page_title = "Sign in to KanbanFlow"

  def around_template
    render Views::Layouts::AuthLayout.new(title: page_title) do
      super
    end
  end

  def view_template
    div(class: "flex items-center justify-center min-h-screen px-4") do
      div(class: "w-full max-w-md") do
        div(class: "text-center mb-8") do
          h1(class: "text-3xl font-bold text-text") { "KanbanFlow" }
          p(class: "text-text-muted mt-2") { "Sign in to your account" }
        end

        div(class: "bg-surface rounded-lg border border-border p-8 shadow-sm") do
          form_with(url: session_path, class: "space-y-4") do |f|
            TextInput(field: :email_address, label: "Email address", type: "email", placeholder: "you@example.com")
            TextInput(field: :password, label: "Password", type:  "password")
            )

            div(class: "flex items-center justify-between") do
              Checkbox(field: :remember_me, label: "Remember me")
              Link(label: "Forgot password?", href: new_password_path, variant: :secondary)
            end

            Button(label: "Sign in", type: "submit")
          end

          div(class: "mt-6 text-center text-sm text-text-muted") do
              plain "Don't have an account? "
              Link(label: "Sign up", href: new_registration_path)
          end
        end
      end
    end
  end
end

Note that TextInput is called without a form: argument here — these are standalone inputs outside a form_with block. Check that TextInput handles a nil form: gracefully. If it requires a form object, pass f through or use a raw input tag instead.

Views::Passwords::New — forgot password

 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
# app/views/passwords/new.rb

class Views::Passwords::New < Views::Base
  def page_title = "Reset your password"

  def around_template
    render Views::Layouts::AuthLayout.new(title: page_title) do
      view_template
    end
  end

  def view_template
    div(class: "flex items-center justify-center min-h-screen px-4") do
      div(class: "w-full max-w-md") do
        div(class: "bg-surface rounded-lg border border-border p-8 shadow-sm") do
          h1(class: "text-xl font-bold text-text mb-2") { "Reset your password" }
          p(class: "text-text-muted text-sm mb-6") { "Enter your email and we'll send you a reset link." }

          form_with(url: passwords_path, class: "space-y-4") do |f|
            TextInput(
              field:       :email_address,
              label:       "Email address",
              type:        "email",
              placeholder: "you@example.com"
            )

            Button(label: "Send reset link", type: "submit")
          end

          div(class: "mt-4 text-center") do
            Link(label: "← Back to sign in", href: new_session_path, variant: :secondary)
          end
        end
      end
    end
  end
end

Views::Passwords::Edit — reset password

 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
# app/views/passwords/edit.rb
class Views::Passwords::Edit < Views::Base
  def page_title = "Set new password"

  def around_template
    render Views::Layouts::AuthLayout.new(title: page_title) do
      view_template
    end
  end

  def view_template
    div(class: "flex items-center justify-center min-h-screen px-4") do
      div(class: "w-full max-w-md") do
        div(class: "bg-surface rounded-lg border border-border p-8 shadow-sm") do
          h1(class: "text-xl font-bold text-text mb-6") { "Set new password" }

          form_with(url: password_path(params[:token]), method: :put,
                    class: "space-y-4") do |f|
            TextInput(field: :password, label: "New password", type:  "password")
            TextInput(field: :password_confirmation, label: "Confirm new password", type:  "password")
            Button(label: "Update password", type: "submit")
          end
        end
      end
    end
  end
end

Pointing the controllers at Phlex views

The generated controllers render ERB by convention. We need to ensure new and edit render our Phlex views.

1
2
3
4
5
6
7
8
# app/controllers/sessions_controller.rb
class SessionsController < ApplicationController
...

  def new
    render Views::Sessions::New.new
  end
...
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
# app/controllers/passwords_controller.rb
class PasswordsController < ApplicationController
...
def new
  render Views::Passwords::New.new
end
...
def edit
  render Views::Passwords::Edit.new
end
...