Layouts
In Phlex, everything is a component. A “view” is just a component that renders an entire HTML document — typically starting with <!doctype html>
and ending with </html>
. Views are also entrypoints — they’re usually rendered directly from a controller.
In this world of components, where do layouts fit in? Rails’ built-in view library, ActionView
, has a special concept of layouts where they are separate from the rest of your views (and partials) and treated differently. ActionView renders the view first and then wraps it in a layout right at the end.
There are a few problems with this approach:
- it adds complexity to the rendering process — there’s a whole different concept to understand with special rules;
- it necessitates the
content_for
helper and strangeyield
behaviours in order to get content from the view up into the layout; and - it makes it impossible to stream the response to the client until both the view and the layout are ready.
In Phlex, layouts are just components.
Layouts through composition
The simplest way to implement layouts in Phlex is through composition. You render a view from a controller and the first thing it does is render the layout component passing content via a block.
class ArticlesController < ApplicationController
layout false
def index
render Views::Articles::Index.new(
articles: Article.all.limit(10)
)
end
end
class Views::Articles::Index < Views::Base
def initialize(articles:)
@articles = articles
end
def view_template
Layout(title: "Articles") do
h1 { "Articles" }
ul do
@articles.each do |article|
li { article.title }
end
end
end
end
end
class Components::Layout < Components::Base
def initialize(title:)
@title = title
end
def view_template
doctype
html do
head do
title { @title }
end
body { yield }
end
end
end
Layouts through inheritance
Another way to implement layouts in Phlex is through inheritance. You create a base class for your views to inherit from, and that base view actually renders the layout around the template.
In this example, we use the around_template
hook. Alternatively, we could have just defined a view_template
method in the base class and called super
.
class ArticlesController < ApplicationController
layout false
def index
render Views::Articles::Index.new(
articles: Article.all.limit(10)
)
end
end
class Views::Articles::Index < Views::Base
def initialize(articles:)
@articles = articles
end
def page_title = "Articles"
def view_template
h1 { "Articles" }
ul do
@articles.each do |article|
li { article.title }
end
end
end
end
class Views::Base < Phlex::HTML
def around_template
doctype
html do
head do
title { page_title }
end
body { super }
end
end
end
Notice in the above example how we use a method to set the title of the page. The super class could call the method, requiring the subclass to implement it.
Combining inheritance and composition
The final approach is to combine both inheritance and composition. This final example has quite a lot going on, but it’s extremely flexible.
- the controller renders the view as normal
- the view defines
#page_title
and#layout
methods - the base view uses the
#around_template
hook to render the layout that was specified by the view - it passes a
page_info
object to the layout component containing the page title - finally, the layout takes the
page_info
object and renders as normal
class ArticlesController < ApplicationController
layout false
def index
render Views::Articles::Index.new(
articles: Article.all.limit(10)
)
end
end
class Views::Articles::Index < Views::Base
def initialize(articles:)
@articles = articles
end
def page_title = "Articles"
def layout = Layout
def view_template
h1 { "Articles" }
ul do
@articles.each do |article|
li { article.title }
end
end
end
end
class Views::Base < Phlex::HTML
PageInfo = Data.define(:title)
def around_template
render layout.new(page_info) do
super
end
end
def page_info
PageInfo.new(
title: page_title
)
end
end
class Components::Layout < Components::Base
def initialize(page_info)
@page_info = page_info
end
def view_template
doctype
html do
head do
title { @page_info.title }
end
body { yield }
end
end
end
Working with legacy layouts
Using Phlex for the whole view stack makes it faster and also means you can stream responses and use advanced features like selective rendering. But if you’re bringing Phlex into an existing Rails app, there’s no urgent need to convert your layouts over. In fact, the layouts are probably the lowest priority.
You’ll get way more benefits from extracting small, reusable components since you can immediately start rendering them in both your new Phlex views and your old Rails views. Plus, your new Phlex views will render just fine inside normal Rails layouts.
With that said, there is a way to write your layouts in Phlex while still rendering normal non-Phlex views from Rails, though you won’t be able to use streaming.
If you include Phlex::Rails::Layout
, components can be used as normal Rails layouts. You can even yield symbols to get content_for
blocks.
class Components::Layout < Components::Base
include Phlex::Rails::Layout
def view_template
doctype
html do
head do
title { yield(:title) }
end
body { yield }
end
end
end
In the controller, you need to specify the layout with a block.
class ArticlesController
layout { Components::Layout }
end