Introduction
Say we are working on a Ruby or a Rails aplication and we'd like to write as much ruby as we can, both on the front and on the backend.
There are many alternatives to replace view templates with Ruby. In this episode we're going to take a look at one of them: Phlex.
According to the Phlex site: "Phlex is a framework for building fast, reusable, testable views in pure Ruby."
So lets try it out.
First steps
In order to use phlex, we first need to install it either for our project via the Gemfile
or globally using gem install
.
gem install phlex $
Creating a class
Lets build up an episode card component to show off how Phlex
works.
We start by creating a class that inherits from Phlex::HTML
.
A Plex::HTML
object must implement a method named template
.
This method defines the content that will be rendered by our class.
Inside that method, we have available a collection of methods that correlate one to one with the available HTML tag names.
For example, we can insert an <h2>
heading by calling the h2
method; and we'll add the episode title by passing a block that returns it as a String
.
We can render our component by creating an instance of the EpisodeCard
class and sending it the call
message.
And see that it effectively created an <h2>
tag with the text "Phlex".
"phlex"
require
class EpisodeCard < Phlex::HTML
def template
"Phlex" }
h2 { end
end
EpisodeCard.new.call
puts
# >> <h2>Phlex</h2>
We can create other tags, for example span
or p
.
"Tired of writing HTML?" }
span { "Tired of writing HTML?" } p {
Let's stick with the p
tag and add a few description lines to our card.
And render it.
As we can see, Phlex
's output is just a continuous string of HTML code, there's no line breaks or indentation. This is this way because the browser doesn't need any of that to properly render a page. In fact, adding line breaks or indentation, will add characters to the output that won't actually be appreciated.
"phlex"
require
class EpisodeCard < Phlex::HTML
def template
"Phlex" }
h2 {
"Tired of writing HTML?" }
p { "Learn how to build fast, reusable and testable views in pure Ruby using Phlex" }
p { end
end
EpisodeCard.new.call
puts
# >> <h2>Phlex</h2><p>Tired of writing HTML?</p><p>Learn how to build fast, reusable and testable views in pure Ruby using Phlex</p>
But it doesn't look good in a text buffer on a screencast. For that reason, I'll create a small helper method to make it look more readable.
Just keep in mind that this has nothing to do with Phlex
and that this is not the actual Phlex
output.
'htmlbeautifier'
require
def pretty_print(html)
HtmlBeautifier.beautify(html)
puts end
If we change the puts
call with a call to pretty_print
, it now reads better.
"phlex"
require
class EpisodeCard < Phlex::HTML
def template
"Phlex" }
h2 {
"Tired of writing HTML?" }
p { "Learn how to build fast, reusable and testable views in pure Ruby using Phlex" }
p { end
end
EpisodeCard.new.call
pretty_print
# >> <h2>Phlex</h2>
# >> <p>Tired of writing HTML?</p>
# >> <p>Learn how to build fast, reusable and testable views in pure Ruby using Phlex</p>
So far our compontent doesn't look like an actual HTML compontent because we just have a flat structure. We can add some nesting; it's as easy as creating a tag, say a <div>
and rendering the nested elements inside it's block.
We'll also create a <div>
for the episode description.
And render it to see the result.
"phlex"
require
class EpisodeCard < Phlex::HTML
def template
div {"Phlex" }
h2 {
div {"Tired of writing HTML?" }
p { "Learn how to build fast, reusable and testable views in pure Ruby using Phlex" }
p {
}
}end
end
EpisodeCard.new.call
pretty_print
# >> <div>
# >> <h2>Phlex</h2>
# >> <div>
# >> <p>Tired of writing HTML?</p>
# >> <p>Learn how to build fast, reusable and testable views in pure Ruby using Phlex</p>
# >> </div>
# >> </div>
Now, our episode card is missing an id, so lets add it to the root <div>
. To add attributes to an element, we can pass them as named arguments to the div
method.
We can do the same for the css classes. And even for data attributes.
And we can see them properly rendered.
"phlex"
require
class EpisodeCard < Phlex::HTML
def template
id: "episode-5", class: "episode-card") {
div(class: "episode-card__title") { "Phlex" }
h2(
class: "episode-card__description", data: { controller: "truncate" }) {
div("Tired of writing HTML?" }
p { "Learn how to build fast, reusable and testable views in pure Ruby using Phlex" }
p {
}
}end
end
EpisodeCard.new.call
pretty_print
# >> <div id="episode-5" class="episode-card">
# >> <h2 class="episode-card__title">Phlex</h2>
# >> <div class="episode-card__description" data-controller="truncate">
# >> <p>Tired of writing HTML?</p>
# >> <p>Learn how to build fast, reusable and testable views in pure Ruby using Phlex</p>
# >> </div>
# >> </div>
Embedded Ruby code
So far, we've only created harcoded information for our template, but what if we want to add some dynamic data?
If we had our hardcoded component as an ERB
template,
<div id="episode-5" class="episode-card">
<h2 class="episode-card__title">Phlex</h2>
<div class="episode-card__description" data-controller="truncate">
<p>Tired of writing HTML?</p>
<p>Learn how to build fast, reusable and testable views in pure Ruby using Phlex</p>
</div>
</div>
we could interpolate dynamic data using the 〈%= %〉
expansion tag and add loops and other control structures using the 〈% %〉
scriplet tag.
<div id="episode-〈%= @episode.id %〉" class="episode-card">
<h2 class="episode-card__title">〈%= @episode.title %〉</h2>
<div class="episode-card__description" data-controller="truncate">
〈% @episode.description.lines do |line| %〉<p>〈%= line %〉</p>
〈% end %〉</div>
</div>
How do we interpolate in Phlex
?. Well, since Phlex
components are just Ruby code, we can just write Ruby code.
The title can be just returned from the h2
block.
The description paragraphs can be obtained by iterating over the @episode.description.lines
and then calling the p
method and returning the line from the block.
And the @episode.id
can be interpolated directly onto the corresponding string. Note that we don't need specialized interpolation syntax.
Now this blows up because we don't actually have an episode to render.
"phlex"
require
class EpisodeCard < Phlex::HTML
def template
id: "episode-#{@episode.id}", class: "episode-card") { # ~> NoMethodError: undefined method `id' for nil:NilClass
div(class: "episode-card__title") { @episode.title }
h2(
class: "episode-card__description", data: { controller: "truncate" }) {
div(@episode.description.lines do |line|
p { line }end
}
}end
end
EpisodeCard.new.call
pretty_print
# ~> NoMethodError
# ~> undefined method `id' for nil:NilClass
# ~>
# ~> xmptmp-innpXHzP.rb:19:in `template'
# ~> /home/fedex/.rvm/gems/ruby-3.2.2/gems/phlex-1.8.1/lib/phlex/sgml.rb:122:in `block in __final_call__'
# ~> /home/fedex/.rvm/gems/ruby-3.2.2/gems/phlex-1.8.1/lib/phlex/sgml.rb:269:in `around_template'
# ~> /home/fedex/.rvm/gems/ruby-3.2.2/gems/phlex-1.8.1/lib/phlex/sgml.rb:107:in `__final_call__'
# ~> /home/fedex/.rvm/gems/ruby-3.2.2/gems/phlex-1.8.1/lib/phlex/sgml.rb:91:in `call'
# ~> xmptmp-innpXHzP.rb:31:in `<main>'
Lets instantiate it in the initializer just to see it in action.
"phlex"
require
class EpisodeCard < Phlex::HTML
def initialize
@episode = Episode.new(
id: 5,
title: "Phlex",
description: "Tired of writing HTML?\nLearn how to build fast, reusable and testable views in pure Ruby using Phlex"
)end
def template
id: "episode-#{@episode.id}", class: "episode-card") {
div(class: "episode-card__title") { @episode.title }
h2(
class: "episode-card__description", data: { controller: "truncate" }) {
div(@episode.description.lines do |line|
p { line }end
}
}end
end
EpisodeCard.new.call
pretty_print
# >> <div id="episode-5" class="episode-card">
# >> <h2 class="episode-card__title">Phlex</h2>
# >> <div class="episode-card__description" data-controller="truncate">
# >> <p>Tired of writing HTML?
# >> </p>
# >> <p>Learn how to build fast, reusable and testable views in pure Ruby using Phlex</p>
# >> </div>
# >> </div>
This works, but we're hardcoding our episode. In a real world scenario, our episode will be comming from outside of the card compontent.
In order to inject the episode to the component, we can add an argument to the initializer and assign it from there.
And then pass it in when we create our instance.
When we render it, we see the desired output.
"phlex"
require
class EpisodeCard < Phlex::HTML
def initialize(episode:)
@episode = episode
end
def template
id: "episode-#{@episode.id}", class: "episode-card") {
div(class: "episode-card__title") { @episode.title }
h2(
class: "episode-card__description", data: { controller: "truncate" }) {
div(@episode.description.lines do |line|
p { line }end
}
}end
end
Episode.new(
episode = id: 5,
title: "Phlex",
description: "Tired of writing HTML?\nLearn how to build fast, reusable and testable views in pure Ruby using Phlex"
)
EpisodeCard.new(episode: episode).call
pretty_print
# >> <div id="episode-5" class="episode-card">
# >> <h2 class="episode-card__title">Phlex</h2>
# >> <div class="episode-card__description" data-controller="truncate">
# >> <p>Tired of writing HTML?
# >> </p>
# >> <p>Learn how to build fast, reusable and testable views in pure Ruby using Phlex</p>
# >> </div>
# >> </div>
Conclusion
Writing HTML output in Ruby is not just convenient, but also very easy using Phlex
. I encourage you to try it out in your next project and ponder if it's the right fit for you.
I hope you've enjoyed this episode and I'll see you on the next one.