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
endIn 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 installNext, 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.rbLets 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
endWe'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" }
endApplicationView 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.
endFinally, 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
endThat'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::IndexThis 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
endWe 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
endLets 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 }
endAnd 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
endTo 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
endWhen 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 UserItemOur 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
endNow 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
endWe 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
endAnd, 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.