Notes on 1.6 to 1.7 upgrade with verified_routes and phoenix_view removal

I recently started on a 1.6 to 1.7 upgrade with all the changes to remove phoenix_view and had some troubles. Despite Jose mentioning that phoenix_view will be maintained, I was already half-way done, so I continued on a complete migration.

Hopefully, this documentation helps someone. Also, the pre-requisite reading is:

Phoenix 1.6 to 1.7 upgrade
Phoenix View - replaced by Phoenix.Component

Main changes

A sample phoenix 1.7 project is required (currently at 1.7.2).

app_web.ex

Several additional functions need to be copied from a new project

  • live_component
  • live_view
  • html
  • html_helpers
  • verified_routes

Need to eventually remove “view”

Copy files / replace from app_web/components/*

  • app_web/components/core_components.ex

  • app_web/controllers/error_html.ex

  • app_web/controllers/error_json.ex

  • Layout replacement. root.html.heex now takes on the function of both templates/layouts/{app.html.heex, live_app.html.heex}

    • app_web/components/layouts.ex (replaces app_web/views/layout_view.ex)
    • app_web/components/layouts/app.html.heex
<%= @inner_content %>
  • app_web/components/layouts/root.html.heex
-     <%= csrf_meta_tag() %>
+     <meta name="csrf-token" content={get_csrf_token()} />

  • router.ex
-  plug :put_layout, {AppWeb.LayoutView, :app}
+  plug :put_root_layout, {AppWeb.Layouts, :root}
  • config/config.ex
 config :app, AppWeb.Endpoint,
-   render_errors: [view: AppWeb.ErrorView, accepts: ~w(html json)]
+   render_errors: [
+    formats: [html: AppWeb.ErrorHTML, json: AppWeb.ErrorJSON],
+   layout: false
+ ]

Non-Liveview

Directory structure changes

app_web/controllers/bla_controller.ex → app_web/controllers/bla_controller.ex
app_web/templates/bla/* → app_web/controllers/bla_html/*
app_web/views/bla_view.ex → app_web/controllers/bla_html.ex

File changes bla_view.ex → bla_html.ex

use AppWeb, :view

def some_helper() do
end

to

use AppWeb, :html
embed_templates "bla_html/*"
# Or to keep directory structure
# embed_templates "../templates/bla/*"

def some_helper() do
end

 @doc """
  NOTE: In migration to Phoenix 1.7.2, needed to keep this function for some generated eex files
  Generates tag for inlined form input errors.
  """
  def error_tag(form, field) do
    Enum.map(Keyword.get_values(form.errors, field), fn error ->
      content_tag(:span, translate_error(error), class: "help-block")
    end)
  end

Moving/Deletion

Can eventually delete

  • lib/app_web/controllers/fallback_controller.ex
  • lib/app_web/views/*

New location could be found for

  • lib/app_web/templates/*

Render changes

Have to be very explicit in templates now as there is no way to dynamically choose a function that I can see so far. The functions here are actually template names as found by the directive embed_templates/* in the controllers/bla_html.ex

-         <%= render "#{@page_name}.html", email: @email, action: Routes.session_path(@conn, @page_action), changeset: @changeset %>
+     <%=  case @page_name do
+          "landing" -> landing(assigns)
+          "login_form" -> login_form(assigns)
+          "register_form" -> register_form(assigns)
+          "forgot_form" -> forgot_form(assigns)
+        end
+      %>

LiveView changes

Directory structure changes

Perhaps I’m doing it wrong, but this is what I had to do.

  • app_web/live/first/ - the directory
  • app_web/live/first/main.ex - the live_view / live_component
- use Phoenix.LiveComponent
+ use AppWeb, :live_component
+ import AppWeb.First.Helpers # these were previously defined in AppWeb.FirstView
+ embed_templates "templates/*"

- def render(assigns) do
    AppWeb.FirstView.render("main.html", assigns)
  end
  • app_web/live/first/helpers.ex - whatever functions that were previously in FirstView
+ use AppWeb, :html
  • app_web/live/first/main.html.heex - the file that gets directly rendered on component/view load. Any sub-templates need to be rendered from the templates/* directory. The render changes to a function call. If there is any conditional rendering, there needs to be a case statement with the function calls.
-  <%= render("file.html", target: @myself) %>
+ <.file target={@myself}

What if you want to keep the render function in your liveview?

This is needed for conditional rendering of templates. In that case, move the main.html.heex into the whatever templates directory you have defined in embed_templates/* and it changes from

 def render(assigns) do
    cond do
      assigns.print == true  ->
        AppWeb.FirstView.render("print.html", assigns)
      !is_nil(assigns.alt_layout ) >
        AppWeb.FirstView.render("layout#{assigns.alt_layout}.html", assigns)
      true ->
        AppWeb.FirstView.render("main.html", assigns)
     end
  end

  def render(assigns) do
    cond do
      assigns.print == true  ->
        ~H"<%= print(assigns) %>"

     !is_nil(assigns.alt_layout)  ->
        current_layout = assigns.alt_layout
        case current_layout do
          "1" -> ~H"<%= layout1(assigns) %>"
          "2" -> ~H"<%= layout2(assigns) %>"
        end

      true ->
        ~H"<%= main(assigns) %>"
     end
  end

Conclusion

In the end, the migration wasn’t hard, just time consuming. I had an organic sprawl of 30 templates and 20 liveview components in templates and live and had to touch around 70 files total and took the opportunity to namespace everything better.

I’m solo right now, but in the future if someone had to touch the code and look at the phoenix documentation, they would have been very confused.

Regardless, I prefer this new setup without phoenix_view as there seems like there is a bit less magic. Verified routes is also a nice incremental improvement. Also, perhaps this is a pipedream, but I’d like to keep this application running for the next 10 years (preferably 50) without too many major migrations.

7 Likes

Great job with the write up!

Swapping the cond block with pattern matching in function heads could be a nice approach.

def render(%{print: true} = assigns), do: ~H"<%= print(assigns) %>"
def render(%{alt_layout: "1"} = assigns), do: ~H"<%= layout1(assigns) %>"
def render(%{alt_layout: "2"} = assigns), do: ~H"<%= layout2(assigns) %>"
def render(assigns), do: ~H"<%= main(assigns) %>"

Or if sticking with one function head, collapsing the case nested in cond with one case statement.

def render(assigns) do
  case assigns do
    %{print: true} -> ~H"<%= print(assigns) %>"
    %{alt_layout: "1"} -> ~H"<%= layout1(assigns) %>"
    %{alt_layout: "2"} -> ~H"<%= layout2(assigns) %>"
    _ -> ~H"<%= main(assigns) %>"
  end
end
1 Like

Really nice notes! I wish I had seen this thread a few days ago.

Thank you for this resource, @tj0!