Illustration in bright colors. Woman at laptop with long hear and hearts in background

What’s New in Phoenix 1.7 + LiveView .18 – File Structure and Templates

I recently had the opportunity to use Phoenix 1.7 and LiveView .18.3 in a production environment and it was, in a word, amazing.

It’s hard to know where to start, as I’m all 😍 over the whole experience, but this series will introduce some of the most important aspects of Phoenix 1.7 that you need to know going in.

While it’s pretty straightforward to upgrade to 1.7 from 1.6, and everything is backward-compatible (though you need to make some mods to use all the latest features), I’m demonstrating these new features in a fresh new project, and I recommend you do the same.

If you haven’t already, update your generator:

mix archive.install hex phx_new

Then start a new project:

mix phx.new hey_girl

(hey_girl is my own personal, more casual take on hello_world, but you can sub whatever greeting you’re most comfortable with)

If we look in our mix.exs file, you’ll see that we are using Phoenix 1.7.0 and LiveView 0.18.3. You’ll also notice that TailwindCSS is now included by default in new installations, with no more dependency on npm or need to manually install.

So we’re off to a great start. Let’s keep exploring.

A new way of laying things out

The hey_girl_web directory, which is currently simply a landing page for a new Phoenix project, looks like this:

lib/hey_girl_web
β”œβ”€β”€ controllers
β”‚   β”œβ”€β”€ page_controller.ex
β”‚   β”œβ”€β”€ page_html.ex
β”‚   β”œβ”€β”€ error_html.ex
β”‚   β”œβ”€β”€ error_json.ex
β”‚   └── page_html
β”‚       └── home.html.heex
β”œβ”€β”€ components
β”‚   β”œβ”€β”€ core_components.ex
β”‚   β”œβ”€β”€ layouts.ex
β”‚   └── layouts
β”‚       β”œβ”€β”€ app.html.heex
β”‚       └── root.html.heex
β”œβ”€β”€ endpoint.ex
└── router.ex

On the top level of that directory, where we once found controllers, templates, and views, we now just have two directories – controllers and components.

Let’s first dive into the new components directory, where there are a few things worth noting:

  • app.html.heex and root.html.heex, formerly located in templates, now live within a sub-directory called layouts. Also, live.html.heex is gone.
  • There’s a new file called layouts.ex
  • There’s also a new file called core_components.ex (but let’s save that one for Part 2 of this series)

Goodbye Phoenix.View; Hello Phoenix.Component + Phoenix.Template

In previous versions of Phoenix, the view and templating were controlled by Phoenix.View, while in Phoenix 1.7, Phoenix.View has been removed as a dependency in favor of Phoenix.Template, which uses function components as the basis for rendering.

*What exactly do you mean by that? Hexdocs says, “a function component is any function that receives an assigns map as an argument and returns a rendered struct built with the ~H sigil:”

Basically, everything related to the view is wrapped in an H sigil now. That means that instead of having to build a table like render("table", user: user), you can do it in the pretty function component syntax, including accessing assigns with an @, e.g. <.table rows={@users}>, across all types of views.

Let’s see what this looks like in practice

Start by opening components/layouts.ex:

defmodule HeyGirlWeb.Layouts do 
	use HeyGirlWeb, :html
	
	embed_templates "layouts/**"
end

The first line below the module definition invokes the using macro of the HeyGirlWeb module, and declares we are using the html format, which we defined in our hey_girl_web.ex file. This html definition takes the place of old view definitions.

We then invoke a new bit of functionality called embed_templates that comes from the Phoenix.Component dependency. embed_templates/2 is given a directory where our templates live, and then when we call a function, it will look for a match of that template name minus the format and engine. (We could also pass it an optional second argument with additional options.)

If we take a peek at hey_girl_web.ex, for example, we can see HeyGirlWeb.Layouts referenced in two places:

  • In the live_view definition, where we now set it as a parameter of Phoenix.LiveView and it tells that LiveView to use the app.html.heex as the template
use Phoenix.LiveView,
	layout: {HeyGirlWeb.Layouts, :app}
  • And also in the controller definition, it tells the html layouts to use the same layouts file.
use Phoenix.Controller,
	namespace: HeyGirlWeb,
	formats: [:html, :json],
	layouts: [html: HeyGirlWeb.Layouts]

As you can see, whether you’ve got a controller-based rendering (cough aka dead view cough) or a Liveview-based rendering, they all share the same function components and layouts! This is why we no longer need the live.html.heex file: both types of views now share a unified layout coming from app.html.heex.

Looking more closely at controller-based layouts

Our router defines our home page as the following,
get "/", PageController, :home

In Phoenix 1.6, the controller would have called the view, HeyGirlWeb.PageController.render("home.html", assigns). But now, thanks to Phoenix.Template, PageController looks for a home/1 function component instead.

Take a look at page_controller.ex, where things look very famliiar, but you’ll see a home/1 definition:

defmodule HeyGirlWeb.PageController do
	use HeyGirlWeb, :controller
	
	def home(conn, _params) do
		# The home page is often custom made,
		# so skip the default app layout.
		render(conn, :home, layout: false)
	end
end

The home function component will use the the base layout from app.html.heex because we are using the html def set up in our web interface… that is, unless we set layout: false, as we did in the controller above.

Moving forward, the view module should be named something like, HeyGirlWeb.PageHTML or HeyGirlWeb.PageJSON, depending on your view format, and it should be collocated in the same directory as your controller.

Notice the page_controller.ex and page_html.ex in the directory structure below:

lib/hey_girl_web
β”œβ”€β”€ controllers
β”‚   └── page_html
|	    β”œβ”€β”€ home.html.heex
|   β”œβ”€β”€ page_controller.ex
|   └── page_html.ex

Finally, if we open PageHTML, we can see that we are using an html format, and embed_templates tells the app to look in the page_html directory for the relevant template files.

defmodule HeyGirlWeb.PageHTML do
	use HeyGirlWeb, :html
	
	embed_templates "page_html/*"
end

So the PageController uses the PageHTML view module, which invokes the embed_templates to set where your templates live. Then, in this case, we’re calling home, so it looks in page_html and finds the home.html.heex template file!

Now if we want to add a controller-based “About” page, all we have to do is add an about definition in our page_controller.ex file,

def about(conn, _params) do
	render(conn, :about)
end

Then we can add a route to it in our router (get "/about", PageController, :about), and add a file named about.html.heex in our page_html directory. It will use the default app.html.heex layout, and everything looks great.

Cool, but I ❀️ LiveViews

Same, girl, same. πŸ‘―β€β™€οΈ

In Phoenix LiveView 0.18, the .heex templates are colocated with our .ex files.

Our file structure might look like this,

lib/hey_girl_web
β”œβ”€β”€ live
β”‚   └── about
β”‚       β”œβ”€β”€ index.ex    
|	    └── index.html.heex

And in our if router we have live("/about", HeyGirlWeb.AboutLive.Index, :index), then Phoenix will automatically look for an Index module first before it tries for render/2.

All our liveview lifecycle logic lives in the .ex file, and we keep all the content we might normally have in the render function in the .heex template file.

If we have other modules, such as Show, our file structure might look like the following:

lib/hey_girl_web
β”œβ”€β”€ live
β”‚   └── about
|	    β”œβ”€β”€ index.ex 
|	    β”œβ”€β”€ index.html.heex
|	    β”œβ”€β”€ show.ex
|       └── show.html.heex 

And in our router, we would access the Show module like this:
live("/about/:id", HeyGirlWeb.AboutLive.Show, :show)

One Step Further: Fun with Tabs

Let’s say we had a styleguide with a few tabs on it and we wanted to keep the content of our tabs in a directory together.

First, let’s think about how we might set up the router. Here are a few of the routes we could set up:

live "/styleguide/typography", StyleguideLive, :typography
live "/styleguide/colors", StyleguideLive, :colors
live "/styleguide/buttons", StyleguideLive, :buttons

And this is what our live/ directory looks like:

lib/hey_girl_web
β”œβ”€β”€ live
β”œβ”€β”€ styleguide_live.ex
β”‚   └── styleguide
|	    β”œβ”€β”€ buttons.html.heex
|	    β”œβ”€β”€ colors.html.heex
|       └── typography.html.heex 

Now let’s head into live/styleguide_live.ex. We’re going to do a few things in this file:

  • Set up live/styleguide/ as our template directory for our .heex files
  • Lay out a few “tabs” (which can just be styled links)
    • Note that the links used below are coming from Phoenix.Component’s new link/1 function, which we’ll cover more in detail in Part 3 of this series. For now, just know that this does the same thing as live_patch did before.
  • Create a switch for the live_action assigns value.
    • This value is the third parameter in your live macro in your router, so in live "/styleguide/colors", StyleguideLive, :colors, the @live_action is :colors.
  • In that switch, display the content of the correct template by simply calling the function component by name, for example <.colors /> will display the content of colors.html.heex.
defmodule HeyGirlWeb.Styleguide do 
	use Phoenix.Component 
	embed_template("styleguide/*)
	
	def render(assigns) do
		~H"""
			<.link patch={~p"/styleguide/colors"} replace={true}>
				Colors
			</.link>
			<.link patch={~p"/styleguide/typography"} replace={true}>
				Typography
			</.link>
			<.link patch={~p"/styleguide/buttons"} replace={true}>
				Buttons
			</.link>
			
			<%= case @live_action do %>
				<% :colors -> %>
					<.colors />
				<% :typography -> %>
					<.typography assigns={@assigns} />
				<% :buttons -> %>
					<.buttons />
			<% end %>
			
			</div>
		"""
	end
end

If you have some assigns data you wanted to use in your <.typography /> function component, just pass it along in your function component like the example above. You could also invoke it it like `<%= _typography(assigns) %>.

Sounds like function components are where it’s at! Tell me more.

Now that we’ve got some base pages set up, it’s time to add components. In the next article, I’m going to discuss:

  • Declarative Assigns
  • Building Function Components
  • How to best organize components with Phoenix 1.7
  • Core Components
  • New Phoenix.Component function components such as <.link />

We're building an AI-powered Product Operations Cloud, leveraging AI in almost every aspect of the software delivery lifecycle. Want to test drive it with us? Join the ProdOps party at ProdOps.ai.