#05More Phlex

Subscribe to our episode notifications

* indicates required

Intuit Mailchimp

Introduction

We've taken a look at how to write a component using Phlex. In this episode we'll further explore this awesome gem.

Render a component inside another

We have a Badge component. For it to be properly rendered, it requires a name and a kind.

And it's output is a span enlement with a class that interpolates the badge kind and an inner text that shows the badge name.

require "phlex"

class Badge < Phlex::HTML
  def initialize(name:, kind: "primary")
    @name  = name
    @kind  = kind
  end

  def template
    span(class: "badge-#{@kind}") { @name }
  end
end

pretty_print Badge.new(name: "error", kind: "danger").call

# >> <span class="badge-danger">error</span>

Now we want to create a badge list in order to show all of our badges. Lets write if from scratch.

We start by creating a hardcoded version so we can see the output we expect.

We'll have a title and the an unordered list of badges.

Each individual badge will be rendered inside a list item and will consist of a <span> element, with a class that inerpolates the kind and the name of the badge as the inner text.

Lets add a couple of badges.

And take a look at the output.

require "phlex"

class BadgesList < Phlex::HTML
  def template
    div {
      h1 { "My Badges" }

      ul {
        li {
          span(class: "badge-primary") { "first" }
        }
        li {
          span(class: "badge-warning") { "second" }
        }
        li {
          span(class: "badge-error")   { "third" }
        }
      }
    }
  end
end

pretty_print BadgesList.new.call

# >> <div>
# >>   <h1>My Badges</h1>
# >>   <ul>
# >>     <li><span class="badge-primary">first</span></li>
# >>     <li><span class="badge-warning">second</span></li>
# >>     <li><span class="badge-error">third</span></li>
# >>   </ul>
# >> </div>

We can continue by adding a hash with the badge information and iterating over it.

require "phlex"

class BadgesList < Phlex::HTML
  def template
    div {
      h1 { "My Badges" }

      ul {
        {
          "First"  =>  "primary",
          "Second" => "warning",
          "Third"  =>  "danger",
        }.each do |name, kind|
          li {
            span(class: "badge-#{kind}") { name }
          }
        end
      }
    }
  end
end

pretty_print BadgesList.new.call

# >> <div>
# >>   <h1>My Badges</h1>
# >>   <ul>
# >>     <li><span class="badge-primary">first</span></li>
# >>     <li><span class="badge-warning">second</span></li>
# >>     <li><span class="badge-danger">third</span></li>
# >>   </ul>
# >> </div>

And we can remove the hardcoding by adding an argument to the initializer and using it on the template.

Of course, we now need to pass the hash to the constructor before rendering.

So far, nothing new under the sun. But if we look in our template, the span line inside the list item element is the same exact line we used in our Badge component. We'd like to dry this out so we don't have to change code in two (or more) places every time we want to change our badge.

require "phlex"

class BadgesList < Phlex::HTML
  def initialize(names_and_kinds: )
    @names_and_kinds = names_and_kinds
  end

  def template
    div {
      h1 { "My Badges" }

      ul {
        @names_and_kinds.each do |name, kind|
          li {
            span(class: "badge-#{kind}") { name }
          }
        end
      }
    }
  end
end

pretty_print BadgesList.new(
               names_and_kinds: {
                 "First" =>  "primary",
                 "Second" => "warning",
                 "Third" =>  "danger",
               }
             ).call

# >> <div>
# >>   <h1>My Badges</h1>
# >>   <ul>
# >>     <li><span class="badge-primary">first</span></li>
# >>     <li><span class="badge-warning">second</span></li>
# >>     <li><span class="badge-danger">third</span></li>
# >>   </ul>
# >> </div>

Luckily, this is easy enough to do in with Phlex. We can just use our badge component, by passing it to the render method.

And if we re-render, we can see that the output is the same.

require "phlex"

class BadgesList < Phlex::HTML
  def initialize(names_and_kinds: )
    @names_and_kinds = names_and_kinds
  end

  def template
    div {
      h1 { "My Badges" }

      ul {
        @names_and_kinds.each do |name, kind|
          li {
            render Badge.new(name: name, kind: kind)
          }
        end
      }
    }
  end
end

pretty_print BadgesList.new(
               names_and_kinds: {
                 first:  :primary,
                 second: :warning,
                 third:  :danger,
               }
             ).call

# >> <div>
# >>   <h1>My Badges</h1>
# >>   <ul>
# >>     <li><span class="badge-primary">first</span></li>
# >>     <li><span class="badge-warning">second</span></li>
# >>     <li><span class="badge-danger">third</span></li>
# >>   </ul>
# >> </div>

Layouts

Lets switch examples. We have a home page, which needs to be rendered as a full HTML page.

For this we can just use the html, head and body methods as expected.

require "phlex"

class HomePage < Phlex::HTML
  def template
    html {
      head {
        title { "My Site" }
      }

      body {
        div {
          "Page content"
        }
      }
    }
  end
end

pretty_print HomePage.new.call

# >> <html><head><title>My Site</title></head><body>
# >>     <div>Page content</div>
# >>   </body></html>

That was easy enough, but what if we had a second page which also requires this structure? Should we duplicate this code?

Well, of course not. Phlex allows us to yield a block of code from one page to another in order to share code.

We can implement a Layout class with a template method populated with the content that will be repeated across the site.

Let's add a couple of markers before and after where the content should go.

Finally, we can yield a block to be filled in by the page content.

class Layout < Phlex::HTML
  def template
    html {
      head {
        title { "My Site" }
      }

      body {
        p { "Before content" }

        yield

        p { "After content" }
      }
    }
  end
end

In order to render some code from the template, we can pass the content block to the call method yielding our layout object to it. Now we can call tag methods on the yielded layout.

When we render, we see our layout with the content we passed to the block.

pretty_print Layout.new.call { |layout|
  layout.p { "Page content" }
}

# >> <html><head><title>My Site</title></head><body>
# >>     <p>Before content</p>
# >>     <p>Page content</p>
# >>     <p>After content</p>
# >>   </body></html>

This works, but having to add the code to the block is not ideal. What we'd like is to create our page and tell it to use a specific layout. So lets do that.

We first remove all the code that will be repeated across all the site from our HomePage; and we make it inherit from our Layout class.

When we try rendering it we can see that this doesn't work as we expected. Non of our layout code is there.

This happened because we've overriden the template method.

class HomePage < Layout
  def template
    div {
      "Page content"
    }
  end
end

pretty_print HomePage.new.call

# >> <div>Page content</div>

The first approach we can take to solve this problem is to add a call to super and pass in a block with our page code.

Which works just fine.

class HomePage < Layout
  def template
    super do
      div {
        "Page content"
      }
    end
  end
end

pretty_print HomePage.new.call

# >> <html><head><title>My Site</title></head><body>
# >>     <p>Before content</p>
# >>     <div>Page content</div>
# >>     <p>After content</p>
# >>   </body></html>

But this means we have to remember to add a call to super every time, which is not intuitive.

Instead of this, we can remove the super call and, on the Layout, use the around_template method, which is made just for this.

This produces the same output, but is more intuitive to write.

class Layout < Phlex::HTML
  def around_template
    html {
      head {
        title { "My Site" }
      }

      body {
        p { "Before content" }

        yield

        p { "After content" }
      }
    }
  end
end

class HomePage < Layout
  def template
    div {
      "Page content"
    }
  end
end

pretty_print HomePage.new.call

# >> <html><head><title>My Site</title></head><body>
# >>     <p>Before content</p>
# >>     <div>Page content</div>
# >>     <p>After content</p>
# >>   </body></html>

And if we want to add a second page, it's just a matter of inheriting from Layout and then rendering it.

class HomePage < Layout
  def template
    div {
      "Page content"
    }
  end
end

class AboutPage < Layout
  def template
    div {
      "About page content"
    }
  end
end

pretty_print HomePage.new.call
puts
pretty_print AboutPage.new.call

# >> <html><head><title>My Site</title></head><body>
# >>     <p>Before content</p>
# >>     <div>Page content</div>
# >>     <p>After content</p>
# >>   </body></html>
# >>
# >> <html><head><title>My Site</title></head><body>
# >>     <p>Before content</p>
# >>     <div>About page content</div>
# >>     <p>After content</p>
# >>   </body></html>

Rails

If you're working on a Rails project and you're interested in using Phlex, you can use the phlex-rails gem.

To do so, we first need to add it to the Gemfile

gem "phlex-rails"

and run bundle install.

$ bundle install

Next, we can run the phlex:install generator to add some default configurations and templates.

$ bin/rails generate phlex:install
insert  config/application.rb
insert  config/application.rb
insert  config/application.rb
insert  config/tailwind.config.js
create  app/views/components/application_component.rb
create  app/views/layouts/application_layout.rb
create  app/views/application_view.rb

Lets see what it created for us.

First, we have the ApplicationLayout, which is meant to be the default layout for all our Phlex pages. We can see that it inherits from ApplicationView.

class ApplicationLayout < ApplicationView
  include Phlex::Rails::Layout

  def template(&block)
    doctype

    html do
      head do
        title { "You're awesome" }
        meta name: "viewport", content: "width=device-width,initial-scale=1"
        csp_meta_tag
        csrf_meta_tags
        stylesheet_link_tag "application", data_turbo_track: "reload"
        javascript_importmap_tags
      end

      body do
        main(&block)
      end
    end
  end
end

We'll add some code to this layout in order to see it in action in time.

body do
  p { "before content" }
  main(&)
  p { "after content" }
end

ApplicationView is the base class for all of our views. It's an abstract class where we can add any behaviour we want present in all of our views.

This class inherits from ApplicationComponent.

class ApplicationView < ApplicationComponent
  # The ApplicationView is an abstract class for all your views.

  # By default, it inherits from `ApplicationComponent`, but you
  # can change that to `Phlex::HTML` if you want to keep views and
  # components independent.
end

Finally, ApplicationComponent is the base class for our components and it's a Phlex::HTML object, just as the ones we've been working on in this episode.

class ApplicationComponent < Phlex::HTML
  include Phlex::Rails::Helpers::Routes

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

That's the basic structure that phlex-rails gives us. Lets now see it in action.

We want to create an index page for our users. We can create it by using the phlex:view generator, which receives the class name as an argument, in our case Users::IndexView.

$ bin/rails g phlex:view Users::Index

This generator creates our Users::IndexView class with a default template method. Remember this class is a descendant of Phlex::HTML.

class Users::IndexView < ApplicationView
  def template
    h1 { "Users::Index" }
    p { "Find me in app/views/users/index_view.rb" }
  end
end

We can now go to our controller and render it. Note that we don't need to send the call message here, since Rails will take care of rendering it.

class UsersController < ApplicationController
  def index
    @users = User.all

    render Users::IndexView.new
  end
end

Lets now browse to the http://localhost:3000/users page. We see that the page has been loaded, but there's no layout.

In order to use the ApplicationLayout as the default layout for our application, we can add this line to the ApplicationController.

class ApplicationController < ActionController::Base
  layout -> { ApplicationLayout }
end

And now we see the layout (http://localhost:3000/users).

Since this is our index page, we need to have access to our application users. Lets write the code we want to have.

class UsersController < ApplicationController
  def index
    @users = User.all

    render Users::IndexView.new(users: @users)
  end
end

To make this code valid, we need to add the users: argument to the Users::IndexView constructor. Then we assign it and we can now use it in the template.

class Users::IndexView < ApplicationView
  def initialize(users:)
    @users = users
  end

  def template
    div {
      h1(class: "font-lg font-semibold") { "Users List" }

      ul {
        @users.each do |user|
          li {
            "#{user.username} (#{user.id})"
          }
        end
      }
    }
  end
end

When we reload http://localhost:3000/users we can see our users listed in the page.

Now, it looks like this user item can be used in multiple places in our application, so lets create a component for it.

We can use the phlex:component generator with the class name as an argument.

$ rails generate phlex:component UserItem

Our component looks similar to our view, except it inherits from ApplicationComponent.

# frozen_string_literal: true

class UserItemComponent < ApplicationComponent
  def template
    h1 { "UserItem" }
    p { "Find me in app/views/components/user_item_component.rb" }
  end
end

Now we can render our component the same way we learned at the beginning of this episode. We'll pass it the user to be rendered, the same way we did with our users list in the index page.

class Users::IndexView < ApplicationView
  def initialize(users:)
    @users = users
  end

  def template
    div {
      h1(class: "font-lg font-semibold") { "Users List" }

      ul {
        @users.each do |user|
          li {
            render UserItemComponent.new(user: user)
          }
        end
      }
    }
  end
end

We can now add the user to the constructor and assign it.

And add the code from our page into the template method, changing the calls to the user local variable to use the @user instance variable instead.

We'll also and add some extra text in order to see it change.

class UserItemComponent < ApplicationComponent
  def initialize(user:)
    @user = user
  end

  def template
    plain "#{@user.name} (#{@user.id}) -- component"
  end
end

And, when we reload our index page, we can see that it's now rendering the newly created UserItemComponent.

Conclusion

We've learned to create pages and components using Phlex both inside or outside Rails. I hope this encourages you to try it out in your next project. If you do it, let us know your experience.

Thank you for watching and I'll see you on the next episode.

Subscribe to our episode notifications

* indicates required

Intuit Mailchimp

Comments