Building your first component in Phlex
Let’s start by building a simple Card component. A Card is a presentational component, it doesn’t really have any behavior. So it’s going to be mostly for abstracting away the CSS classes and specific markup. We’ll use simple classes in our example, but you could imagine if you were using a utility based CSS framework like Tailwind that you would be abstracting away a lot more.
# app/components/card.rb
class Components::Card < Phlex::HTML
def view_template(&)
div(class: "card", &)
end
end
Not the most exciting component, but it’s a start. We can use it like this:
class View < Phlex::HTML
def view_template
render Card.new do
p { "Card content" }
end
end
end
<div class="card">
<p>Card content</p>
</div>
This is nice, but some of our cards will need a header section that has a title. Let’s keep adding to our Card
component so that it can have an optional header:
class Card < Phlex::HTML
def view_template(&)
div(class: "card", &)
end
def header(&)
div(class: "card-header", &)
end
def title(&)
h2(class: "card-title", &)
end
end
We’ve added header
and title
methods to our Card
component. Now when we render a card, we can use the block argument, which will be the instance of our Card
, and call our new methods:
class View < Phlex::HTML
def view_template
render Card.new do
render Card.new do |c|
c.header do
c.title { "Card Title" }
end
p { "Card content" }
end
end
end
<div class="card">
<div class="card-header">
<h2 class="card-title">Card Title</h2>
</div>
<p>Card content</p>
</div>
Ok, this is starting to look a bit better. But we also need a way to add an action button to our card. This will need a bit more flexibility than just changing the content of the button. Let’s see what that looks like:
class Card < Phlex::HTML
def view_template(&)
div(class: "card", &)
end
def header(&)
div(class: "card-header", &)
end
def title(&)
h2(class: "card-title", &)
end
def action(**, &)
a(class: "card-action", **, &)
end
end
This will let us pass in attributes to the action button.
class View < Phlex::HTML
def view_template
render Card.new do |c|
c.header do
c.title { "Card Title" }
end
p { "Card content" }
c.action(href: "/") { "Action" }
end
end
end
<div class="card">
<div class="card-header">
<h2 class="card-title">Card Title</h2>
</div>
<p>Card content</p>
<a class="card-action" href="/">Action</a>
</div>
This seems like it’s working, but we actually have a bit of a problem. We can’t specify a class for our action button without overriding the class provided in the action
method. Any custom class or classes we provide will clobber the default card-action
class. Phlex provides a helper method to help us deal with this situation called mix
. It mixes attributes together, and is aware of token lists. Let’s update our action
method to use mix
:
def action(**attributes, &)
a(**mix(class: "card-action", attributes), &)
end
Now if we specify a custom class when we call our action
method, it will be added to the class list instead of overriding it.
class View < Phlex::HTML
def view_template
render Card.new do |c|
c.header do
c.title { "Card Title" }
c.action(class: "custom-class") { "Action" }
end
p { "Card content" }
end
end
end
<div class="card">
<div class="card-header">
<h2 class="card-title">Card Title</h2>
<a class="card-action custom-class" href="/">Action</a>
</div>
<p>Card content</p>
</div>