On using Phlex

by Paweł Świątkowski
03 Oct 2023

Seb Wilgosz recently published an article Phlex with Hanami on Hanami Mastery. It made the latest Ruby Weekly issue as the first item and generally was quite well received. I write this post as an addendum to Seb’s article, giving my perspective on using Phlex in Ruby applications.

Important note: you should probably read Seb’s article first, as I don’t do an introduction to Phlex here.

Establishing credentials

Who am I to talk? I have been using Phlex in my toy project, writing a forum engine in Hanami. I also added it to some pages in one of my oldest Rails projects, serving a small community in production for 10+ years (source code not available). It’s not too much, but more than just playing around with hello worlds.

Phlex – a missing view layer we always needed

The most important characteristic of Phlex in my opinion is that, at least for Rails, it’s a view layer we never had but always needed. Rails, despite stating that it’s MVC, does not really offer much on the “V” front. It feels much more like a model-template-controller, where templates are dumb and typical responsibilities of a view are shoved into a model, controller or a separate kind of object (like form objects). Oh, and there are helpers that do not fit anywhere in this model.

Let’s take a quick example. The requirement is:

User has an optional avatar attached. In the user profile view we display that avatar. If the avatar is not present, we use a placeholder image. However, if it’s a VIP user, the placeholder image is different than the one for a regular user.

With “traditional Rails” I wouldn’t be surprised if I saw it added as avatar_url method of a User model. The alternative is to put the logic directly in the ERB with ifs and elses, or to use a helper method. The first issue here is that Rails does not really offer an answer on where to put that kind of logic. The second - that all these options are flawed in some way:

  • In a model – this is a purely presentational thing, it simply does not belong in the model
  • In an ERB file – it makes the template less readable, the logic is not reusable (unless you use partials) and perhaps more importantly: it’s really hard to test, as Rails basically offers only testing views by executing the whole controller action
  • In a helper – it is probably the right place, but many people will reject this as “not OOP enough”

How would that look in Phlex? (a proper view layer)

class UserProfile < Phlex::HTML
  def initialize(user)
    @user = user
  end
    
  def template
    div(class: "user-profile") do
      img(src: avatar_url)
    end
  end
    
  private
    
  def avatar_url
    if @user.has_avatar?
      @user.avatar_url(:small)
    elsif @user.vip?
      "/path/to/vip_avatar_placeholder.png"
    else
      "/path/to/avatar_placeholder.png"
    end
  end
end

That’s it. this is just a Ruby object with a state (user is available via an instance variable) where we can define all kinds of helper methods we want. It’s all collocated together, you don’t need to do file-jumping in search of a definition of user_avatar_url helper method. It’s also testable.

Phlex is Ruby - it’s extremely testable

To test the code above you don’t need to instrument the whole request machinery. You don’t even need a user persisted in the database, you also don’t need to set up a session. It’s a simple test, testing exactly what you need to test. It’s also extremely fast to run.

require "phlex/testing/view_helper"

class TestHello < Minitest::Test
  include Phlex::Testing::Nokogiri::FragmentHelper

  def test_user_with_avatar
    user = build_user(avatar: "some_file.png")
    output = render UserProfile.new(user)
    assert_equal "/path/to/avatars/#{user.id}/avatar.png", output.css("img").attr("src")
  end
    
  def test_user_without_avatar_but_vip
    user = build_user(avatar: nil, vip: true)
    output = render UserProfile.new(user)
    assert_equal "/path/to/vip_avatar_placeholder.png", output.css("img").attr("src")
  end
  
  def test_user_without_avatar
    user = build_user(avatar: nil, vip: false)
    output = render UserProfile.new(user)
    assert_equal "/path/to/avatar_placeholder.png", output.css("img").attr("src")
  end
end

Phlex is Ruby - you can do Ruby things with it

Phlex views being pure Ruby classes mean that you can do everything what you could do with any other Ruby class. I, for example, use a mixin to include common typography helpers in a Phlex view.

class ArticlePartial < Phlex::HTML
  include Ui::Typography
  
  def initialize(article)
    @article = article
  end
    
  def template
    article(class: "article") do
      heading1(@article.title)
      subtitle(@article.motto)
      
      div(class: "content") { @article.content }
    end
  end
end

In this example both heading1 and subtitle are just methods in Ui::Typography module.

But that’s not it. If you really want it for some reason, you can do really fancy Ruby things, like this:

class Page < Phlex::HTML
  def template
    emph = [:i, :b, :u].sample
    
    div do
      plain "this text will have "
      public_send(emph) { "emphasis"}
      plain " of some kind"
    end
  end
end

On each render, the word “emphasis” will randomly be underlined, bold or in italics. Because why not?

Phlex is Ruby - you can package it

If you have a set of components, such as Card, DataTable, PaginationControls etc., that just accept simple primitive values (strings, numbers, arrays, but not objects such as models), you can easily put them in a gem. And suddenly you have a distributable design system, which you could use across several applications - and have them look the same.

Some final words

This is my experience with working with Phlex so far. However, there might be some questions popping up in your head.

Should I rewrite everything in Phlex?

Phlex has some downsides. The most important is that is abstracts out HTML as Ruby and it might have some negative consequences on your workflow. If you are copying a lot of HTML from open source projects, StackOverflow or HTML file prepared by your designer, you will have to convert everything into Phlex Ruby code. This might be a huge slowdown.

Another problem is if you expect a designer or a UX specialist to actually modify HTML templates in your project. While ERB views are almost-HTML, Phlex views are not. It might be difficult to comprehend how they work for people not knowing Ruby.

How does it compare to ViewComponent?

I have used both ViewComponent and Cells when ViewComponent has not yet been born. They both offer a bit different approach, in which you put a mini-controllers inside your templates. There controllers perform some logic and then render another template, defined in a separate file. For many people, this might feel more familiar or more in line with Rails philosophy.

I personally think that the Phlex approach is closer to the original Rails approach to views. It does not introduce the whole new layer of those mini-controller-components rendering their own views (so you get model-view-template-component-componenttemplate), but rather replaces the dumb default view layer with something smarter, more Rubyish and testable.

Of course, everyone’s mileage can vary. These are just tools that are, in fact, very different and it’s not like one is automatically better than another.

end of the article

Tags: ruby

This article was written by me – Paweł Świątkowski – on 03 Oct 2023. I'm on Fediverse (Ruby-flavoured account, Elixir-flavoured account). Let's talk.