You can stop using `form_for` in Phoenix

Phoenix on Rails is a comprehensive guide to Elixir, Phoenix and LiveView for developers who already know Ruby on Rails. If you’re a Rails developer who wants to learn Phoenix fast, click here to learn more!

In older versions of Phoenix, the standard way to render a <form> tag was with the form_for/4 helper from Phoenix.HTML.Form:

<%= form_for @changeset, ~p"/comments", fn f -> %>
  <%= text_input f, :body %>
<% end %>

(The name form_for reminds me of the similarly-named helper in Rails, although form_with is more popular in Rails these days.)

However, since Phoenix LiveView v0.16.0, there’s also the form/1 function component, which does essentially the same thing as form_for/4:

<.form :let={f} for={@changeset} action={~p"/comments"}>
  <.input field={f[:body]} />
</.form>

form/1 was originally defined within Phoenix.LiveView.Helpers, but v0.18.0 moved it to Phoenix.Component.

form_for/4 is still available in Phoenix 1.7[1], and the docs still describe this function as “[t]he entry point for defining forms in Phoenix”. So I was confused as to the difference between form_for/4 and form/1, and when I should prefer one over the other.

One of the many things I love about Elixir and Phoenix is the helpfulness and approachability of their communities. And in this case I managed to get an answer on the Elixir Slack from none other than Phoenix’s creator, Chris McCord:

So that settles it: form_for/4 is deprecated and should no longer be used. I’ve opened a PR to make this clearer in the documentation, and you can stop reading here if that’s all you care about. But there are a few other points about Phoenix 1.7, form/1, and simple_form/1 that are worth clarifying.

Learn Phoenix fast

Phoenix on Rails is a 72-lesson tutorial on web development with Elixir, Phoenix and LiveView, for programmers who already know Ruby on Rails.

Get part 1 for free:

What exactly changed in Phoenix 1.7?

The soft-deprecation of form_for/4 was reflected in a subtle change from Phoenix 1.6 to 1.7.

In a newly-generated Phoenix 1.6 app, your views and components use Phoenix.HTML, as injected by view_helpers/0 in <your_app>_web.ex:

defp view_helpers do
  quote do
    # Use all HTML functionality (forms, tags, etc)
    use Phoenix.HTML

    

This runs the macro Phoenix.HTML.__using__/0, which imports various modules including Phoenix.HTML.Form:

@doc false
defmacro __using__(_) do
  quote do
    import Phoenix.HTML
    import Phoenix.HTML.Form
    import Phoenix.HTML.Link
    import Phoenix.HTML.Tag, except: [attributes_escape: 1]
    import Phoenix.HTML.Format
  end
end

With this imported, you can call form_for(…) directly from within views and templates without needing to use the fully-qualified name Phoenix.HTML.Form.form_for.

In Phoenix 1.7, however, view_helpers/0 changed to html_helpers/0, and we no longer use Phoenix.HTML, we merely import it:

defp html_helpers do
  quote do
    # HTML escaping functionality
    import Phoenix.HTML

    

This imports only those functions that are defined directly within the Phoenix.HTML module. All the other modules like Phoenix.HTML.Form aren’t imported by default anymore - so if you want to keep using form_for, you’ll need to import it yourself:

defmodule YourAppWeb.CommentHTML do
  use YourAppWeb, :html

  import Phoenix.HTML.Form

  def new(assigns) do
    ~H"""
    <%= form_for @changeset, ~p"/comments", fn f -> %>
      <%= text_input f, :body %>
    <% end %>
    """
  end
end

Or you can just write <%= Phoenix.HTML.Form.form_for … %>. But really you shouldn’t do either - just use <.form> 😉.

What does <.form> do anyway?

form/1 isn’t actually that complicated. It takes a map, changeset, or Phoenix.HTML.Form struct - the precise differences between these three use cases are explained clearly in the docs - and outputs a <form> tag.

If you look at the source code, you’ll see that it also outputs up to two hidden inputs:

~H"""
<form {@attrs}>
  <%= if @hidden_method && @hidden_method not in ~w(get post) do %>
    <input name="_method" type="hidden" hidden value={@hidden_method}>
  <% end %>
  <%= if @csrf_token do %>
    <input name="_csrf_token" type="hidden" hidden value={@csrf_token}>
  <% end %>
  <%= render_slot(@inner_block, @form) %>
</form>
"""

The first hidden input is used when the form’s method attribute is something other than GET or POST. Browsers can’t send other types of HTTP request from a <form> submission, so Phoenix fakes it by submitting a POST request with an additional parameter called _method whose value is the method we really want, like "patch" or "put".

The server processes this _method parameter using the module Plug.MethodOverride. It knows to do this because we tell it explicitly: the plug is called within lib/<your_app>_web/endpoint.ex:

defmodule YourAppWeb.Endpoint do
  use Phoenix.Endpoint, otp_app: :your_app

  

  plug Plug.MethodOverride

  

The second hidden input in <.form> protects against a common type of attack called cross-site request forgery (CSRF). Phoenix will reject POST requests that don’t contain a valid CSRF token.

Again, this happens explicitly: the :browser pipeline in the default router includes the protect_against_forgery plug, which is a thin wrapper around Plug.CSRFProtection:

defmodule YourAppWeb.Router do
  use YourAppWeb, :router

  pipeline :browser do
    

    plug :protect_from_forgery

    
  end

  

What about simple_form?

If you generate code in Phoenix 1.7 with commands like mix phx.gen.html, you’ll see a new function component being used called simple_form/1:

<.simple_form :let={f} for={@changeset} action={@action}>
  <.error :if={@changeset.action}>
    Oops, something went wrong! Please check the errors below.
  </.error>
  <.input field={f[:body]} type="text" label="Body" />
  <:actions>
    <.button>Save Comment</.button>
  </:actions>
</.simple_form>

simple_form/1 doesn’t come directly from your dependencies. It’s defined within YourAppWeb.CoreComponents, which is included at lib/<your_app>_web/components/core_components.ex in a newly-generated Phoenix 1.7 app.

CoreComponents.simple_form/1 is a wrapper around <.form> that provides some basic styling. Since this function is part of your app’s own code, you can customise however you see fit:

def simple_form(assigns) do
  ~H"""
  <.form :let={f} for={@for} as={@as} {@rest}>
    <div class="space-y-8 bg-white mt-10">
      <%= render_slot(@inner_block, f) %>
      <div :for={action <- @actions} class="mt-2 flex items-center justify-between gap-6">
        <%= render_slot(action, f) %>
      </div>
    </div>
  </.form>
  """
end

Personally, I don’t find much use for simple_form/1 and prefer to just write <.form> directly. But your mileage may vary.

Photo by Scott Graham on Unsplash.

Learn Phoenix fast

Phoenix on Rails is a 72-lesson tutorial on web development with Elixir, Phoenix and LiveView, for programmers who already know Ruby on Rails.

Get part 1 for free: