Byron Salty
Posted on December 29, 2023
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
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 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.
Posted on December 29, 2023
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.