Phoenix 1.7.0 final is out!

The final release of Phoenix 1.7 is out! Most of the new features have been outlined in the 1.7 RC thread, but it has been a few months since then and we snuck in some new goodies along the way to final release, as well as discussed a bit about what’s next.

You can check the announcement on the Phoenix site, or read the dup’d content here for convenience:

The final release of Phoenix 1.7 is out! Phoenix 1.7 packs a number of long-awaited new features like verified routes, Tailwind support, LiveView authentication generators, unified HEEx templates, LiveView Streams for optimized collections, and more. This is a backwards compatible release with a few deprecations. Most folks should be able to update just by changing a couple dependencies.

Note: To generate a new 1.7 project, you’ll need to install the phx.new generator from hex:

mix archive.install hex phx_new

Verified Routes

Verified routes replace router helpers with a sigil-based (~p), compile-time verified approach.

note: Verified routes make use of new Elixir 1.14 compiler features. Phoenix still supports older Elixir versions, but you’ll need to update to enjoy the new compile-time verification features.

In practice this means where before you used autogenerated functions like:

  # router
  get "/oauth/callbacks/:id", OAuthCallbackController, :new

  # usage
  MyRouter.Helpers.o_auth_callback_path(conn, :new, "github")
  # => "/oauth/callbacks/github"

  MyRouter.Helpers.o_auth_callback_url(conn, :new, "github")
  # => "http://localhost:4000/oauth/callbacks/github"

You can now do:

  # router
  get "/oauth/callbacks/:id", OAuthCallbackController, :new

  # usage
  ~p"/oauth/callbacks/github"
  # => "/oauth/callbacks/github"

  url(~p"/oauth/callbacks/github")
  # => "http://localhost:4000/oauth/callbacks/github"

This has a number of advantages. There’s no longer guesswork on which function was inflected – is it Helpers.oauth_callback_path or o_auth_callback_path, etc. You also no longer need to include the %Plug.Conn{}, or %Phoenix.Socket{}, or endpoint module everywhere when 99% of the time you know which endpoint configuration should be used.

There is also now a 1:1 mapping between the routes you write in the router, and how you call them with ~p. You simply write it as if you’re hard-coding strings everywhere in your app – except you don’t have maintenance issues that come with hardcoding strings. We can get the best of both worlds with ease of use and maintenance because ~p is a compile-time verified against the routes in your router.

For example, imagine we typo a route:

<.link href={~p"/userz/profile"}>Profile</.link>

The compiler will dispatch all ~p’s at compile-time against your router, and let you know when it can’t find a matching route:

    warning: no route path for AppWeb.Router matches "/postz/#{post}"
      lib/app_web/live/post_live.ex:100: AppWeb.PostLive.render/1

Dynamic “named params” are also simply interpolated like a regular string, instead of arbitrary function arguments:

~p"/posts/#{post.id}"

Additionally, interpolated ~p values are encoded via the Phoenix.Param protocol.
For example, a %Post{} struct in your application may derive the Phoenix.Param
protocol to generate slug-based paths rather than ID based ones. This allows you to
use ~p"/posts/#{post}" rather than ~p"/posts/#{post.slug}" throughout your
application.

Query strings are also supported in verified routes, either in traditional query
string form:

~p"/posts?page=#{page}"

Or as a keyword list or map of values:

params = %{page: 1, direction: "asc"}
~p"/posts?#{params}"

Like path segments, query strings params are proper URL encoded and may be interpolated
directly into the ~p string.

Once you try out the new feature, you won’t be able to go back to router helpers. The new phx.gen.html|live|json|auth generators use verified routes.

Component-based Tailwind generators

Phoenix 1.7 ships with TailwindCSS by default, with no dependency on nodejs on the system. TailwindCSS is the best way I’ve found to style interfaces in my 20 years of web development. Its utility-first approach is far more maintainable and productive than any CSS system or framework I’ve used. It’s collocated approach also aligns perfectly within the function component and LiveView landscape.

The Tailwind team also generously designed the new project landing page, CRUD pages, and authentication system pages for new projects, giving you a first-class and polished starting point for building out your apps.

A new phx.new project will contain a CoreComponents module, housing a core set of UI components like tables, modals, forms, and data lists. The suite of Phoenix generators (phx.gen.html|live|json|auth) make use of the core components. This has a number of neat advantages.

First, you can customize your core UI components to suit whatever needs, designs, and tastes that you have. If you want to use Bulma or Bootstrap instead of Tailwind – no problem! Simply replace the function definitions in core_components.ex with your framework/UI specific implementations and the generators continue to provide a great starting point for new features whether you’re a beginner, or seasoned expert building bespoke product features.

In practice, the generators give you templates that make use of your core components, which look like this:

<.header>
  New Post
  <:subtitle>Use this form to manage post records in your database.</:subtitle>
</.header>

<.simple_form for={@form} action={~p"/posts"}>
  <input field={@form[:title]} type="text" label="Title" />
  <input field={@form[:views]} type="number" label="Views" />

  <:actions>
    <.button>Save Post</.button>
  </:actions>
</.simple_form>

<.back navigate={~p"/posts"}>Back to posts></.back>

We love what the Tailwind team designed for new applications, but we also can’t wait to see the community release their own drop-in replacements for core_components.ex for various frameworks of choice.

Unified function components across Controllers and LiveViews

Function components provided by HEEx, with declarative assigns and slots, are massive step-change in the way we write HTML in Phoenix projects. Function components provide UI building blocks, allowing features to be encapsulated and better extended over the previous template approach in Phoenix.View. You get a more natural way to write dynamic markup, reusable UI that can be extended by the caller, and compile-time features to make writing HTML-based applications a truly first-class experience.

Function components bring a new way to write HTML applications in Phoenix, with new sets of conventions. Additionally, users have struggled with how to marry controller-based Phoenix.View features with Phoenix.LiveView features in their applications. Users found themselves writing render("table", user: user) in controller-based templates, while their LiveViews made use of the new <.table rows={@users}> features. There was no great way to share the approaches in an application.

For these reasons, the Phoenix team unified the HTML rendering approaches whether from a controller request, or a LiveView. This shift also allowed us to revisit conventions and align with the LiveView approach of collocating templates and app code together.

New applications (and the phx generators), remove Phoenix.View as a dependency in favor of a new Phoenix.Template dependency, which uses function components as the basis for all rendering in the framework.

Your controllers still look the same:

defmodule AppWeb.UserController do
  use MyAppWeb, :controller

  def index(conn, _params) do
    users = ...
    render(conn, :index, users: users)
  end
end

But instead of the controller calling AppWeb.UserView.render("index.html", assigns), we’ll now first look for an index/1 function component on the view module, and call that for rendering if it exists. Additionally, we also renamed the inflected view module to look for AppWeb.UserHTML, or AppWeb.UserJSON, and so on for a view-per-format approach for rendering templates. This is all done in backwards compatible way, and is opt-in based on options to use Phoenix.Controller.

All HTML rendering is then based on function components, which can be written directly in a module, or embedded from an external file with the new embed_templates macro provided by Phoenix.Component. Your PageHTML module in a new application looks like this:

defmodule AppWeb.PageHTML do
  use AppWeb, :html

  embed_templates "page_html/*"
end

The new directory structure will look something like this:

lib/app_wb
├── controllers
│   ├── page_controller.ex
│   ├── page_html.ex
│   ├── error_html.ex
│   ├── error_json.ex
│   └── page_html
│       └── home.html.heex
├── live
│   ├── home_live.ex
├── components
│   ├── core_components.ex
│   ├── layouts.ex
│   └── layouts
│       ├── app.html.heex
│       └── root.html.heex
├── endpoint.ex
└── router.ex

Your controllers-based rendering or LiveView-based rendering now all share the same function components and layouts. Whether running phx.gen.html, phx.gen.live, or phx.gen.auth, the new generated templates all make use of your components/core_components.ex definitions.

Additionally, we have collocated the view modules next to their controller files. This brings the same benefits of LiveView collocation – highly coupled files live together. Files that must change together now live together, whether writing LiveView or controller features.

These changes were all about a better way to write HTML-based applications, but they also simplified rendering other formats, like JSON. For example, JSON based view modules follow the same conventions – Phoenix will first look for an index/1 function when rendering the index template, before trying render/2. This allowed us to simplify JSON rendering in general and do away with concepts like Phoenix.View.render_one|render_many.

For example, this is a JSON view generated by phx.gen.json:

defmodule AppWeb.PostJSON do
  alias AppWeb.Blog.Post

  @doc """
  Renders a list of posts.
  """
  def index(%{posts: posts}) do
    %{data: for(post <- posts, do: data(post))}
  end

  @doc """
  Renders a single post.
  """
  def show(%{post: post}) do
    %{data: data(post)}
  end

  defp data(%Post{} = post) do
    %{
      id: post.id,
      title: post.title
    }
  end
end

Notice how it’s all simply regular Elixir functions – as it should be!

These features provide a unified rendering model for applications going forward with a new and improved way to write UIs, but they are a deviation from previous practices. Most large, established applications are probably best served by continuing to depend on Phoenix.View.

LiveView Streams

LiveView now includes a streaming interface for managing large collections in the UI without having to store the collections in memory on the server. With a couple function calls you can insert new items into the UI, append or prepend dynamically, or re-order items without reloading the items on the server.

The phx.gen.live live CRUD generator in Phoenix 1.7 uses streams to manage the your list of items. This allows data entry, updates, and deletes without ever needing to refetch the list of items after the initial load. Let’s see how.

The following PostLive.Index module is generated when you run mix phx.gen.live Blog Post posts title views:integer

defmodule DemoWeb.PostLive.Index do
  use DemoWeb, :live_view

  alias Demo.Blog
  alias Demo.Blog.Post

  @impl true
  def mount(_params, _session, socket) do
    {:ok, stream(socket, :posts, Blog.list_posts())}
  end

  ...
end

Notice how instead of the regular assign(socket, :posts, Blog.list_posts()), we have a new stream/3 interface. This sets up the stream with the initial collection of posts. Then in the generated index.html.heex template, we consume the stream to render the posts table:

<.table
  id="posts"
  rows={@streams.posts}
  row_click={fn {_id, post} -> JS.navigate(~p"/posts/#{post}") end}
>
  <:col :let={{_id, post}} label="Title"><%= post.title %></:col>
  <:col :let={{_id, post}} label="Views"><%= post.views %></:col>
  <:action :let={{_id, post}}>
    <div class="sr-only">
      <.link navigate={~p"/posts/#{post}"}>Show</.link>
    </div>
    <.link patch={~p"/posts/#{post}/edit"}>Edit</.link>
  </:action>
  <:action :let={{id, post}}>
    <.link
      phx-click={JS.push("delete", value: %{id: post.id}) |> hide("##{id}")}
      data-confirm="Are you sure?"
    >
      Delete
    </.link>
  </:action>
</.table>

This looks very similar to the old template, but instead of accessing the bare @posts assign, we pass the @stream.posts to our table. When consuming a stream we also are passed the stream’s DOM id, along with the item.

Back on the server, we can see how simple it is to insert new items into the table. When our generated FormComponent updates or saves a post via the form, we send a message pack to the parent PostLive.Index LiveView about the new or updated post:

PostLive.FormComponent:

defmodule DemoWeb.PostLive.FormComponent do
  ...
  defp save_post(socket, :new, post_params) do
    case Blog.create_post(post_params) do
      {:ok, post} ->
        notify_parent({:saved, post})

        {:noreply,
         socket
         |> put_flash(:info, "Post created successfully")
         |> push_patch(to: socket.assigns.patch)}

      {:error, %Ecto.Changeset{} = changeset} ->
        {:noreply, assign_form(socket, changeset)}
    end
  end

  defp notify_parent(msg), do: send(self(), {__MODULE__, msg})
end

Then we pick the message up in a PostLive.Index handle_info clause:

@impl true
def handle_info({DemoWeb.PostLive.FormComponent, {:saved, post}}, socket) do
  {:noreply, stream_insert(socket, :posts, post)}
end

So the form tells us it saved a post, and we simply stream_insert the post in our stream. That’s it! If a post already exists in the UI, it will be updated in place. Otherwise it is appended to the container by default. You can also prepend with stream_insert(socket, :posts, post, at: 0), or pass any index to :at for arbitrary item insertion or re-ordering.

Streams were one of the final building blocks on our way to LiveView 1.0 and I’m super happy where we landed.

New Form field datastructure

We’re all familiar with the Phoenix.HTML form primitives of <.form for={@changeset}>, where the form takes a datastructure that implements the Phoenix.HTML.FormData protocol and returns a %Phoenix.HTML.Form{}. One issue our approach had is the form datastructure couldn’t track individual form field changes. This made optimizations impossible in LiveView where we’d have to re-render and resend the form on any individual change. With the introduction of Phoenix.HTML.FormData.to_form and Phoenix.Component.to_form, we now have a %Phoenix.HTML.FormField{} datastructue for individual field changes.

The new phx.gen.live generators and your core_components.ex take advantage of these new additions.

What’s Next for Phoenix and LiveView

The Phoenix generators make use of LiveViews latest features, and that will continue to expand. With streaming collections as the default, we can move towards more advanced out-of-the-box features for our live CRUD generators in phx.gen.live. For example, we plan to introduce synced UIs out-of-the-box for resources. The generated Phoenix form features will continue to evolve with the addition of new the to_form interface.

For LiveView, to_form allowed us to ship the basis of optimized forms. Now an individual change to one form field will produce an optimized diff.

Following this optimization work, the major remaining feature for LiveView 1.0 is an expanded form API that better supports dynamic forms inputs, wizard-style forms, and delegating form inputs to child LiveComponents.

Alternative Webserver Support

Thanks to work by Mat Trudel, we now have the basis for first-class webserver support in Plug and Phoenix, allowing other webservers like Bandit to be swapped in Phoenix while enjoying all features like WebSockets, Channels, and LiveView. Stay tuned to the Bandit project if you’re interested in a pure Elixir HTTP server or give it a try in your own Phoenix projects!

What’s Next

As always, step-by-step upgrade guides are there to take your existing 1.6.x apps up to 1.7.

The full changelog can be found here.

Find us on elixir slack or the forums if you have issues.

Happy coding!

–Chris

183 Likes

Whoo hoo congrats :041:

Thank you Chris and everyone who has worked on making it such an awesome release :hugs:

I can’t wait to get stuck in and I reckon we’ll be running a Programming Phoenix LiveView (PragProg) book club soon… if anyone would like to join please PM me (we should also have a few copies to give away). For all you @BartOtten fans, you’ll be pleased to learn he has already agreed to be one of our club leaders :003:

15 Likes

Awesome! Thank you. Don’t remember looking this forward to any framework release before. You folks are awesome!

8 Likes

Congrats and thank you!

5 Likes

This is huge! Thank you Chris and everybody else involved!

5 Likes

I’m not a web-dev-guy, had to do it every once in a while and hated it.
You guys made me love web-dev. I’m really relaxing when I spin my components and liveviews. Now with tailwind I’m even creating good looking stuff (at least the cat says that).

Peace of mind. :innocent:

16 Likes

3 posts were split to a new topic: Is it possible to invoke a route without knowing the full path including scopes in Phoenix 1.7?

Great news :slight_smile: Thanks to everyone involved.

2 Likes

Awesome job!

@chrismccord, does this look like a <=RC.2 syntax here?

4 Likes

Awesome! Thanks for all your hard work everyone!

2 Likes

Awesome!

2 Likes

Thanks to all contributors! You all contributed in making this release wonderful.

A minor step for semver but a major leap for Phoenix devs.

Hear hear! Read read! The book is highly recommended at this forum. Can’t wait to see what is behind it’s beautiful cover.

5 Likes

fixed. Thanks!

3 Likes

This is great news, already updating!

Any tips for converting routes from the non-verified format to the ~p verified routes sigil? I have about 700 route calls in my code-base to convert :grimacing:

Update: I see this note about updating routes under “Optional updates” in Upgrading from Phoenix v1.6.x to v1.7.0

While these changes are a big step forward for new applications, it probably makes sense to maintain your current application conventions and skip these steps for established, large 1.6 applications.

4 Likes

I never thought I’d get this excited about streams. I don’t even like to fish…

5 Likes

and in example/generators:

Blog.list_posts/0 calls Repo.all/1 which then fetches all records, right? The stream/3 even calls for on said collection (to add generated dom_id and -1 index to a new Tuple). Where we win here when talking about stream?

From what I can see using Repo.stream/2 instead of Repo.all/2 would not give us much as per mentioned for call that returns a List, right?

I guess that after rendering said (empty) table the live stream is rendering one row at a time and sends the DOM to JavaScript which is then appended to table element, right?

So let’s say I have 1 million database entries and I want to stream them using Repo.stream/2 only when needed to have infinite scrolling. Can I do so with this new API?

2 Likes

The win for streams will come when pushing updates to LV via PubSub. It is not in the generated code but streams get us closer to that and adding the remaining PubSub bits should be easy.

11 Likes

I can confirm that I was successfully able to modify several items at once, may be not in a very efficient way, within one handle_info callback and they seem to arrive as one update to the client. This is what I did. I need to modify checkbox selections of several items at once:

socket = Enum.reduce(cells, socket, fn c, acc -> stream_insert(acc, :cells, c) end)
{:noreply, socket}

Unfortunately, in my case the payload is around 8K-14K per each round. I’m blaming Heroicons as they get included as inline svg. I show them by “if”, but I also tried “ifing” hidden / block CSS class to a surrounding div and couldn’t achieve any better results yet. Still playing.

Edit: There’s also Edit & Delete links with JS coming for each updated row - another source of a heavy bandwidth. Plus Tailwind class names, as now they are also part of each message. I think this will at some point require attention, I just don’t want to prematurely optimize while sketching the app.

4 Likes

@chrismccord Is there a known show stopper for Phoenix to send the static segments of components only once? Then over the lifespan of a session a user would build ‘a library of components’ and only dynamics have to be send for each component which was sent earlier in the session. Especially useful with rows.

Asking directly as you went to great extend to minimize data over the wire so maybe you have thought of it earlier and know the answer already.

—-

…and next we request to prebuild the component lib for CDN etc etc. One step at the time…

3 Likes

I tried to cover this in the writeup, but even ignoring the PubSub use case, we get huge wins with streams in phx.gen.live compared to the 1.6 approach. We used to push_navigate back to the PostLive.Index LV when you submitted the modal form, which would require refetching the entire listing every time you created or edited a post. Now with streams, we can push_patch back to the index LV and stream_insert the created or updated post. No extra fetching of the listing required.

Our generated context functions have nothing to say about how you’d handle pagination, so this is a little tangential to our generated code. But LV streams could absolutely be used to do “realtime feeds”, especially because you can prepend or append (or insert any any place) on demand. Hope the helps!

12 Likes