You can stop using `form_for` in Phoenix

UPDATE: As of Phoenix v1.7.11, a newly-generated Phoenix app depends on v4.0 of phoenix_html, which no longer defines form_for/4 at all. That function, along with other HTML tag helpers like link/2, img_tag/2, etc., has been extracted to an entirely new package called phoenix_html_helpers, which you can use if you still need backwards compatibility with those old-style Phoenix helpers. For new code, use <.form>.

The below article was written when Phoenix still depended by default on phoenix_html v3.3. While its information about form_for/4 is now out of date, it still has some explanations of <.form> and <.simple_form> that may be useful if you’re learning Phoenix.


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 LiveView in 15 lessons

Sign up to my newsletter to get instant access to the LiveView and OTP crash course, the FREE tutorial that’ll teach you Phoenix LiveView and OTP in 15 easy lessons.

Your privacy is my top priority. Your email address will never be shared with third parties.

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 LiveView in 15 lessons

Sign up to my newsletter to get instant access to the LiveView and OTP crash course, the FREE tutorial that’ll teach you Phoenix LiveView and OTP in 15 easy lessons.

Your privacy is my top priority. Your email address will never be shared with third parties.