Phlex ๐Ÿ’ช

Phlex is a Ruby gem for building fast object-oriented HTML and SVG components. Views are described using Ruby constructs: methods, keyword arguments and blocks, which directly correspond to the output. For example, this is how you might describe an HTML <nav> with a list of links:

class Nav < Phlex::HTML
  def template
    nav(class: "main-nav") {
		 	ul {
				li { a(href: "/") { "Home" } }
				li { a(href: "/about") { "About" } }
				li { a(href: "/contact") { "Contact" } }
			}
		}
  end
end
<nav class="main-nav">
  <ul>
    <li><a href="/">Home</a></li>
    <li><a href="/about">About</a></li>
    <li><a href="/contact">Contact</a></li>
  </ul>
</nav>

Why render HTML in Ruby?

Building components in Ruby makes it possible to build powerful abstractions. The Nav menu above could be refactored into a Ruby class to allow developers to add items to the menu without needing to understand the underlying HTML.

class Nav < Phlex::HTML
  def template(&content)
  	nav(class: "main-nav") {
			ul(&content)
  	}
  end

  def item(url, &content)
  	li { a(href: url, &content) }
  end
end
<nav class="main-nav">
  <ul>
    <li><a href="/">Home</a></li>
    <li><a href="/about">About</a></li>
    <li><a href="/contact">Contact</a></li>
  </ul>
</nav>

The component can be called from Ruby or Erb without a bunch of <% %> tags.

render Nav.new do |nav|
	nav.item("/") { "Home" }
	nav.item("/about") { "About" }
	nav.item("/contact") { "Contact" }
end

Since the component is just a Ruby class, it can be extended with inheritence and modules. Let's create a subclass that works with Tailwind CSS.

class TailwindNav < Nav
	def template(&content) = nav(class: "flex flex-row gap-4", &content)

	def item(url, &content)
	  a(href: url, class: "text-underline", &content)
	end
end
<nav class="flex flex-row gap-4"><a href="/" class="text-underline">Home</a><a href="/about" class="text-underline">About</a><a href="/contact" class="text-underline">Contact</a></nav>

Your view data, code, and markup live together in the same place making it easier to reason through an application's UI. Since views are just Ruby, you get more flexibility than templating languages like Erb, Slim, Haml, and Liquid.

Getting Started

Installation

Install Phlex to your project by running:

$ bundle add phlex

This will add the following to your Gemfile:

gem "phlex"

and automatically run bundle install

Whatโ€™s a view?

Views are Ruby objects that represent a piece of output from your app. We plan to support various different types of output โ€” such as JSON, XML and SVG โ€” but for now, weโ€™re focusing on HTML.

Views can have an initialize method that dictates which arguments the view accepts and is responsible for setting everything up โ€” usually assigning instance variables for use in the template.

The template is a special method thatโ€™s called when rendering a view. The template method determines the output of the view by calling methods that append to the output.

Instance methods perform important calculations or encapsulate a small part of the template. Public instance methods can expose an interface thatโ€™s yielded to the parent when rendering.

HTML

HTML Views

You can create an HTML view by subclassing Phlex::HTML and defining a template instance method.

class Hello < Phlex::HTML
	def template
		h1 { "๐Ÿ‘‹ Hello World!" }
	end
end
<h1>๐Ÿ‘‹ Hello World!</h1>

The template method determines what your view will output when its rendered. The above example calls the h1 method which outputs an <h1> tag.

Accepting arguments

You can define an initializer for your views just like any other Ruby class. Letโ€™s make our Hello view take a name as a keyword argument, save it in an instance variable and render that variable in the template.

Weโ€™ll render this view with the arguments name: "Joel" and see what it produces.

class Hello < Phlex::HTML
	def initialize(name:)
		@name = name
	end

	def template
		h1 { "๐Ÿ‘‹ Hello #{@name}!" }
	end
end
<h1>๐Ÿ‘‹ Hello Joel!</h1>

Rendering views

Views can render other views in their templates using the render method. Let's try rendering a couple of instances of this Hello view from a new Example view and look at the output of the Example view.

class Example < Phlex::HTML
	def template
		render Hello.new(name: "Joel")
		render Hello.new(name: "Alexandre")
	end
end
<h1>๐Ÿ‘‹ Hello Joel!</h1>
<h1>๐Ÿ‘‹ Hello Alexandre!</h1>

Content

Views can also yield content blocks, which can be passed in when rendering. Let's make a Card component that yields content in an <article> element with a drop-shadow class on it.

class Card < Phlex::HTML
	def template
		article(class: "drop-shadow") {
			yield
		}
	end
end
class Example < Phlex::HTML
	def template
		render(Card.new) {
			h1 { "๐Ÿ‘‹ Hello!" }
		}
	end
end
<article class="drop-shadow">
  <h1>๐Ÿ‘‹ Hello!</h1>
</article>

The Example view renders a Card and passes it a block with an <h1> tag.

Looking at the output of the Example view, we can see the <h1> element was rendered inside the <article> element from the Card view.

Delegating content

Since the block of content was the only thing we need in the <article> element, we could have just passed the content block directly to the element instead.

class Card < Phlex::HTML
	def template(&)
		article(class: "drop-shadow", &)
	end
end

Hooks

You can hook into the rendering process by overriding before_template and after_template which are called immediately before and after the template is rendered.

You should always call super from these methods to allow for inherited callbacks.

class Example < Phlex::HTML
	def before_template
		h1 { "Before" }
		super
	end

	def template
		h2 { "Hello World!" }
	end

	def after_template
		super
		h3 { "After" }
	end
end
<h1>Before</h1>
<h2>Hello World!</h2>
<h3>After</h3>

Tags

HTML Tags

Phlex::HTML comes with methods that correspond to the most common HTML tags. Youโ€™ve seen the h1 tag in the previous section.

Content

You pass content as a block to a tag method. If the return value of the block is a String, Symbol, Integer or Float and no output methods were used, the return value will be output as text.

class Greeting < Phlex::HTML
	def template
		h1 { "๐Ÿ‘‹ Hello World!" }
	end
end
<h1>๐Ÿ‘‹ Hello World!</h1>

Attributes

You can add attributes to HTML elements by passing keyword arguments to the methods.

class Greeting < Phlex::HTML
	def template
		h1(class: "text-xl font-bold") { "๐Ÿ‘‹ Hello World!" }
	end
end
<h1 class="text-xl font-bold">๐Ÿ‘‹ Hello World!</h1>

Underscores _ are automatically converted to dashes - for Symbol keys. If you need to use an underscore in an attribute name, you can use a String key instead.

class Greeting < Phlex::HTML
	def template
		h1(foo_bar: "hello") { "๐Ÿ‘‹ Hello World!" }
		h1("foo_bar" => "hello") { "๐Ÿ‘‹ Hello World!" }
	end
end
<h1 foo-bar="hello">๐Ÿ‘‹ Hello World!</h1>
<h1 foo_bar="hello">๐Ÿ‘‹ Hello World!</h1>

Hash attributes

You can pass a Hash as an attribute value and it will be flattened with a dash - between each level.

class Greeting < Phlex::HTML
	def template
		div(data: { controller: "hello" }) {
			# ...
		}
	end
end
<div data-controller="hello"></div>

Boolean attributes

When an attribute value is true, the attribute name will be output without a value; when falsy, the attribute isnโ€™t output at all. You can use the strings "true" and "false" as values for non-boolean attributes.

class ChannelControls < Phlex::HTML
	def template
		input(
			value: "1",
			name: "channel",
			type: "radio",
			checked: true
		)

		input(
			value: "2",
			name: "channel",
			type: "radio",
			checked: false
		)
	end
end
<input value="1" name="channel" type="radio" checked><input value="2" name="channel" type="radio">

The template tag

Because the template method is used to define the view template itself, you'll need to use the method template_tag if you want to to render an HTML <template> tag.

class TemplateExample < Phlex::HTML
	def template
		template_tag {
			img src: "hidden.jpg", alt: "A hidden image."
		}
	end
end
<template><img src="hidden.jpg" alt="A hidden image."></template>

Registering custom tags

You can register custom elements with the register_element macro. The custom element will only be available in the view where it is registered and subclasses of that view.

class CustomTagExample < Phlex::HTML
	register_element :trix_editor

	def template
		trix_editor input: "content", autofocus: true
	end
end
<trix-editor input="content" autofocus></trix-editor>

Helpers

HTML Helpers

Stand-alone text

You can output text content without wrapping it in an element by using the plain method. It accepts a single argument which can be a String, Symbol, Integer or Float.

class Heading < Phlex::HTML
	def template
		h1 do
			strong { "Hello " }
			plain "World!"
		end
	end
end
<h1><strong>Hello </strong>World!</h1>

Whitespace

If you need to add whitespace, you can use the whitespace method. This is useful for adding space between inline elements to allow them to wrap.

class Links < Phlex::HTML
	def template
		a(href: "/") { "Home" }
		whitespace
		a(href: "/about") { "About" }
		whitespace
		a(href: "/contact") { "Contact" }
	end
end
<a href="/">Home</a> <a href="/about">About</a> <a href="/contact">Contact</a>

If you pass a block to whitespace, the content is wrapped in whitespace on either side.

whitespace { a(href: "/") { "Home" } }

Comments

The comment method takes a block and wraps the content in an HTML comment.

comment { "Hello" }

Conditional tokens

The tokens method helps you define conditional HTML attribute tokens such as CSS classes. You can use it to combine multiple tokens together.

tokens("a", "b", "c") # โ†’ "a b c"

You can use keyword arguments to specify the conditions for specific tokens. A condition can be a Proc or Symbol that maps to an instance method. The :active? Symbol for example maps to the active? instance method.

tokens(
	-> { true } => "foo",
	-> { false } => "bar"
) # โ†’ "foo"

Here we have a Link view that produces an <a> tag with the CSS class nav-item. If the link is active, we also apply the CSS class active.

class Link < Phlex::HTML
	def initialize(text, to:, active:)
		@text = text
		@to = to
		@active = active
	end

	def template
		a(href: @to, class: tokens("nav-item",
				active?: "active")) { @text }
	end

	private

	def active? = @active
end
class TokensExample < Phlex::HTML
	def template
		nav {
			ul {
				li { render Link.new("Home", to: "/", active: true) }
				li { render Link.new("About", to: "/about", active: false) }
			}
		}
	end
end
<nav>
  <ul>
    <li><a href="/" class="nav-item active">Home</a></li>
    <li><a href="/about" class="nav-item">About</a></li>
  </ul>
</nav>

Conditional classes

The classes method helps to create a token list of CSS classes. This method returns a Hash with the key :class and the value as the result of tokens, allowing you to destructure it into a keyword argument using the ** prefix operator.

class Link < Phlex::HTML
	def initialize(text, to:, active:)
		@text = text
		@to = to
		@active = active
	end

	def template
		a(href: @to, **classes("nav-item",
			active?: "active")) { @text }
	end

	private

	def active? = @active
end
class ClassesExample < Phlex::HTML
	def template
		nav {
			ul {
				li { render Link.new("Home", to: "/", active: true) }
				li { render Link.new("About", to: "/about", active: false) }
			}
		}
	end
end
<nav>
  <ul>
    <li><a href="/" class="nav-item active">Home</a></li>
    <li><a href="/about" class="nav-item">About</a></li>
  </ul>
</nav>

Unsafe output

unsafe_raw takes a String and outputs it without any safety or HTML escaping. You should never use this method with any string that could come from an untrusted person. In fact, you should pretty much never use this method. If you do, donโ€™t come crying when someone hacks your website.

If you think you need to use unsafe_raw, maybe open a discussion thread for other ideas.

Slots

You can build reusable & composable Phlex views.

For example, you may need to define multiple sections (slots) in a view. This can be accomplished by defining public instance methods on the view that accept blocks:

class Card < Phlex::HTML
	def template(&)
		article(class: "card", &)
	end

	def title(&)
		div(class: "title", &)
	end

	def body(&)
		div(class: "body", &)
	end
end
class CardExample < Phlex::HTML
	def template
		render Card.new do |card|
			card.title do
				h1 { "Title" }
			end

			card.body do
				p { "Body" }
			end
		end
	end
end
<article class="card">
  <div class="title">
    <h1>Title</h1>
  </div>
  <div class="body">
    <p>Body</p>
  </div>
</article>

This would work just fine for a list of views as each method can be called multiple times.

One caveat of defining the view this way is title and body could be called in any order. This offers flexibility, but what if you need to make sure your markup is output in a consistent order?

First, include Phlex::DeferredRender in your view. This changes the behavior of template so it does not receive a block and is yielded early. Then use public methods to save blocks, passing them to back to the template at render time.

class List < Phlex::HTML
	include Phlex::DeferredRender

	def initialize
		@items = []
	end

	def template
		if @header
			h1(class: "header", &@header)
		end

		ul do
			@items.each do |item|
				li { render(item) }
			end
		end
	end

	def header(&block)
		@header = block
	end

	def with_item(&content)
		@items << content
	end
end
class ListExample < Phlex::HTML
	def template
		render List.new do |list|
			list.header do
				"Header"
			end

			list.with_item do
				"One"
			end

			list.with_item do
				"two"
			end
		end
	end
end
<h1 class="header">Header</h1>
<ul>
  <li>One</li>
  <li>two</li>
</ul>

Testing Introduction

Testing Phlex Views

The Phlex::Testing::ViewHelper module defines render allowing you to render Phlex views directly in your tests and make assertions against the output.

Youโ€™ll need to require phlex/testing/view_helper and include Phlex::Testing::ViewHelper your test.

require "phlex/testing/view_helper"

class TestHello < Minitest::Test
	include Phlex::Testing::ViewHelper

	def test_hello_output_includes_name
		output = render Hello.new("Joel")
		assert_equal "<h1>Hello Joel</h1>", output
	end
end
class Hello < Phlex::HTML
	def initialize(name)
		@name = name
	end

	def template
		h1 { "Hello #{@name}" }
	end
end

Nokogiri

Testing HTML Views with Nokogiri [beta]

The phlex-testing-nokogiri gem provides helpers for working with rendered views as Nokogiri documents and fragments.

Installation

Add the following to the test group in your Gemfile and run bundle install.

gem "phlex-testing-nokogiri"

Testing Documents

If your view represents a whole HTML document, you can require phlex/testing/nokogiri and include the Phlex::Testing::Nokogiri::DocumentHelper module to render your view as Nokogiri::Document using the render method.

require "phlex/testing/nokogiri"

class TestExample < Minitest::Test
	include Phlex::Testing::Nokogiri::DocumentHelper

	def test_example
		output = render Example.new
		assert_equal "Hello Joel", output.css("h1").text
	end
end
class Hello < Phlex::HTML
	def initialize(name)
		@name = name
	end

	def template
		h1 { "Hello #{@name}" }
	end
end

Testing Fragments

If your view represents a fragment (partial), you can require phlex/testing/nokogiri and include the Phlex::Testing::Nokogiri::FragmentHelper module to render your view as Nokogiri::Fragment with the render method.

require "phlex/testing/nokogiri"

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

	def test_example
		output = render Example.new("Joel")
		assert_equal "Hello Joel", output.css("h1").text
	end
end
class Hello < Phlex::HTML
	def initialize(name)
		@name = name
	end

	def template
		h1 { "Hello #{@name}" }
	end
end

Capybara

Testing with Capybara [beta]

The phlex-testing-capybara gem provides a test helper that lets you use Capybara matchers.

Installation

Add the following to the test group in your Gemfile and run bundle install.

gem "phlex-testing-capybara"

Usage

Youโ€™ll need to require phlex/testing/capybara and include Phlex::Testing::Capybara::ViewHelper.

The render method will return a Capybara::Node::Simple and set the page attribute to the result.

require "phlex/testing/capybara"

class TestExample < Minitest::Test
	include Phlex::Testing::Capybara::ViewHelper

	def test_example
		render Example.new("Joel")
		assert_selector "h1", text: "Hello Joel"
	end
end
class Hello < Phlex::HTML
	def initialize(name)
		@name = name
	end

	def template
		h1 { "Hello #{@name}" }
	end
end

Testing Rails

Testing Phlex views in Rails

When you include Phlex::Testing::Rails::ViewHelper, views rendered in the test will have a view context, so they can use Rails helpers.

Rails Introduction

Getting started with Phlex on Rails

While Phlex can be used in any Ruby project, itโ€™s especially great with Rails. But before we get into the details, itโ€™s important to understand that Phlex is very different from ActionView and ViewComponent.

Setup

To use Phlex with Rails, youโ€™ll need to install the phlex-rails gem. Add the following to your Gemfile and run bundle install.

gem "phlex-rails"

Note, you do not need to install phlex separately because phlex is a dependency of phlex-rails.

Once the gem is installed, run the install generator.

bin/rails generate phlex:install

This script will:

  1. update config/application.rb to include app/views, app/views/components, and app/views/layouts in your auto-load paths;
  2. generate views/application_view.rb
  3. generate views/layouts/application_layout.rb
  4. generate views/components/application_component.rb

ApplicationComponent is your base component which all your other components inherit from. By default, ApplicationView inherits from ApplicationComponent.

Generators

Rails Generators

Component

bin/rails g phlex:component Card

This will generate a CardComponent in card_component.rb under app/views/components.

View

bin/rails g phlex:view Articles::Index

This will generate an Articles::IndexView in index_view.rb under app/views/articles.

Controller

bin/rails g phlex:controller Articles index show

This will generate an ArticlesController in app/controllers. It will have the actions index and show, which will render the views Articles::IndexView and Articles::ShowView generated in index_view.rb and show_view.rb under app/views/articles.

Rendering Views

Render Phlex views in Rails

You can render a Phlex view from your Rails controller actions or other views โ€” Phlex, ActionView or ViewComponent.

Instead of implicitly rendering an ERB template with automatic access to all your controller instance variables, youโ€™ll need to explicitly render Phlex views from your controller action methods.

class ArticlesController < ApplicationController
	layout -> { ApplicationLayout }

	def index
		render Articles::IndexView.new(
			articles: Article.all.load_async
		)
	end

	def show
		render Articles::ShowView.new(
			article: Article.find(params[:id])
		)
	end
end

Layouts

Layouts in Rails

If you ran the install generator, you should have an ApplicationLayout file under app/views/layouts/application_layout.rb.

You can configure a controller to use this layout with the layout method. Phlex layouts are even compatible with non-Phlex views.

class FooController < ApplicationController
	layout -> { ApplicationLayout }

	def index
		render Foo::IndexView
	end
end

Yielding content

Rails doesn't provide a mechanism for passing arguments to a layout component, but your layout can yield content provided by content_for.

class ApplicationLayout < Phlex::HTML
	include Phlex::Rails::Layout

	def template
		doctype

		html do
			head do
				title { yield(:title) }
			end

			body do
				yield
			end
		end
	end
end

Helpers

Using Rails Helpers

Phlex aims to ship with an adapter for every Rails view helper. (Please open an issue if we're missing one.)

Each adapter can be included from its own module under Phlex::Rails::Helpers, e.g. Phlex::Rails::Helpers::ContentFor. The module name will match the title-cased method name.

You can include these adapters as needed, or include the ones you use most commonly in ApplicationComponent or ApplicationView.

If you need to call the original unadapted helper, you can do that through the helpers proxy. For example, helpers.link_to "Home", "/" will return the HTML link as a String, while the adapter would output it.

Migrating to Phlex

Migrating an existing Rails app to Phlex

Whether you currently use ActionView or ViewComponent with ERB, HAML or Slim, you can start using Phlex in your Rails app today without a big rewrite.

You can render Phlex views into existing templates

Phlex views implement the renderable interface for Rails, which means they can be rendered from a controller or another view template โ€” even ViewComponent templates. This means you can gradually migrate specific views and components to Phlex without having to change everything at once.

If you're migrating from ViewComponent, you might find you can convert components to Phlex views without even changing any call-sites.

You can render ActionView partials and ViewComponent components in Phlex views

The render method in Phlex doesn't only work with Phlex views. You can use it to render ActionView partials and ViewComponent components.

Use an ERB โ†’ Phlex converter

The ERB โ†’ Phlex converter, Phlexing, can do the heavy-lifting but it won't help you architect your components / design system.

๐Ÿ“ Edit this page on GitHub