#04Phlex

Subscribe to our episode notifications

* indicates required

Intuit Mailchimp

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".

require "phlex"

class EpisodeCard < Phlex::HTML
  def template
    h2 { "Phlex" }
  end
end

puts EpisodeCard.new.call

# >> <h2>Phlex</h2>

We can create other tags, for example span or p.

span { "Tired of writing HTML?" }
p { "Tired of writing HTML?" }

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.

require "phlex"

class EpisodeCard < Phlex::HTML
  def template
    h2 { "Phlex" }

    p { "Tired of writing HTML?" }
    p { "Learn how to build fast, reusable and testable views in pure Ruby using Phlex" }
  end
end

puts EpisodeCard.new.call

# >> <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.

require 'htmlbeautifier'

def pretty_print(html)
  puts HtmlBeautifier.beautify(html)
end

If we change the puts call with a call to pretty_print, it now reads better.

require "phlex"

class EpisodeCard < Phlex::HTML
  def template
    h2 { "Phlex" }

    p { "Tired of writing HTML?" }
    p { "Learn how to build fast, reusable and testable views in pure Ruby using Phlex" }
  end
end

pretty_print EpisodeCard.new.call

# >> <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.

require "phlex"

class EpisodeCard < Phlex::HTML
  def template
    div {
      h2 { "Phlex" }

      div {
        p { "Tired of writing HTML?" }
        p { "Learn how to build fast, reusable and testable views in pure Ruby using Phlex" }
      }
    }
  end
end

pretty_print EpisodeCard.new.call

# >> <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.

require "phlex"

class EpisodeCard < Phlex::HTML
  def template
    div(id: "episode-5", class: "episode-card") {
      h2(class: "episode-card__title") { "Phlex" }

      div(class: "episode-card__description", data: { controller: "truncate" }) {
        p { "Tired of writing HTML?" }
        p { "Learn how to build fast, reusable and testable views in pure Ruby using Phlex" }
      }
    }
  end
end

pretty_print EpisodeCard.new.call

# >> <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.

require "phlex"

class EpisodeCard < Phlex::HTML
  def template
    div(id: "episode-#{@episode.id}", class: "episode-card") { # ~> NoMethodError: undefined method `id' for nil:NilClass
      h2(class: "episode-card__title") { @episode.title }

      div(class: "episode-card__description", data: { controller: "truncate" }) {
        @episode.description.lines do |line|
          p { line }
        end
      }
    }
  end
end

pretty_print EpisodeCard.new.call

# ~> 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.

require "phlex"

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
    div(id: "episode-#{@episode.id}", class: "episode-card") {
      h2(class: "episode-card__title") { @episode.title }

      div(class: "episode-card__description", data: { controller: "truncate" }) {
        @episode.description.lines do |line|
          p { line }
        end
      }
    }
  end
end

pretty_print EpisodeCard.new.call

# >> <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.

require "phlex"

class EpisodeCard < Phlex::HTML
  def initialize(episode:)
    @episode = episode
  end

  def template
    div(id: "episode-#{@episode.id}", class: "episode-card") {
      h2(class: "episode-card__title") { @episode.title }

      div(class: "episode-card__description", data: { controller: "truncate" }) {
        @episode.description.lines do |line|
          p { line }
        end
      }
    }
  end
end

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"
)

pretty_print EpisodeCard.new(episode: episode).call

# >> <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.

Subscribe to our episode notifications

* indicates required

Intuit Mailchimp

Comments