Set up Phlex UI components

Written by
Juan Gallardo
Published
Last updated

Table of Contents

Previous: Set up a new Ruby on Rails project Article 2 in Series: Build a blog with Ruby on Rails Next: Set up Article resource

This guide explains my set-up for using Phlex components. However, Phlex components are just Plain Ruby Objects, and this object-oriented approach to the View layer of our MVC is extremely flexible. Feel free to modify this system to suit your needs

Install Phlex

See Phlex Beta website

  1. Install the Phlex gem. We want the beta version.

bundle add phlex-rails --version=2.0.0.rc1

  1. Run the install generator

bundle exec rails generate phlex:install

Pages, Components, and ApplicationLayout

Set up a Component base class from which all our components will inherit

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# /app/views/components/_base.rb

# frozen_string_literal: true

class Components::Base < Phlex::HTML
  include Components

  # Include any helpers you want to be available across all components
  include Phlex::Rails::Helpers::Routes
  include Phlex::Rails::Helpers::AssetPath

  if Rails.env.development?
    def before_template
      comment { "Before #{self.class.name}" }
      super
    end
  end
end

Set up a Page base class which inherits from our Component class.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# /app/views/pages/_base.rb
# frozen_string_literal: true

class Pages::Base < Components::Base
  PageInfo = Data.define(:title)

  def around_template
    render layout.new(page_info) do
      super
    end
  end

  def page_info
    PageInfo.new(
      title: page_title
    )
  end
end

Add a layout

Each page can specify its own layout.

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
class ApplicationLayout < Components::Base

  def initialize(page_info)
    @page_info = page_info
  end

  def view_template
    doctype

    html(lang: "en") do
      head do
        ...
      end

      body {
        header do
          Navbar do |navbar|
            navbar.links do
              a(href: root_path) { "Home" }
              a(href: about_path) { "About" }
              a(href: contact_path) { "Contact" }
              a(href: articles_path) { "Blog" }
            end
          end
        end

        main do
          yield
        end

        footer do
          p { "Footer" }
        end
      }
    end
  end
end

Explanation:

Configure Rails to auto load our components

Folder structure:

my_app_root/
├─ app/
│  ├─ views/
│  │  ├─ components/
│  │  │  ├─ link/
│  │  │  │  ├─ link.rb
│  │  │  ├─ navbar/
│  │  │  │  ├─ navbar.rb
│  │  │  ├─ _base.rb
│  │  ├─ layouts/
│  │  │  ├─ application_layout.rb
│  │  ├─ pages/
│  │  │  ├─ _base.rb
│  │  │  ├─ home.rb
│  │  │  ├─ about.rb
│  │  │  ├─ contact.rb

Write an initializer to define our Pages and Components modules

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# /config/initializers/phlex.rb

# frozen_string_literal: true

module Pages
end

module Components
  extend Phlex::Kit
end

Rails.autoloaders.main.push_dir(
  "#{Rails.root}/app/views/pages", namespace: Pages
)

Rails.autoloaders.main.push_dir(
  "#{Rails.root}/app/views/components", namespace: Components
)
Rails.autoloaders.main.collapse("#{Rails.root}/app/views/components/*")

Explanation:

Modify autoload_paths

The following code fragment is simplified for convenience. Add the relevant lines to your file

1
2
3
4
5
6
7
8
9
# /config/application.rb

module App
  class Application < Rails::Application
    config.autoload_paths << "#{root}/app/views/layouts"
    config.autoload_paths << "#{root}/app/views/components"
    config.autoload_paths << "#{root}/app/views/pages"
  end
end

Pages

Set up a PagesController

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# /app/controllers/pages_controller.rb

class PagesController < ApplicationController
  def home
    render Pages::Home.new
  end

  def about
    render Pages::About.new
  end

  def contact
    render Pages::Contact.new
  end
end

Write some placeholder pages

1
2
3
4
5
6
7
8
9
10
11
# /app/views/pages/home.rb

class Pages::Home < Pages::Base
  def page_title = "Home"

  def layout = ApplicationLayout

  def view_template(&)
    h1 { "Home" }
  end
end

Do the same for other static pages

Your first custom components

1
2
3
4
5
6
7
8
9
10
11
12
13
# /app/view/components/navbar/navbar.rb

class Components::Navbar < Components::Base
  def view_template(&)
    div(class: "container") do
      div(class: "nav", &)
    end
  end

  def links(&)
    nav(class: "links", &)
  end
end
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
# /app/view/components/link/link.rb

class Components::Link < Components::Base
  def initialize(href:, is_disabled: false)
    @href        = href
    @is_disabled = is_disabled
    super()
  end

  def view_template(&)
    tag do
      yield     
    end
  end

  def tag(&)
    if @is_disabled
      span(class: [ ("link"),
                 ("link--disabled") ]) do
        yield
      end
    else
      a(class: "link", href: @href) do
        yield
      end
    end
  end
end

This should give you an idea of what’s possible with Phlex components. In our application layout we are initializing our Navbar component with Link instances. We are giving these links an is_disabled argument that is based on whether the current route matches the link target. The Link component encapsulates the business logic for rendering disabled links. Pretty cool, right?