Upgrading to Phoenix 1.7

byronsalty

Byron Salty

Posted on December 29, 2023

Upgrading to Phoenix 1.7

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 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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

(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},
    ...
Enter fullscreen mode Exit fullscreen mode

Run:

mix deps.clean --all
rm mix.lock
mix deps.get
mix deps.compile
Enter fullscreen mode Exit fullscreen mode

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 
...
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

3. Create components dir

This is where layouts and standard components will live now.

mkdir lib/<app>_web/components
Enter fullscreen mode Exit fullscreen mode

Move you layouts from the former templates dir:

mv lib/<app>_web/templates/layouts lib/<app>_web/components/.
Enter fullscreen mode Exit fullscreen mode

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/.
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

After that you should be able to get rid of the templates dir:

rm -fr lib/appName_web/templates
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode
  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
Enter fullscreen mode Exit fullscreen mode

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 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
Enter fullscreen mode Exit fullscreen mode

7. Small change to the csrf_token

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

csrf_token_value()
Enter fullscreen mode Exit fullscreen mode

This needs to change to:

get_csrf_token()
Enter fullscreen mode Exit fullscreen mode

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}
Enter fullscreen mode Exit fullscreen mode

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"}
Enter fullscreen mode Exit fullscreen mode
# from
<img src={Routes.static_path(@conn, "/images/logo.png")}

# to
<img src={~p"/images/logo.png")}
Enter fullscreen mode Exit fullscreen mode
# from
Routes.post_path(@conn, :index)

# to
~p"/posts"
Enter fullscreen mode Exit fullscreen mode
# from
Routes.post_path(@conn, :update, @post)

# to
~p"/admin/prompts/#{@prompt}"
Enter fullscreen mode Exit fullscreen mode

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>

Enter fullscreen mode Exit fullscreen mode
# from:
<%= render 'form.html', ... %>

# to:
<.post_form changeset={@changeset} action={~p"/posts/update/#{@post}"} />
Enter fullscreen mode Exit fullscreen mode
# from:
<%= link "Edit", to: Routes.page_path(@conn, :edit, @page) %>

# to:
<.link navigate={~p"/pages/#{@page}"}>Edit</.link>
Enter fullscreen mode Exit fullscreen mode
# 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>
Enter fullscreen mode Exit fullscreen mode
# from:
  <%= label f, :name %>
  <%= text_input f, :name %>
  <%= error_tag f, :name %>

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

Enter fullscreen mode Exit fullscreen mode
# from:
  <%= inputs_for f, :children, fn c -> %>

    <%= label c, :baby_name %>
    <%= text_input c, :baby_name %>
    <%= error_tag c, :baby_name %>

# to:
Enter fullscreen mode Exit fullscreen mode
# 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>
Enter fullscreen mode Exit fullscreen mode
# from:
  <div class="buttons">
    <%= submit "Save" %>
  </div>

# to:
  <:actions>
    <.button>Save</.button>
  </:actions>
Enter fullscreen mode Exit fullscreen mode

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"
Enter fullscreen mode Exit fullscreen mode

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' {} +
Enter fullscreen mode Exit fullscreen mode

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

Happy Coding.

💖 💪 🙅 🚩
byronsalty
Byron Salty

Posted on December 29, 2023

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related

Upgrading to Phoenix 1.7
elixir Upgrading to Phoenix 1.7

December 29, 2023