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.
"phlex"
require
class Badge < Phlex::HTML
def initialize(name:, kind: "primary")
@name = name
@kind = kind
end
def template
class: "badge-#{@kind}") { @name }
span(end
end
Badge.new(name: "error", kind: "danger").call
pretty_print
# >> <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.
"phlex"
require
class BadgesList < Phlex::HTML
def template
div {"My Badges" }
h1 {
ul {
li {class: "badge-primary") { "first" }
span(
}
li {class: "badge-warning") { "second" }
span(
}
li {class: "badge-error") { "third" }
span(
}
}
}end
end
BadgesList.new.call
pretty_print
# >> <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.
"phlex"
require
class BadgesList < Phlex::HTML
def template
div {"My Badges" }
h1 {
ul {
{"First" => "primary",
"Second" => "warning",
"Third" => "danger",
do |name, kind|
}.each
li {class: "badge-#{kind}") { name }
span(
}end
}
}end
end
BadgesList.new.call
pretty_print
# >> <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.
"phlex"
require
class BadgesList < Phlex::HTML
def initialize(names_and_kinds: )
@names_and_kinds = names_and_kinds
end
def template
div {"My Badges" }
h1 {
ul {@names_and_kinds.each do |name, kind|
li {class: "badge-#{kind}") { name }
span(
}end
}
}end
end
BadgesList.new(
pretty_print 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.
"phlex"
require
class BadgesList < Phlex::HTML
def initialize(names_and_kinds: )
@names_and_kinds = names_and_kinds
end
def template
div {"My Badges" }
h1 {
ul {@names_and_kinds.each do |name, kind|
li {Badge.new(name: name, kind: kind)
render
}end
}
}end
end
BadgesList.new(
pretty_print 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.
"phlex"
require
class HomePage < Phlex::HTML
def template
html {
head {"My Site" }
title {
}
body {
div {"Page content"
}
}
}end
end
HomePage.new.call
pretty_print
# >> <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 {"My Site" }
title {
}
body {"Before content" }
p {
yield
"After content" }
p {
}
}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.
Layout.new.call { |layout|
pretty_print "Page content" }
layout.p {
}
# >> <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
HomePage.new.call
pretty_print
# >> <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
HomePage.new.call
pretty_print
# >> <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 {"My Site" }
title {
}
body {"Before content" }
p {
yield
"After content" }
p {
}
}end
end
class HomePage < Layout
def template
div {"Page content"
}end
end
HomePage.new.call
pretty_print
# >> <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
HomePage.new.call
pretty_print
putsAboutPage.new.call
pretty_print
# >> <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
"phlex-rails" gem
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
Phlex::Rails::Layout
include
def template(&block)
doctype
do
html do
head "You're awesome" }
title { name: "viewport", content: "width=device-width,initial-scale=1"
meta
csp_meta_tag
csrf_meta_tags"application", data_turbo_track: "reload"
stylesheet_link_tag
javascript_importmap_tagsend
do
body
main(&block)end
end
end
end
We'll add some code to this layout in order to see it in action in time.
do
body "before content" }
p {
main(&)"after content" }
p { 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
Phlex::Rails::Helpers::Routes
include
if Rails.env.development?
def before_template
"Before #{self.class.name}" }
comment { 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
"Users::Index" }
h1 { "Find me in app/views/users/index_view.rb" }
p { 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
Users::IndexView.new
render 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
ApplicationLayout }
layout -> { 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
Users::IndexView.new(users: @users)
render 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 {class: "font-lg font-semibold") { "Users List" }
h1(
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
"UserItem" }
h1 { "Find me in app/views/components/user_item_component.rb" }
p { 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 {class: "font-lg font-semibold") { "Users List" }
h1(
ul {@users.each do |user|
li {UserItemComponent.new(user: user)
render
}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
"#{@user.name} (#{@user.id}) -- component"
plain 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.