Skip to content

Lesson 3 — Layouts in Phlex

In Rails with ERB, layouts are a special concept — separate files with mysterious yield behaviour and content_for for hoisting content up from views into the layout. In Phlex, layouts are just components. There is nothing special to learn.

We will cover three approaches to wiring up a layout. But first, let’s build the layout component itself — it is the same regardless of which approach you choose.

Step 1 — Build AppLayout

Create the directory and file:

1
mkdir -p app/views/layouts
 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
# app/views/layouts/app_layout.rb
class Views::Layouts::AppLayout < 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(lang: "en") 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
        stylesheet_link_tag "tailwind", "data-turbo-track": "reload"
        stylesheet_link_tag "application", "data-turbo-track": "reload"
        stylesheet_link_tag "application", "data-turbo-track": "reload"
        javascript_importmap_tags
      end
      body(class: "bg-gray-50 text-gray-900") do
        render_nav
        main(class: "px-8 py-8") { yield }
      end
    end
  end

  private

  def render_nav
    nav(class: "bg-white border-b border-gray-200 px-4 py-3") do
      div(class: "flex items-center justify-between") do
        a(href: root_path, class: "font-bold text-lg text-gray-900") { "KanbanFlow" }
      end
    end
  end
end

This is a standard Phlex component. It accepts a title: prop, renders the full HTML document structure, and yields to render the page content inside <main>. Nothing layout-specific about it — it just happens to contain doctype and <html>.

Step 2 — Choose how to wire it up

There are three approaches. Read all three, then see which one KanbanFlow uses and why.


Approach 1 — Composition

Each view renders the layout explicitly, passing its content as a block:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
# app/views/boards/index.rb
class Views::Boards::Index < Views::Base
  def initialize(boards:)
    @boards = boards
  end

  def view_template
    render Views::Layouts::AppLayout.new(title: "Your Boards") do
      h1 { "Your Boards" }
      # ... rest of content
    end
  end
end

Every view is completely explicit about which layout it uses and what title it passes. This is the simplest approach to understand — there is no around_template hook or base class magic to reason about. Each view is self-contained.

The downside is repetition — every view has render Views::Layouts::AppLayout.new(title: "...") do ... end wrapping its content.


Approach 2 — Inheritance via around_template

The base view class handles the layout wrapping automatically. Individual views just define their content and optionally override page_title:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
# app/views/base.rb
class Views::Base < Components::Base
  def cache_store = Rails.cache

  def page_title = "KanbanFlow"

  def around_template
    render Views::Layouts::AppLayout.new(title: page_title) do
      super
    end
  end
end
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
# app/views/boards/index.rb
class Views::Boards::Index < Views::Base
  def page_title = "Your Boards"

  def initialize(boards:)
    @boards = boards
  end

  def view_template
    h1 { "Your Boards" }
    # ... rest of content
  end
end

Views are now minimal — just a title override and content. The layout wrapping is invisible. around_template is a Phlex hook that wraps the call to view_templatesuper is where the view’s own content renders.


Approach 3 — Legacy ERB compatibility

If you are in a hybrid setup and want a Phlex layout that still wraps non-Phlex ERB views, include Phlex::Rails::Layout:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
class Views::Layouts::AppLayout < Components::Base
  include Phlex::Rails::Layout

  def view_template
    doctype
    html do
      head { title { yield(:title) || "KanbanFlow" } }
      body { yield }
    end
  end
end
1
2
3
4
# app/controllers/application_controller.rb
class ApplicationController < ActionController::Base
  layout -> { Components::Layouts::AppLayout }
end

This lets ERB views render inside a Phlex layout without any changes to the views themselves. The right choice during migration when some views are still ERB and others are Phlex.


Step 3 — What KanbanFlow uses

KanbanFlow uses Approach 2 — inheritance via around_template.

Composition (Approach 1) is the simplest to understand and perfectly valid. We choose inheritance because KanbanFlow has many views and the repetition of wrapping every view explicitly would add noise without benefit. With around_template in Views::Base, every view gets the layout for free — there is nothing to remember and nothing to repeat.

Update Views::Base to add around_template while keeping the generated cache_store:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
# app/views/base.rb
# frozen_string_literal: true

class Views::Base < Components::Base
  def cache_store = Rails.cache

  def page_title = "KanbanFlow"

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

Views::Base inherits from Components::Base — so it already has Literal Properties, class_names, Routes helpers, and the Kit available. No need to add anything else.

Exercise

Create Views::Layouts::AppLayout as shown. Update Views::Base with around_template. Create a minimal placeholder view:

1
2
3
4
5
6
7
8
# app/views/home/index.rb
class Views::Home::Index < Views::Base
  def page_title = "Welcome"

  def view_template
    h1(class: "text-2xl font-bold") { "Welcome to KanbanFlow" }
  end
end

Add a route and controller action:

1
2
# config/routes.rb
root "home#index"
1
2
3
4
5
6
# app/controllers/home_controller.rb
class HomeController < ApplicationController
  def index
    render Views::Home::Index.new
  end
end

Run the app using bin/dev, then visit http://localhost:3000. You should see the nav, the heading inside <main>, and “Welcome” in the browser tab.