Upgrading to Phoenix 1.7
Tags: elixir, phoenix, phoenix17
Published: 2023-12-29 05:20:27

I consider this a work in progress. This is a guide that I'll use myself as I migrate all my projects to 1.7 so it will refine over time and please submit comments / suggestions for improvements.

# The Problem

I have several older projects that are in the Phoenix 1.6 style and structure, and lots of newer ones in 1.7. In order to share code and solutions between them I need to move them all to 1.7 and do it by making the 1.6 look exactly like a project started on 1.7.

Therefore, I'm moving things and removing things resulting in changes that may not be strictly necessary if you are just trying to get 1.7 to run.

Some other [good tutorials exist](https://elixircasts.io/upgrading-to-phoenix-1.7) that can be helpful to just make a project work, for example by adding Views back into the project via an optional dependency.

---

# Steps

---

## 0. Create a branch in git

 
*Disclaimer*

In order to make the app look like a true 1.7 app, we need to move, edit and delete a lot of files. 

Start with creating a branch in git in case you ever need to revert or reference the 1.6 code:

```
git checkout -b phoenix_1_6
git checkout -b upgrading_to_1_7
```

I also suggest that you keep a separate clone of the old version, or at least grab the list of generated routes for use later when you convert to path sigils:
```
mix phx.routes > routes.txt
```

(I'm saying store it because once you start making these changes your project may not compile again until all the issues are fixed. In my latest conversion, there were over 800 issues to be fixed to get the project to compile again.)


---

## 1. Update dependencies

List of deps that I changed:
```
defp deps do
 [
	{:phoenix, "~> 1.7.10"},
	{:phoenix_html, "~> 3.3"},
	{:phoenix_live_reload, "~> 1.2", only: [:dev]},
	{:phoenix_live_view, "~> 0.19"},
	{:phoenix_live_dashboard, "~> 0.8.0"},
	{:esbuild, "~> 0.7", runtime: Mix.env() == :dev},
	...
```

Run:
```
mix deps.clean --all
rm mix.lock
mix deps.get
mix deps.compile
```

---

## 2. Fix issues related to Views


Phoenix 1.7 removed the view files so things need to be updated. You'll see errors like:
```
error: module Phoenix.View is not loaded and could not be found. This may be happening because the module you are trying to load directly or indirectly depends on the current module 
...
```

Instead, Phoenix 1.7 uses `html_template` embedding modules. (See Step 5)

Take note of any functionality that you may have added to your `*_view.ex` modules. This will likely move into your `*_html.ex` files that we'll create in Step 5.

I suggest keeping those `View` modules for now, but remove the `use <App>Web, :view` so that it won't throw compilation errors. 


But if you only have boilerplate code in `/views` then you can safely remove those files and that directory:
```
# NOTE: Only if you don't need to keep any of those files
#   Otherwise run this after Step 5.
rm -fr lib/appName_web/views
```

---

## 3. Create components dir


This is where layouts and standard components will live now.

```
mkdir lib/<app>_web/components
```

Move you layouts from the former `templates` dir:
```
mv lib/<app>_web/templates/layouts lib/<app>_web/components/.
```

*Note*: If you have a `live.html.heex` layout, you should be able to remove it. One of the major benefits of 1.7 is that the layouts are shared across live and dead views.

---

## 4. Move template dirs


Instead of being in a separate `templates` directory, the remaining templates folders with the `*.html.heex` files move into the `controllers` dir as subfolders:
```
mv lib/appName_web/templates/* lib/appName_web/controllers/.
```

*Note:* I then manually renamed them to include the standard `_html` suffix.

```
cd lib/appName_web/controllers
mv page page_html
mv post post_html
```

After that you should be able to get rid of the templates dir:
```
rm -fr lib/appName_web/templates
```

---

## 5. Add the html embedding modules


Let's say you still have a PageController. You'll want to add a companion file next to the `page_controller.ex` conventionally called `page_html.ex` which looks like this by default:

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

You will probably get an error that `:html` is an unknown function in your app's web module. You'll need to find your base module at `lib/appName_web/appName_web.ex` and change a few functions. 

Add: `html`, `html_helpers`, `verified_routes`, and `static_paths`:

```
def html do
	quote do
		use Phoenix.Component
		# Import convenience functions from controllers

		import Phoenix.Controller,
			only: [get_csrf_token: 0, view_module: 1, view_template: 1]

		# Include general helpers for rendering HTML
		unquote(html_helpers())
	end
end

defp html_helpers do
	quote do
		# HTML escaping functionality
		import Phoenix.HTML
		
		# Core UI components and translation
		import AppNameWeb.CoreComponents
		import AppNameWeb.Gettext

		# Shortcut for generating JS commands
		alias Phoenix.LiveView.JS

		# Routes generation with the ~p sigil
		unquote(verified_routes())
	end
end

def verified_routes do
	quote do
		use Phoenix.VerifiedRoutes,
			endpoint: AppNameWeb.Endpoint,
			router: AppNameWeb.Router,
			statics: AppNameWeb.static_paths()
	end
end

def static_paths, do: ~w(assets fonts images favicon.ico robots.txt)
```

Remove `component`, `view` and `view_helpers`

Update references to `view_helpers` to `html_helpers`

Also - update the `controller`,`live_view` and `live_component` function:
```
def controller do
	quote do
		use Phoenix.Controller,
			formats: [:html, :json],
			layouts: [html: AppNameWeb.Layouts]

		import Plug.Conn
		import AppNameWeb.Gettext

		unquote(verified_routes())
	end
end
```

```
  def live_view do
    quote do
      use Phoenix.LiveView,
        layout: {AppNameWeb.Layouts, :app}

      unquote(html_helpers())
    end
  end

  def live_component do
    quote do
      use Phoenix.LiveComponent

      unquote(html_helpers())
    end
  end
```

---

## 6. Add standard files

### Add some components

Grab the `core_components.ex` and `layouts.ex` from any 1.7 projects or clone [this example project](https://github.com/byronsalty/app_name) and put it into `lib/appName_web/components`

### Add Gettext

If you don't have GetText in the base of your app then grab that too from `lib/appName_web/gettext.ex`

### Make sure you updated "appName"

Make sure this doesn't find anything:
```
grep -ri lib appname
```


---

## 7. Small change to the csrf_token


Your `root.html.heex` layout probably has a function call like:

```
csrf_token_value()
```

This needs to change to:
```
get_csrf_token()
```

---

## 8. Update router


The `:browser` pipeline needs to reference the layouts with the new module name. Change:
```
#from
plug :put_root_layout, {AppNameWeb.LayoutView, :root}

#to
plug :put_root_layout, html: {AppNameWeb.Layouts, :root}
```

---

## 9. Update Routes to use `~p` sigil


Now you can use the much more convenient `~p` sigil.

Look in all of your html for references to `Routes.` and change them similar to the following examples:


```
# from:
href={Routes.static_path(@conn, "/assets/app.css")}

# to:
href={~p"/assets/app.css"}
```

```
# from
<img src={Routes.static_path(@conn, "/images/logo.png")}

# to
<img src={~p"/images/logo.png")}
```

```
# from
Routes.post_path(@conn, :index)

# to
~p"/posts"
```

```
# from
Routes.post_path(@conn, :update, @post)

# to
~p"/admin/prompts/#{@prompt}"
```



---

## 10. Update heex to use components


Instead of embedding elixir code into templates with `<%= ... %>`, convert these snippets to components where appropriate.

Some example replacements:
```
# from:
<%= live_title_tag assigns[:page_title] || "App" %>

# to:
<.live_title suffix="">
	<%= assigns[:page_title] || "App" %>
</.live_title>

```

```
# from:
<%= render 'form.html', ... %>

# to:
<.post_form changeset={@changeset} action={~p"/posts/update/#{@post}"} />
```

```
# from:
<%= link "Edit", to: Routes.page_path(@conn, :edit, @page) %>

# to:
<.link navigate={~p"/pages/#{@page}"}>Edit</.link>
```

```
# from:
<%= link "Delete", to: Routes.page_path(@conn, :delete, page), method: :delete, data: [confirm: "Are you sure you want to delete #{page.title}?"] %>

# to:
<.link href={~p"/mascots/#{mascot}"} method="delete" data-confirm="Are you sure?">
  Delete
</.link>
```

```
# from:
  <%= label f, :name %>
  <%= text_input f, :name %>
  <%= error_tag f, :name %>

# to:
  <.input field={f[:name]} type="text" label="Name" />

```

```
# from:
  <%= inputs_for f, :children, fn c -> %>
    
    <%= label c, :baby_name %>
    <%= text_input c, :baby_name %>
    <%= error_tag c, :baby_name %>

# to:
```

```
# from:
  <%= if @changeset.action do %>
    <div class="alert alert-danger">
      <p>Oops, something went wrong! Please check the errors below.</p>
    </div>
  <% end %>

# to:
  <.error :if={@changeset.action}>
    Oops, something went wrong! Please check the errors below.
  </.error>
```

```
# from:
  <div class="buttons">
    <%= submit "Save" %>
  </div>

# to:
  <:actions>
    <.button>Save</.button>
  </:actions>
```

TIP: Keep a 1.7 generated app nearby with a some forms etc created with `mix phx.gen.html ...` for comparisons during this phase.

---

## 10B - Check that you didn't miss anything

Here are a few scripts to run against your lib dir to find missed replacements:

```
grep -r lib "<%= link"
```

---

## 11. Update various heex items

The `form` component now needs `:let` instead of `let`.

You could try bulk updating like this:
```
# MacOS
find lib -type f -name "*.heex" -exec sed -i '' 's/form let/form :let/g' {} +

# Linux
find lib -type f -name "*.heex" -exec sed -i 's/form let/form :let/g' {} +
```

---

If this was helpful - give it a Like and Follow!

Happy Coding.