Skip to content

Lesson 5 — Rails helpers in Phlex

Rails view helpers — link_to, image_tag, form_with, route helpers — work in Phlex through an adapter system. Adapters are not included by default, which keeps Phlex components lean.

Routes helpers

Routes are the most commonly needed helpers. Include them in Components::Base and Views::Base so they are available everywhere:

1
2
3
4
5
class Components::Base < Phlex::HTML
  include Phlex::Rails::Helpers::Routes
  extend Literal::Properties
  # ...
end

The generator already does this — but it is worth understanding why. With routes included, you can use path helpers directly:

1
2
3
a(href: boards_path) { "All boards" }
a(href: new_board_path) { "New board" }
a(href: board_path(@board)) { @board.name }

Do you need link_to?

The Phlex docs make a good point: do you really need link_to when you have the a tag? In most cases, no:

1
2
3
4
5
# ERB with link_to
link_to "Edit", edit_board_path(@board), class: "btn"

# Phlex with a tag — equally readable
a(href: edit_board_path(@board), class: "btn") { "Edit" }

The a tag with a route helper is cleaner in Phlex than link_to. You only need the link_to adapter if you are porting existing code that uses it heavily.

Including other adapters

If you need a specific helper, include its adapter. The module name matches the helper name in TitleCase:

1
2
3
4
5
6
7
class Components::Base < Phlex::HTML
  include Phlex::Rails::Helpers::Routes
  include Phlex::Rails::Helpers::LinkTo
  include Phlex::Rails::Helpers::ImageTag
  include Phlex::Rails::Helpers::ContentFor
  include Phlex::Rails::Helpers::TurboFrameTag
end

Tip: If you call a helper you haven’t included, Phlex gives you a helpful error message that tells you exactly which adapter to include. You don’t need to know the full list upfront — just add adapters as you need them.

CSRF and meta tags

These are needed in the layout head. Include their adapters in Components::Layouts::AppLayout or in Components::Base:

1
2
3
4
include Phlex::Rails::Helpers::CSRFMetaTags
include Phlex::Rails::Helpers::CSPMetaTag
include Phlex::Rails::Helpers::StylesheetLinkTag
include Phlex::Rails::Helpers::JavascriptImportmapTags

Then in the layout:

1
2
3
4
5
6
head do
  csrf_meta_tags
  csp_meta_tag
  stylesheet_link_tag "application", "data-turbo-track": "reload"
  javascript_importmap_tags
end

Custom helpers

Rails application helpers live in app/helpers/ and are automatically available in ERB views. In Phlex they are not automatically available — you register them explicitly on your base class.

A typical example is current_user. In a standard Rails app with authentication, current_user is defined in ApplicationController and made available to views as a helper method:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
# app/controllers/application_controller.rb
class ApplicationController < ActionController::Base
  helper_method :current_user

  private

  def current_user
    @current_user ||= User.find_by(id: session[:user_id])
  end
end

To make current_user available inside Phlex components and views, register it on Components::Base:

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

  register_value_helper :current_user
  ...
end

Now you can call current_user directly inside any component or view:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
class Views::Layouts::AppLayout < Components::Base
  def render_nav
    nav do
      a(href: root_path) { "KanbanFlow" }
      if current_user
        span { "Signed in as #{current_user.email}" }
      end
    end
  end
end

For helpers defined in app/helpers/application_helper.rb:

1
2
3
4
5
6
# app/helpers/application_helper.rb
module ApplicationHelper
  def formatted_date(date)
    date.strftime("%d %B %Y")
  end
end

Register it as a value helper since it returns a value rather than outputting HTML directly:

1
register_value_helper :formatted_date

Then use it directly in any component or view:

1
p { formatted_date(board.created_at) }

Output helpers are for methods that generate and push HTML directly to the buffer — like pagination helpers from gems such as Pagy:

1
register_output_helper :pagy_nav

The distinction matters: output helpers push their return value directly to the buffer, while value helpers return a value you use in an expression. Getting this wrong produces either double-rendered output or escaped HTML strings appearing as visible text on the page.

Never include Rails helper modules directly into a Phlex component. Rails helper modules are not designed to work with Phlex and may override core Phlex methods like capture, causing subtle and hard-to-diagnose bugs. Always use register_value_helper or register_output_helper instead.

We don’t have authentication wired up yet — that comes in Module 10. So current_user won’t work as shown until then. But establishing the pattern now means when authentication arrives, you know exactly where to register it.


Date and time formats

Formatting dates consistently across an app is a common need. The instinct is to reach for a helper method — but Ruby already has a clean built-in solution that needs no helper registration at all.

Date::DATE_FORMATS and Time::DATE_FORMATS are Ruby hashes you can extend in an initializer to define named formats. Once defined, call them anywhere with to_s(:format_name) — in components, views, models, or the console:

1
2
3
4
5
6
7
8
# config/initializers/date_formats.rb
Date::DATE_FORMATS[:short]     = "%d %b %Y"           # 15 Jan 2025
Date::DATE_FORMATS[:long]      = "%d %B %Y"            # 15 January 2025
Date::DATE_FORMATS[:with_day]  = "%A %d %B %Y"         # Wednesday 15 January 2025

Time::DATE_FORMATS[:short]     = "%H:%M %d %b %Y"     # 09:30 15 Jan 2025
Time::DATE_FORMATS[:long]      = "%H:%M %A %d %B %Y"  # 09:30 Wednesday 15 January 2025
Time::DATE_FORMATS[:time_only] = "%H:%M"               # 09:30

Usage in a Phlex component — no helper adapter, no registration, just to_fs with a named format:

1
2
3
4
5
6
7
8
class Views::Boards::Show < Views::Base
  def view_template
    h1(class: "text-2xl font-bold") { @board.name }
    p(class: "text-sm text-gray-500") do
      plain "Created #{@board.created_at.to_fs(:long)}"
    end
  end
end

Define your app’s full set of formats in config/initializers/date_formats.rb at the start of the project. Consistent naming means to_fs(:short) always means the same thing everywhere, and changing a format means changing one line.

What about I18n.localize? Rails also ships with I18n.localize (aliased as l in views) which reads formats from locale YAML files. This is the right choice if you need multi-locale support — different date formats for different languages. For a single-locale app like KanbanFlow, DATE_FORMATS is simpler and sufficient. If you do need l in a Phlex component, register it as a value helper: register_value_helper :l.

The helpers proxy

For any helper that doesn’t have an adapter and you haven’t registered, access it through the helpers proxy. This returns the value rather than outputting it:

1
2
3
4
5
# helpers.link_to returns the HTML string — doesn't output it
raw safe(helpers.link_to("Edit", edit_board_path(@board)))

# Better — just use the a tag directly
a(href: edit_board_path(@board)) { "Edit" }

In practice, reaching for helpers is a code smell — there’s almost always a cleaner Phlex alternative.

Exercise

Set up the app’s date and time formats. Create the initializer and add some defaults:

Then add a footer to AppLayout that uses them:

1
2
3
4
5
6
7
def render_footer
  footer(class: "border-t border-gray-200 mt-12 py-4 px-4") do
    div(class: "mx-auto max-w-7xl text-sm text-gray-400") do
      plain "© KanbanFlow #{Date.today.to_fs(:short)}"
    end
  end
end

Now update the body in the view-template of AppLayout to use the new footer (we’re also doing a bit of css change to force the footer to the bottom of the page).

1
2
3
4
5
      body(class: "bg-gray-50 text-gray-900 min-h-screen flex flex-col") do
        render_nav
        main(class: "mx-auto max-w-7xl px-4 py-8 flex-1 w-full") { yield }
        render_footer
      end

Restart the server so the initializer is picked up, then visit http://localhost:3000. You should see the footer with the current date and time formatted using your named format.

Verify the formats work as expected in the Rails console:

1
2
3
4
Time.now.to_fs(:short)     # => "09:30 15 Jan 2025"
Time.now.to_fs(:long)      # => "09:30 Wednesday 15 January 2025"
Time.now.to_fs(:time_only) # => "09:30"
Date.today.to_fs(:with_day) # => "Wednesday 15 January 2025"