Repository: phoenixframework/phoenix
Pull request #5252 (2023-02-04 09:44:03)

only compress once if asset content and digest are equal
Phoenix compresses all assets twice, even when their contents are the same. While this is unlikely for non trivial CSS/JS assets, it's always the case for all other kinds of assets. The issue is only noticable when using slow compressors and/or a lot of assets.

Technically the compressor might return a different result for the digest and regular version, since it also receives the filename as metadata. However, the behaviour doesn't promise anything in this regard, nor do any of the public implementations on Github use the filename in the actual compression. The specific search used was
https://github.com/search?l=Elixir&q=%22behaviour+Phoenix.Digester.Compressor%22&type=Code -- most of the results are from vendoring Phoenix, there are only a couple of independent implementations.

Below results are for a ~2019 4+4 core laptop CPU using only the standard Phoenix gzip compressor. The files are not an artificial scenario, but auto generated OpenStreetMap vector tiles for which I would like to reuse the Phoenix asset pipeline. All results are on top of https://github.com/phoenixframework/phoenix/pull/5246 .

```
Benchmark 1: few files, double compress
  Time (mean ± σ):      1.467 s ±  0.048 s    [User: 1.990 s, System: 0.407 s]
  Range (min … max):    1.389 s …  1.559 s    20 runs

Benchmark 2: few files, content compare
  Time (mean ± σ):      1.481 s ±  0.081 s    [User: 2.004 s, System: 0.403 s]
  Range (min … max):    1.366 s …  1.615 s    20 runs
```

"many files" are ~150 Mi in ~9000 files:

```
Benchmark 3: many files, double compress
  Time (mean ± σ):     13.614 s ±  0.497 s    [User: 43.400 s, System: 7.573 s]
  Range (min … max):   13.058 s … 14.579 s    7 runs

Benchmark 4: many files, content compare
  Time (mean ± σ):     11.309 s ±  0.372 s    [User: 29.574 s, System: 6.649 s]
  Range (min … max):   10.881 s … 11.963 s    7 runs
```


Patch:
diff --git a/guides/contexts.md b/guides/contexts.md
index 188982138..98529eed0 100644
--- a/guides/contexts.md
+++ b/guides/contexts.md
@@ -514,7 +514,7 @@ With our `category_select` function in place, we can open up `lib/hello_web/cont
 
 ```diff
   ...
-  <.input type="number" field={{f, :views}} label="Views" />
+  <.input type="number" field={f[:views]} label="Views" />
 
 + <%= category_select f, @changeset %>
 
@@ -1299,12 +1299,12 @@ We tweaked the show action to pass our `conn.assigns.current_uuid` to `get_order
 
   <li :for={item <- @order.line_items}>
     <%= item.product.title %>
-    (<%= item.quantity %>) - <%= HelloWeb.CartView.currency_to_str(item.price) %>
+    (<%= item.quantity %>) - <%= HelloWeb.CartHTML.currency_to_str(item.price) %>
   </li>
 
   <li>
     <strong>Total price:</strong>
-    <%= HelloWeb.CartView.currency_to_str(@order.total_price) %>
+    <%= HelloWeb.CartHTML.currency_to_str(@order.total_price) %>
   </li>
 
 </ul>
diff --git a/guides/introduction/packages_glossary.md b/guides/introduction/packages_glossary.md
new file mode 100644
index 000000000..7f6f06b68
--- /dev/null
+++ b/guides/introduction/packages_glossary.md
@@ -0,0 +1,52 @@
+# Packages Glossary
+
+By default, Phoenix applications depend on several packages with different purposes.
+This page is a quick reference of the different packages you may work with as a Phoenix
+developer.
+
+The main packages are:
+
+  * [Ecto](https://hexdocs.pm/ecto) - a language integrated query and
+    database wrapper
+
+  * [Phoenix](https://hexdocs.pm/phoenix) - the Phoenix web framework
+    (these docs)
+
+  * [Phoenix LiveView](https://hexdocs.pm/phoenix_live_view) - build rich,
+    real-time user experiences with server-rendered HTML. The LiveView
+    project also defines [`Phoenix.Component`](https://hexdocs.pm/phoenix_live_view/Phoenix.Component.html) and
+    [the HEEx template engine](https://hexdocs.pm/phoenix_live_view/Phoenix.Component.html#sigil_H/2),
+    used for rendering HTML content in both regular and real-time applications
+
+  * [Plug](https://hexdocs.pm/plug) - specification and conveniences for
+    building composable modules web applications. This is the package
+    responsible for the connection abstraction and the regular request-
+    response life-cycle
+
+You will also work with the following:
+
+  * [ExUnit](https://hexdocs.pm/ex_unit) - Elixir's built-in test framework
+
+  * [Gettext](https://hexdocs.pm/gettext) - internationalization and
+    localization through [`gettext`](https://www.gnu.org/software/gettext/)
+
+  * [Swoosh](https://hexdocs.pm/swoosh) - a library for composing,
+    delivering and testing emails, also used by `mix phx.gen.auth`
+
+When peaking under the covers, you will find those libraries play
+an important role in Phoenix applications:
+
+  * [Phoenix HTML](https://hexdocs.pm/phoenix_html) - building blocks
+    for working with HTML and Forms safely
+
+  * [Phoenix PubSub](https://hexdocs.pm/phoenix_pubsub) - a distributed
+    pub/sub system with presence support
+
+When it comes to instrumentation and monitoring, check out:
+
+  * [Phoenix LiveDashboard](https://hexdocs.pm/phoenix_live_dashboard) -
+    real-time performance monitoring and debugging tools for Phoenix
+    developers
+
+  * [Telemetry Metrics](https://hexdocs.pm/telemetry_metrics) - common
+    interface for defining metrics based on Telemetry events
diff --git a/installer/lib/mix/tasks/phx.new.ex b/installer/lib/mix/tasks/phx.new.ex
index 1bbdbfcf9..82822f580 100644
--- a/installer/lib/mix/tasks/phx.new.ex
+++ b/installer/lib/mix/tasks/phx.new.ex
@@ -173,12 +173,14 @@ defmodule Mix.Tasks.Phx.New do
       if mix_step == [] do
         tasks =
           if Keyword.get(project.opts, :assets, true) do
+            Mix.shell().info([:green, "* running ", :reset, "mix assets.setup"])
+
             # First compile only esbuild and tailwind so we can install in parallel
-            cmd(project, "mix deps.compile castore esbuild tailwind")
+            cmd(project, "mix deps.compile castore esbuild tailwind", false)
 
             Enum.map(
               ["mix do loadpaths --no-compile + tailwind.install", "mix do loadpaths --no-compile + esbuild.install"],
-              &Task.async(fn -> cmd(project, &1) end)
+              &Task.async(fn -> cmd(project, &1, false) end)
             )
           else
             []
@@ -287,8 +289,10 @@ defmodule Mix.Tasks.Phx.New do
 
   ## Helpers
 
-  defp cmd(%Project{} = project, cmd) do
-    Mix.shell().info [:green, "* running ", :reset, cmd]
+  defp cmd(%Project{} = project, cmd, log? \\ true) do
+    if log? do
+      Mix.shell().info [:green, "* running ", :reset, cmd]
+    end
 
     case Mix.shell().cmd(cmd, cmd_opts(project)) do
       0 -> []
diff --git a/installer/templates/phx_web/components/core_components.ex b/installer/templates/phx_web/components/core_components.ex
index ca10a7add..d1ed11e32 100644
--- a/installer/templates/phx_web/components/core_components.ex
+++ b/installer/templates/phx_web/components/core_components.ex
@@ -77,7 +77,7 @@ defmodule <%= @web_namespace %>.CoreComponents do
                   phx-click={hide_modal(@on_cancel, @id)}
                   type="button"
                   class="-m-3 flex-none p-3 opacity-20 hover:opacity-40"
-                  aria-label=<%= if @gettext do %>{gettext("close")}<% else %>"close"<% end %>
+                  aria-label={gettext("close")}
                 >
                   <Heroicons.x_mark solid class="h-5 w-5 stroke-current" />
                 </button>
@@ -166,7 +166,7 @@ defmodule <%= @web_namespace %>.CoreComponents do
         :if={@close}
         type="button"
         class="group absolute top-2 right-1 p-2"
-        aria-label=<%= if @gettext do %>{gettext("close")}<% else %>"close"<% end %>
+        aria-label={gettext("close")}
       >
         <Heroicons.x_mark solid class="h-5 w-5 stroke-current opacity-40 group-hover:opacity-70" />
       </button>
@@ -174,14 +174,41 @@ defmodule <%= @web_namespace %>.CoreComponents do
     """
   end
 
+  @doc """
+  Shows the flash group with standard titles and content.
+
+  ## Examples
+
+      <.flash_group flash={@flash} />
+  """
+  attr :flash, :map, required: true, doc: "the map of flash messages"
+
+  def flash_group(assigns) do
+    ~H"""
+    <.flash kind={:info} title="Success!" flash={@flash} />
+    <.flash kind={:error} title="Error!" flash={@flash} />
+    <.flash
+      id="disconnected"
+      kind={:error}
+      title="We can't find the internet"
+      close={false}
+      autoshow={false}
+      phx-disconnected={show("#disconnected")}
+      phx-connected={hide("#disconnected")}
+    >
+      Attempting to reconnect <Heroicons.arrow_path class="ml-1 w-3 h-3 inline animate-spin" />
+    </.flash>
+    """
+  end
+
   @doc """
   Renders a simple form.
 
   ## Examples
 
-      <.simple_form :let={f} for={:user} phx-change="validate" phx-submit="save">
-        <.input field={{f, :email}} label="Email"/>
-        <.input field={{f, :username}} label="Username" />
+      <.simple_form phx-change="validate" phx-submit="save">
+        <.input field={@form[:email]} label="Email"/>
+        <.input field={@form[:username]} label="Username" />
         <:actions>
           <.button>Save</.button>
         </:actions>
@@ -189,7 +216,6 @@ defmodule <%= @web_namespace %>.CoreComponents do
   """
   attr :for, :any, default: nil, doc: "the datastructure for the form"
   attr :as, :any, default: nil, doc: "the server side parameter to collect all input under"
-
   attr :rest, :global,
     include: ~w(autocomplete name rel action enctype method novalidate target),
     doc: "the arbitrary HTML attributes to apply to the form tag"
@@ -249,10 +275,10 @@ defmodule <%= @web_namespace %>.CoreComponents do
 
   ## Examples
 
-      <.input field={{f, :email}} type="email" />
+      <.input field={@form[:email]} type="email" />
       <.input name="my-input" errors={["oh no!"]} />
   """
-  attr :id, :any
+  attr :id, :any, default: nil
   attr :name, :any
   attr :label, :string, default: nil
 
@@ -262,7 +288,7 @@ defmodule <%= @web_namespace %>.CoreComponents do
                range radio search select tel text textarea time url week)
 
   attr :value, :any
-  attr :field, :any, doc: "a %Phoenix.HTML.Form{}/field name tuple, for example: {f, :email}"
+  attr :field, :any, doc: "a %Phoenix.HTML.Form{}/field name tuple, for example: @form[:email]"
   attr :errors, :list
   attr :checked, :boolean, doc: "the checked flag for checkbox inputs"
   attr :prompt, :string, default: nil, doc: "the prompt for select inputs"
@@ -272,21 +298,17 @@ defmodule <%= @web_namespace %>.CoreComponents do
                                    pattern placeholder readonly required rows size step)
   slot :inner_block
 
-  def input(%{field: {f, field}} = assigns) do
+  def input(%{field: %Phoenix.HTML.FormField{} = field} = assigns) do
     assigns
-    |> assign(field: nil)
-    |> assign_new(:name, fn ->
-      name = Phoenix.HTML.Form.input_name(f, field)
-      if assigns.multiple, do: name <> "[]", else: name
-    end)
-    |> assign_new(:id, fn -> Phoenix.HTML.Form.input_id(f, field) end)
-    |> assign_new(:value, fn -> format_input_value(Phoenix.HTML.Form.input_value(f, field)) end)
-    |> assign_new(:errors, fn -> translate_errors(f.errors || [], field) end)
+    |> assign(field: nil, id: assigns.id || field.id)
+    |> assign_new(:name, fn -> if assigns.multiple, do: field.name <> "[]", else: field.name end)
+    |> assign_new(:value, fn -> Phoenix.HTML.Form.normalize_value(assigns.type, field.value) end)
+    |> assign_new(:errors, fn -> Enum.map(field.errors, &translate_error(&1)) end)
     |> input()
   end
 
   def input(%{type: "checkbox"} = assigns) do
-    assigns = assign_new(assigns, :checked, fn -> input_equals?(assigns.value, "true") end)
+    assigns = assign_new(assigns, :checked, fn -> assigns.value == "true" end)
 
     ~H"""
     <label phx-feedback-for={@name} class="flex items-center gap-4 text-sm leading-6 text-zinc-600">
@@ -332,14 +354,14 @@ defmodule <%= @web_namespace %>.CoreComponents do
         id={@id || @name}
         name={@name}
         class={[
-          input_border(@errors),
           "mt-2 block min-h-[6rem] w-full rounded-lg border-zinc-300 py-[7px] px-[11px]",
           "text-zinc-900 focus:border-zinc-400 focus:outline-none focus:ring-4 focus:ring-zinc-800/5 sm:text-sm sm:leading-6",
-          "phx-no-feedback:border-zinc-300 phx-no-feedback:focus:border-zinc-400 phx-no-feedback:focus:ring-zinc-800/5"
+          "phx-no-feedback:border-zinc-300 phx-no-feedback:focus:border-zinc-400 phx-no-feedback:focus:ring-zinc-800/5",
+          "border-zinc-300 focus:border-zinc-400 focus:ring-zinc-800/5",
+          @errors != [] && "border-rose-400 focus:border-rose-400 focus:ring-rose-400/10"
         ]}
         {@rest}
-      >
-    <%%= @value %></textarea>
+      ><%%= @value %></textarea>
       <.error :for={msg <- @errors}><%%= msg %></.error>
     </div>
     """
@@ -355,10 +377,11 @@ defmodule <%= @web_namespace %>.CoreComponents do
         id={@id || @name}
         value={@value}
         class={[
-          input_border(@errors),
           "mt-2 block w-full rounded-lg border-zinc-300 py-[7px] px-[11px]",
           "text-zinc-900 focus:outline-none focus:ring-4 sm:text-sm sm:leading-6",
-          "phx-no-feedback:border-zinc-300 phx-no-feedback:focus:border-zinc-400 phx-no-feedback:focus:ring-zinc-800/5"
+          "phx-no-feedback:border-zinc-300 phx-no-feedback:focus:border-zinc-400 phx-no-feedback:focus:ring-zinc-800/5",
+          "border-zinc-300 focus:border-zinc-400 focus:ring-zinc-800/5",
+          @errors != [] && "border-rose-400 focus:border-rose-400 focus:ring-rose-400/10"
         ]}
         {@rest}
       />
@@ -367,12 +390,6 @@ defmodule <%= @web_namespace %>.CoreComponents do
     """
   end
 
-  defp input_border([] = _errors),
-    do: "border-zinc-300 focus:border-zinc-400 focus:ring-zinc-800/5"
-
-  defp input_border([_ | _] = _errors),
-    do: "border-rose-400 focus:border-rose-400 focus:ring-rose-400/10"
-
   @doc """
   Renders a label.
   """
@@ -453,13 +470,14 @@ defmodule <%= @web_namespace %>.CoreComponents do
         <thead class="text-left text-[0.8125rem] leading-6 text-zinc-500">
           <tr>
             <th :for={col <- @col} class="p-0 pb-4 pr-6 font-normal"><%%= col[:label] %></th>
-            <th class="relative p-0 pb-4"><span class="sr-only"><%= if @gettext do %><%%= gettext("Actions") %><% else %>Actions<% end %></span></th>
+            <th class="relative p-0 pb-4"><span class="sr-only"><%%= gettext("Actions") %></span></th>
           </tr>
         </thead>
         <tbody class="relative divide-y divide-zinc-100 border-t border-zinc-200 text-sm leading-6 text-zinc-700">
           <tr
             :for={row <- @rows}
             id={"#{@id}-#{Phoenix.Param.to_param(row)}"}
+            badclass="relative group hover:bg-zinc-50"
             class="group hover:bg-zinc-50"
           >
             <td
@@ -638,14 +656,4 @@ defmodule <%= @web_namespace %>.CoreComponents do
   def translate_errors(errors, field) when is_list(errors) do
     for {^field, {msg, opts}} <- errors, do: translate_error({msg, opts})
   end
-
-  defp input_equals?(val1, val2) do
-    Phoenix.HTML.html_escape(val1) == Phoenix.HTML.html_escape(val2)
-  end
-
-  defp format_input_value(%struct{} = value) when struct in [NaiveDateTime, DateTime] do
-    Calendar.strftime(value, "%Y-%m-%dT%H:%M")
-  end
-
-  defp format_input_value(other), do: other
 end
diff --git a/installer/templates/phx_web/components/layouts/app.html.heex b/installer/templates/phx_web/components/layouts/app.html.heex
index 7b16345d9..e339995a0 100644
--- a/installer/templates/phx_web/components/layouts/app.html.heex
+++ b/installer/templates/phx_web/components/layouts/app.html.heex
@@ -37,19 +37,7 @@
 </header>
 <main class="px-4 py-20 sm:px-6 lg:px-8">
   <div class="mx-auto max-w-2xl">
-    <.flash kind={:info} title="Success!" flash={@flash} />
-    <.flash kind={:error} title="Error!" flash={@flash} />
-    <.flash
-      id="disconnected"
-      kind={:error}
-      title="We can't find the internet"
-      close={false}
-      autoshow={false}
-      phx-disconnected={show("#disconnected")}
-      phx-connected={hide("#disconnected")}
-    >
-      Attempting to reconnect <Heroicons.arrow_path class="ml-1 w-3 h-3 inline animate-spin" />
-    </.flash>
+    <.flash_group flash={@flash} />
     <%%= @inner_content %>
   </div>
 </main>
diff --git a/installer/templates/phx_web/controllers/page_html/home.html.heex b/installer/templates/phx_web/controllers/page_html/home.html.heex
index d729a381a..6b1be15a7 100644
--- a/installer/templates/phx_web/controllers/page_html/home.html.heex
+++ b/installer/templates/phx_web/controllers/page_html/home.html.heex
@@ -1,3 +1,4 @@
+<.flash_group flash={@flash} />
 <div class="fixed inset-y-0 right-0 left-[40rem] hidden lg:block xl:left-[50rem]">
   <svg
     viewBox="0 0 1480 957"
diff --git a/lib/mix/tasks/phx.gen.html.ex b/lib/mix/tasks/phx.gen.html.ex
index 28a329c8e..db2b16068 100644
--- a/lib/mix/tasks/phx.gen.html.ex
+++ b/lib/mix/tasks/phx.gen.html.ex
@@ -172,36 +172,36 @@ defmodule Mix.Tasks.Phx.Gen.Html do
   def inputs(%Schema{} = schema) do
     Enum.map(schema.attrs, fn
       {key, :integer} ->
-        ~s(<.input field={{f, #{inspect(key)}}} type="number" label="#{label(key)}" />)
+        ~s(<.input field={f[#{inspect(key)}]} type="number" label="#{label(key)}" />)
 
       {key, :float} ->
-        ~s(<.input field={{f, #{inspect(key)}}} type="number" label="#{label(key)}" step="any" />)
+        ~s(<.input field={f[#{inspect(key)}]} type="number" label="#{label(key)}" step="any" />)
 
       {key, :decimal} ->
-        ~s(<.input field={{f, #{inspect(key)}}} type="number" label="#{label(key)}" step="any" />)
+        ~s(<.input field={f[#{inspect(key)}]} type="number" label="#{label(key)}" step="any" />)
 
       {key, :boolean} ->
-        ~s(<.input field={{f, #{inspect(key)}}} type="checkbox" label="#{label(key)}" />)
+        ~s(<.input field={f[#{inspect(key)}]} type="checkbox" label="#{label(key)}" />)
 
       {key, :text} ->
-        ~s(<.input field={{f, #{inspect(key)}}} type="text" label="#{label(key)}" />)
+        ~s(<.input field={f[#{inspect(key)}]} type="text" label="#{label(key)}" />)
 
       {key, :date} ->
-        ~s(<.input field={{f, #{inspect(key)}}} type="date" label="#{label(key)}" />)
+        ~s(<.input field={f[#{inspect(key)}]} type="date" label="#{label(key)}" />)
 
       {key, :time} ->
-        ~s(<.input field={{f, #{inspect(key)}}} type="time" label="#{label(key)}" />)
+        ~s(<.input field={f[#{inspect(key)}]} type="time" label="#{label(key)}" />)
 
       {key, :utc_datetime} ->
-        ~s(<.input field={{f, #{inspect(key)}}} type="datetime-local" label="#{label(key)}" />)
+        ~s(<.input field={f[#{inspect(key)}]} type="datetime-local" label="#{label(key)}" />)
 
       {key, :naive_datetime} ->
-        ~s(<.input field={{f, #{inspect(key)}}} type="datetime-local" label="#{label(key)}" />)
+        ~s(<.input field={f[#{inspect(key)}]} type="datetime-local" label="#{label(key)}" />)
 
       {key, {:array, _} = type} ->
         ~s"""
         <.input
-          field={{f, #{inspect(key)}}}
+          field={f[#{inspect(key)}]}
           type="select"
           multiple
           label="#{label(key)}"
@@ -212,7 +212,7 @@ defmodule Mix.Tasks.Phx.Gen.Html do
       {key, {:enum, _}} ->
         ~s"""
         <.input
-          field={{f, #{inspect(key)}}}
+          field={f[#{inspect(key)}]}
           type="select"
           label="#{label(key)}"
           prompt="Choose a value"
@@ -221,7 +221,7 @@ defmodule Mix.Tasks.Phx.Gen.Html do
         """
 
       {key, _} ->
-        ~s(<.input field={{f, #{inspect(key)}}} type="text" label="#{label(key)}" />)
+        ~s(<.input field={f[#{inspect(key)}]} type="text" label="#{label(key)}" />)
     end)
   end
 
diff --git a/lib/mix/tasks/phx.gen.live.ex b/lib/mix/tasks/phx.gen.live.ex
index 7806469ec..d5d5de772 100644
--- a/lib/mix/tasks/phx.gen.live.ex
+++ b/lib/mix/tasks/phx.gen.live.ex
@@ -92,7 +92,7 @@ defmodule Mix.Tasks.Phx.Gen.Live do
   """
   use Mix.Task
 
-  alias Mix.Phoenix.{Context}
+  alias Mix.Phoenix.{Context, Schema}
   alias Mix.Tasks.Phx.Gen
 
   @doc false
@@ -106,7 +106,7 @@ defmodule Mix.Tasks.Phx.Gen.Live do
     {context, schema} = Gen.Context.build(args)
     Gen.Context.prompt_for_code_injection(context)
 
-    binding = [context: context, schema: schema, inputs: Gen.Html.inputs(schema)]
+    binding = [context: context, schema: schema, inputs: inputs(schema)]
     paths = Mix.Phoenix.generator_paths()
 
     prompt_for_conflicts(context)
@@ -147,7 +147,8 @@ defmodule Mix.Tasks.Phx.Gen.Live do
       {:eex, "index.html.heex", Path.join(web_live, "index.html.heex")},
       {:eex, "show.html.heex", Path.join(web_live, "show.html.heex")},
       {:eex, "live_test.exs", Path.join(test_live, "#{schema.singular}_live_test.exs")},
-      {:new_eex, "core_components.ex", Path.join([web_prefix, "components", "core_components.ex"])}
+      {:new_eex, "core_components.ex",
+       Path.join([web_prefix, "components", "core_components.ex"])}
     ]
   end
 
@@ -263,4 +264,74 @@ defmodule Mix.Tasks.Phx.Gen.Live do
       ~s|live "/#{schema.plural}/:id/show/edit", #{inspect(schema.alias)}Live.Show, :edit|
     ]
   end
+
+  @doc false
+  def inputs(%Schema{} = schema) do
+    Enum.map(schema.attrs, fn
+      {_, {:references, _}} ->
+        nil
+
+      {key, :integer} ->
+        ~s(<.input field={@form[#{inspect(key)}]} type="number" label="#{label(key)}" />)
+
+      {key, :float} ->
+        ~s(<.input field={@form[#{inspect(key)}]} type="number" label="#{label(key)}" step="any" />)
+
+      {key, :decimal} ->
+        ~s(<.input field={@form[#{inspect(key)}]} type="number" label="#{label(key)}" step="any" />)
+
+      {key, :boolean} ->
+        ~s(<.input field={@form[#{inspect(key)}]} type="checkbox" label="#{label(key)}" />)
+
+      {key, :text} ->
+        ~s(<.input field={@form[#{inspect(key)}]} type="text" label="#{label(key)}" />)
+
+      {key, :date} ->
+        ~s(<.input field={@form[#{inspect(key)}]} type="date" label="#{label(key)}" />)
+
+      {key, :time} ->
+        ~s(<.input field={@form[#{inspect(key)}]} type="time" label="#{label(key)}" />)
+
+      {key, :utc_datetime} ->
+        ~s(<.input field={@form[#{inspect(key)}]} type="datetime-local" label="#{label(key)}" />)
+
+      {key, :naive_datetime} ->
+        ~s(<.input field={@form[#{inspect(key)}]} type="datetime-local" label="#{label(key)}" />)
+
+      {key, {:array, _} = type} ->
+        ~s"""
+        <.input
+          field={@form[#{inspect(key)}]}
+          type="select"
+          multiple
+          label="#{label(key)}"
+          options={#{inspect(default_options(type))}}
+        />
+        """
+
+      {key, {:enum, _}} ->
+        ~s"""
+        <.input
+          field={@form[#{inspect(key)}]}
+          type="select"
+          label="#{label(key)}"
+          prompt="Choose a value"
+          options={Ecto.Enum.values(#{inspect(schema.module)}, #{inspect(key)})}
+        />
+        """
+
+      {key, _} ->
+        ~s(<.input field={@form[#{inspect(key)}]} type="text" label="#{label(key)}" />)
+    end)
+  end
+
+  defp default_options({:array, :string}),
+    do: Enum.map([1, 2], &{"Option #{&1}", "option#{&1}"})
+
+  defp default_options({:array, :integer}),
+    do: Enum.map([1, 2], &{"#{&1}", &1})
+
+  defp default_options({:array, _}), do: []
+
+  defp label(key), do: Phoenix.Naming.humanize(to_string(key))
 end
diff --git a/lib/phoenix.ex b/lib/phoenix.ex
index 112ef52af..9d13e28cd 100644
--- a/lib/phoenix.ex
+++ b/lib/phoenix.ex
@@ -3,45 +3,6 @@ defmodule Phoenix do
   This is the documentation for the Phoenix project.
 
   To get started, see our [overview guides](overview.html).
-  
-  By default, Phoenix applications depend on the following packages
-  across these categories.
-
-  ## General
-
-    * [Ecto](https://hexdocs.pm/ecto) - a language integrated query and
-      database wrapper
-
-    * [ExUnit](https://hexdocs.pm/ex_unit) - Elixir's built-in test framework
-
-    * [Gettext](https://hexdocs.pm/gettext) - Internationalization and
-      localization through [`gettext`](https://www.gnu.org/software/gettext/)
-
-    * [Phoenix](https://hexdocs.pm/phoenix) - the Phoenix web framework
-      (these docs)
-
-    * [Phoenix PubSub](https://hexdocs.pm/phoenix_pubsub) - a distributed
-      pub/sub system with presence support
-
-    * [Phoenix HTML](https://hexdocs.pm/phoenix_html) - conveniences for
-      working with HTML in Phoenix
-
-    * [Phoenix LiveView](https://hexdocs.pm/phoenix_live_view) - rich,
-      real-time user experiences with server-rendered HTML and components
-
-    * [Phoenix LiveDashboard](https://hexdocs.pm/phoenix_live_dashboard) -
-      real-time performance monitoring and debugging tools for Phoenix
-      developers
-
-    * [Plug](https://hexdocs.pm/plug) - a specification and conveniences
-      for composable modules in between web applications
-
-    * [Swoosh](https://hexdocs.pm/swoosh) - a library for composing,
-      delivering and testing emails, also used by `mix phx.gen.auth`
-
-    * [Telemetry Metrics](https://hexdocs.pm/telemetry_metrics) - common
-      interface for defining metrics based on Telemetry events
-
   """
   use Application
 
diff --git a/lib/phoenix/digester.ex b/lib/phoenix/digester.ex
index fb58fc3c1..f4106d9d2 100644
--- a/lib/phoenix/digester.ex
+++ b/lib/phoenix/digester.ex
@@ -169,15 +169,22 @@ defmodule Phoenix.Digester do
     Enum.each(compressors, fn compressor ->
       [file_extension | _] = compressor.file_extensions
 
-      with {:ok, compressed_digested} <-
-             compressor.compress_file(file.digested_filename, file.digested_content) do
+      compressed_digested_result =
+        compressor.compress_file(file.digested_filename, file.digested_content)
+
+      with {:ok, compressed_digested} <- compressed_digested_result do
         File.write!(
           Path.join(path, file.digested_filename <> file_extension),
           compressed_digested
         )
       end
 
-      with {:ok, compressed} <- compressor.compress_file(file.filename, file.content) do
+      compress_result =
+        if file.digested_content == file.content,
+          do: compressed_digested_result,
+          else: compressor.compress_file(file.filename, file.content)
+
+      with {:ok, compressed} <- compress_result do
         File.write!(
           Path.join(path, file.filename <> file_extension),
           compressed
diff --git a/lib/phoenix/router.ex b/lib/phoenix/router.ex
index dd6e8999e..7bb545834 100644
--- a/lib/phoenix/router.ex
+++ b/lib/phoenix/router.ex
@@ -687,7 +687,7 @@ defmodule Phoenix.Router do
     * `:warn_on_verify` - the boolean for whether matches to this route trigger
       an unmatched route warning for `Phoenix.VerifiedRoutes`. Useful to ignore
       an otherwise catch-all route definition from being matched when verifying routes.
-      Defaults `true`.
+      Defaults `false`.
 
   ## Examples
 
diff --git a/lib/phoenix/router/scope.ex b/lib/phoenix/router/scope.ex
index b4811fab1..d0737ad67 100644
--- a/lib/phoenix/router/scope.ex
+++ b/lib/phoenix/router/scope.ex
@@ -40,7 +40,7 @@ defmodule Phoenix.Router.Scope do
     as = Keyword.get(opts, :as, Phoenix.Naming.resource_name(plug, "Controller"))
     alias? = Keyword.get(opts, :alias, true)
     trailing_slash? = Keyword.get(opts, :trailing_slash, top.trailing_slash?) == true
-    warn_on_verify? = Keyword.get(opts, :warn_on_verify, true)
+    warn_on_verify? = Keyword.get(opts, :warn_on_verify, false)
 
     if to_string(as) == "static" do
       raise ArgumentError,
diff --git a/lib/phoenix/verified_routes.ex b/lib/phoenix/verified_routes.ex
index a139280fc..f3a7b8587 100644
--- a/lib/phoenix/verified_routes.ex
+++ b/lib/phoenix/verified_routes.ex
@@ -684,9 +684,9 @@ defmodule Phoenix.VerifiedRoutes do
 
   defp match_route?(router, split_path) when is_list(split_path) do
     case router.__verify_route__(split_path) do
-      {_forward_plug, false = _warn?} -> false
-      {nil = _forward_plug, true = _warn?} -> true
-      {forward_plug, true = _warn?} -> match_forward_route?(router, forward_plug, split_path)
+      {_forward_plug, true = _warn_on_verify?} -> false
+      {nil = _forward_plug, false = _warn_on_verify?} -> true
+      {fwd_plug, false = _warn_on_verify?} -> match_forward_route?(router, fwd_plug, split_path)
       :error -> false
     end
   end
diff --git a/mix.exs b/mix.exs
index 1bb04ae0e..f8101598c 100644
--- a/mix.exs
+++ b/mix.exs
@@ -141,6 +141,7 @@ defmodule Phoenix.MixProject do
       "guides/introduction/installation.md",
       "guides/introduction/up_and_running.md",
       "guides/introduction/community.md",
+      "guides/introduction/packages_glossary.md",
       "guides/directory_structure.md",
       "guides/request_lifecycle.md",
       "guides/plug.md",
diff --git a/mix.lock b/mix.lock
index 4a71a6e93..b0c0635c0 100644
--- a/mix.lock
+++ b/mix.lock
@@ -6,11 +6,11 @@
   "cowlib": {:hex, :cowlib, "2.11.0", "0b9ff9c346629256c42ebe1eeb769a83c6cb771a6ee5960bd110ab0b9b872063", [:make, :rebar3], [], "hexpm", "2b3e9da0b21c4565751a6d4901c20d1b4cc25cbb7fd50d91d2ab6dd287bc86a9"},
   "db_connection": {:hex, :db_connection, "2.4.2", "f92e79aff2375299a16bcb069a14ee8615c3414863a6fef93156aee8e86c2ff3", [:mix], [{:connection, "~> 1.0", [hex: :connection, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "4fe53ca91b99f55ea249693a0229356a08f4d1a7931d8ffa79289b145fe83668"},
   "decimal": {:hex, :decimal, "2.0.0", "a78296e617b0f5dd4c6caf57c714431347912ffb1d0842e998e9792b5642d697", [:mix], [], "hexpm", "34666e9c55dea81013e77d9d87370fe6cb6291d1ef32f46a1600230b1d44f577"},
-  "earmark_parser": {:hex, :earmark_parser, "1.4.29", "149d50dcb3a93d9f3d6f3ecf18c918fb5a2d3c001b5d3305c926cddfbd33355b", [:mix], [], "hexpm", "4902af1b3eb139016aed210888748db8070b8125c2342ce3dcae4f38dcc63503"},
+  "earmark_parser": {:hex, :earmark_parser, "1.4.30", "0b938aa5b9bafd455056440cdaa2a79197ca5e693830b4a982beada840513c5f", [:mix], [], "hexpm", "3b5385c2d36b0473d0b206927b841343d25adb14f95f0110062506b300cd5a1b"},
   "ecto": {:hex, :ecto, "3.8.1", "35e0bd8c8eb772e14a5191a538cd079706ecb45164ea08a7523b4fc69ab70f56", [:mix], [{:decimal, "~> 1.6 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "f1b68f8d5fe3ab89e24f57c03db5b5d0aed3602077972098b3a6006a1be4b69b"},
   "ecto_sql": {:hex, :ecto_sql, "3.8.1", "1acaaba32ca0551fd19e492fc7c80414e72fc1a7140fc9395aaa53c2e8629798", [:mix], [{:db_connection, "~> 2.5 or ~> 2.4.1", [hex: :db_connection, repo: "hexpm", optional: false]}, {:ecto, "~> 3.8.1", [hex: :ecto, repo: "hexpm", optional: false]}, {:myxql, "~> 0.6.0", [hex: :myxql, repo: "hexpm", optional: true]}, {:postgrex, "~> 0.15.0 or ~> 0.16.0 or ~> 1.0", [hex: :postgrex, repo: "hexpm", optional: true]}, {:tds, "~> 2.1.1 or ~> 2.2", [hex: :tds, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4.0 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "ba7fc75882edce6f2ceca047315d5db27ead773cafea47f1724e35f1e7964525"},
   "esbuild": {:hex, :esbuild, "0.5.0", "d5bb08ff049d7880ee3609ed5c4b864bd2f46445ea40b16b4acead724fb4c4a3", [:mix], [{:castore, ">= 0.0.0", [hex: :castore, repo: "hexpm", optional: false]}], "hexpm", "f183a0b332d963c4cfaf585477695ea59eef9a6f2204fdd0efa00e099694ffe5"},
-  "ex_doc": {:hex, :ex_doc, "0.29.0", "4a1cb903ce746aceef9c1f9ae8a6c12b742a5461e6959b9d3b24d813ffbea146", [:mix], [{:earmark_parser, "~> 1.4.19", [hex: :earmark_parser, repo: "hexpm", optional: false]}, {:makeup_elixir, "~> 0.14", [hex: :makeup_elixir, repo: "hexpm", optional: false]}, {:makeup_erlang, "~> 0.1", [hex: :makeup_erlang, repo: "hexpm", optional: false]}], "hexpm", "f096adb8bbca677d35d278223361c7792d496b3fc0d0224c9d4bc2f651af5db1"},
+  "ex_doc": {:hex, :ex_doc, "0.29.1", "b1c652fa5f92ee9cf15c75271168027f92039b3877094290a75abcaac82a9f77", [:mix], [{:earmark_parser, "~> 1.4.19", [hex: :earmark_parser, repo: "hexpm", optional: false]}, {:makeup_elixir, "~> 0.14", [hex: :makeup_elixir, repo: "hexpm", optional: false]}, {:makeup_erlang, "~> 0.1", [hex: :makeup_erlang, repo: "hexpm", optional: false]}], "hexpm", "b7745fa6374a36daf484e2a2012274950e084815b936b1319aeebcf7809574f6"},
   "gettext": {:hex, :gettext, "0.20.0", "75ad71de05f2ef56991dbae224d35c68b098dd0e26918def5bb45591d5c8d429", [:mix], [], "hexpm", "1c03b177435e93a47441d7f681a7040bd2a816ece9e2666d1c9001035121eb3d"},
   "hpax": {:hex, :hpax, "0.1.1", "2396c313683ada39e98c20a75a82911592b47e5c24391363343bde74f82396ca", [:mix], [], "hexpm", "0ae7d5a0b04a8a60caf7a39fcf3ec476f35cc2cc16c05abea730d3ce6ac6c826"},
   "jason": {:hex, :jason, "1.3.0", "fa6b82a934feb176263ad2df0dbd91bf633d4a46ebfdffea0c8ae82953714946", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "53fc1f51255390e0ec7e50f9cb41e751c260d065dcba2bf0d08dc51a4002c2ac"},
diff --git a/priv/templates/phx.gen.auth/confirmation_edit.html.heex b/priv/templates/phx.gen.auth/confirmation_edit.html.heex
index 06d1b795e..36e5923be 100644
--- a/priv/templates/phx.gen.auth/confirmation_edit.html.heex
+++ b/priv/templates/phx.gen.auth/confirmation_edit.html.heex
@@ -1,7 +1,7 @@
 <div class="mx-auto max-w-sm">
   <.header class="text-center">Confirm account</.header>
 
-  <.simple_form :let={_f} for={:<%= schema.singular %>} action={~p"<%= schema.route_prefix %>/confirm/#{@token}"}>
+  <.simple_form :let={_f} as={:<%= schema.singular %>} action={~p"<%= schema.route_prefix %>/confirm/#{@token}"}>
     <:actions>
       <.button class="w-full">Confirm my account</.button>
     </:actions>
diff --git a/priv/templates/phx.gen.auth/confirmation_instructions_live.ex b/priv/templates/phx.gen.auth/confirmation_instructions_live.ex
index 948921907..55da944bd 100644
--- a/priv/templates/phx.gen.auth/confirmation_instructions_live.ex
+++ b/priv/templates/phx.gen.auth/confirmation_instructions_live.ex
@@ -7,8 +7,8 @@ defmodule <%= inspect context.web_module %>.<%= inspect Module.concat(schema.web
     ~H"""
     <.header>Resend confirmation instructions</.header>
 
-    <.simple_form :let={f} for={:<%= schema.singular %>} id="resend_confirmation_form" phx-submit="send_instructions">
-      <.input field={{f, :email}} type="email" label="Email" required />
+    <.simple_form id="resend_confirmation_form" phx-submit="send_instructions">
+      <.input field={@form[:email]} type="email" label="Email" required />
       <:actions>
         <.button phx-disable-with="Sending...">Resend confirmation instructions</.button>
       </:actions>
@@ -23,7 +23,7 @@ defmodule <%= inspect context.web_module %>.<%= inspect Module.concat(schema.web
   end
 
   def mount(_params, _session, socket) do
-    {:ok, socket}
+    {:ok, assign(socket, form: to_form(%{}, as: "<%= schema.singular %>"))}
   end
 
   def handle_event("send_instructions", %{"<%= schema.singular %>" => %{"email" => email}}, socket) do
diff --git a/priv/templates/phx.gen.auth/confirmation_live.ex b/priv/templates/phx.gen.auth/confirmation_live.ex
index 125b67bf4..c316a0cfd 100644
--- a/priv/templates/phx.gen.auth/confirmation_live.ex
+++ b/priv/templates/phx.gen.auth/confirmation_live.ex
@@ -8,8 +8,8 @@ defmodule <%= inspect context.web_module %>.<%= inspect Module.concat(schema.web
     <div class="mx-auto max-w-sm">
       <.header class="text-center">Confirm Account</.header>
 
-      <.simple_form :let={f} for={:<%= schema.singular %>} id="confirmation_form" phx-submit="confirm_account">
-        <.input field={{f, :token}} type="hidden" value={@token} />
+      <.simple_form id="confirmation_form" phx-submit="confirm_account">
+        <.input field={@form[:token]} type="hidden" />
         <:actions>
           <.button phx-disable-with="Confirming..." class="w-full">Confirm my account</.button>
         </:actions>
@@ -24,8 +24,9 @@ defmodule <%= inspect context.web_module %>.<%= inspect Module.concat(schema.web
     """
   end
 
-  def mount(params, _session, socket) do
-    {:ok, assign(socket, token: params["token"]), temporary_assigns: [token: nil]}
+  def mount(%{"token" => token}, _session, socket) do
+    form = to_form(%{"token" => token}, as: "user")
+    {:ok, assign(socket, form: form), temporary_assigns: [form: nil]}
   end
 
   # Do not log in the <%= schema.singular %> after confirmation to avoid a
diff --git a/priv/templates/phx.gen.auth/confirmation_new.html.heex b/priv/templates/phx.gen.auth/confirmation_new.html.heex
index c6b403597..ca377cf27 100644
--- a/priv/templates/phx.gen.auth/confirmation_new.html.heex
+++ b/priv/templates/phx.gen.auth/confirmation_new.html.heex
@@ -1,7 +1,7 @@
 <.header>Resend confirmation instructions</.header>
 
-<.simple_form :let={f} for={:<%= schema.singular %>} action={~p"<%= schema.route_prefix %>/confirm"}>
-  <.input field={{f, :email}} type="email" label="Email" required />
+<.simple_form :let={f} as={:<%= schema.singular %>} action={~p"<%= schema.route_prefix %>/confirm"}>
+  <.input field={f[:email]} type="email" label="Email" required />
   <:actions>
     <.button>Resend confirmation instructions</.button>
   </:actions>
diff --git a/priv/templates/phx.gen.auth/forgot_password_live.ex b/priv/templates/phx.gen.auth/forgot_password_live.ex
index db0ce3725..87b4102ed 100644
--- a/priv/templates/phx.gen.auth/forgot_password_live.ex
+++ b/priv/templates/phx.gen.auth/forgot_password_live.ex
@@ -11,8 +11,8 @@ defmodule <%= inspect context.web_module %>.<%= inspect Module.concat(schema.web
         <:subtitle>We'll send a password reset link to your inbox</:subtitle>
       </.header>
 
-      <.simple_form :let={f} id="reset_password_form" for={:<%= schema.singular %>} phx-submit="send_email">
-        <.input field={{f, :email}} type="email" placeholder="Email" required />
+      <.simple_form id="reset_password_form" phx-submit="send_email">
+        <.input field={@form[:email]} type="email" placeholder="Email" required />
         <:actions>
           <.button phx-disable-with="Sending..." class="w-full">
             Send password reset instructions
@@ -29,7 +29,7 @@ defmodule <%= inspect context.web_module %>.<%= inspect Module.concat(schema.web
   end
 
   def mount(_params, _session, socket) do
-    {:ok, socket}
+    {:ok, assign(socket, form: to_form(%{}, as: "<%= schema.singular %>"))}
   end
 
   def handle_event("send_email", %{"<%= schema.singular %>" => %{"email" => email}}, socket) do
diff --git a/priv/templates/phx.gen.auth/login_live.ex b/priv/templates/phx.gen.auth/login_live.ex
index 1f68077de..4c0ba149b 100644
--- a/priv/templates/phx.gen.auth/login_live.ex
+++ b/priv/templates/phx.gen.auth/login_live.ex
@@ -16,18 +16,15 @@ defmodule <%= inspect context.web_module %>.<%= inspect Module.concat(schema.web
       </.header>
 
       <.simple_form
-        :let={f}
         id="login_form"
-        for={:<%= schema.singular %>}
         action={~p"<%= schema.route_prefix %>/log_in"}
-        as={:<%= schema.singular %>}
         phx-update="ignore"
       >
-        <.input field={{f, :email}} type="email" label="Email" required />
-        <.input field={{f, :password}} type="password" label="Password" required />
+        <.input field={@form[:email]} type="email" label="Email" required />
+        <.input field={@form[:password]} type="password" label="Password" required />
 
-        <:actions :let={f}>
-          <.input field={{f, :remember_me}} type="checkbox" label="Keep me logged in" />
+        <:actions>
+          <.input field={@form[:remember_me]} type="checkbox" label="Keep me logged in" />
           <.link href={~p"<%= schema.route_prefix %>/reset_password"} class="text-sm font-semibold">
             Forgot your password?
           </.link>
@@ -44,6 +41,7 @@ defmodule <%= inspect context.web_module %>.<%= inspect Module.concat(schema.web
 
   def mount(_params, _session, socket) do
     email = live_flash(socket.assigns.flash, :email)
-    {:ok, assign(socket, email: email), temporary_assigns: [email: nil]}
+    form = to_form(%{email: email}, as: "<%= schema.singular %>")
+    {:ok, assign(socket, form: form), temporary_assigns: [form: form]}
   end
 end
diff --git a/priv/templates/phx.gen.auth/registration_live.ex b/priv/templates/phx.gen.auth/registration_live.ex
index 796f1ac8f..8c1413a0c 100644
--- a/priv/templates/phx.gen.auth/registration_live.ex
+++ b/priv/templates/phx.gen.auth/registration_live.ex
@@ -19,22 +19,19 @@ defmodule <%= inspect context.web_module %>.<%= inspect Module.concat(schema.web
       </.header>
 
       <.simple_form
-        :let={f}
         id="registration_form"
-        for={@changeset}
         phx-submit="save"
         phx-change="validate"
         phx-trigger-action={@trigger_submit}
         action={~p"<%= schema.route_prefix %>/log_in?_action=registered"}
         method="post"
-        as={:<%= schema.singular %>}
       >
-        <.error :if={@changeset.action == :insert}>
+        <.error :if={@check_errors}>
           Oops, something went wrong! Please check the errors below.
         </.error>
 
-        <.input field={{f, :email}} type="email" label="Email" required />
-        <.input field={{f, :password}} type="password" label="Password" required />
+        <.input field={@form[:email]} type="email" label="Email" required />
+        <.input field={@form[:password]} type="password" label="Password" required />
 
         <:actions>
           <.button phx-disable-with="Creating account..." class="w-full">Create an account</.button>
@@ -46,8 +43,13 @@ defmodule <%= inspect context.web_module %>.<%= inspect Module.concat(schema.web
 
   def mount(_params, _session, socket) do
     changeset = <%= inspect context.alias %>.change_<%= schema.singular %>_registration(%<%= inspect schema.alias %>{})
-    socket = assign(socket, changeset: changeset, trigger_submit: false)
-    {:ok, socket, temporary_assigns: [changeset: nil]}
+
+    socket =
+      socket
+      |> assign(trigger_submit: false, check_errors: false)
+      |> assign_form(changeset)
+
+    {:ok, socket, temporary_assigns: [form: nil]}
   end
 
   def handle_event("save", %{"<%= schema.singular %>" => <%= schema.singular %>_params}, socket) do
@@ -60,15 +62,24 @@ defmodule <%= inspect context.web_module %>.<%= inspect Module.concat(schema.web
           )
 
         changeset = <%= inspect context.alias %>.change_<%= schema.singular %>_registration(<%= schema.singular %>)
-        {:noreply, assign(socket, trigger_submit: true, changeset: changeset)}
+        {:noreply, socket |> assign(trigger_submit: true) |> assign_form(changeset)}
 
       {:error, %Ecto.Changeset{} = changeset} ->
-        {:noreply, assign(socket, :changeset, changeset)}
+        {:noreply, socket |> assign(check_errors: true) |> assign_form(changeset)}
     end
   end
 
   def handle_event("validate", %{"<%= schema.singular %>" => <%= schema.singular %>_params}, socket) do
     changeset = <%= inspect context.alias %>.change_<%= schema.singular %>_registration(%<%= inspect schema.alias %>{}, <%= schema.singular %>_params)
-    {:noreply, assign(socket, changeset: Map.put(changeset, :action, :validate))}
+    {:noreply, assign_form(socket, Map.put(changeset, :action, :validate))}
+  end
+
+  defp assign_form(socket, %Ecto.Changeset{} = changeset) do
+    form = to_form(changeset, as: "<%= schema.singular %>")
+
+    case changeset.errors do
+      [_ | _] -> assign(socket, form: form)
+      [] -> assign(socket, form: form, check_errors: false)
+    end
   end
 end
diff --git a/priv/templates/phx.gen.auth/registration_new.html.heex b/priv/templates/phx.gen.auth/registration_new.html.heex
index 874749081..ba4c7acc8 100644
--- a/priv/templates/phx.gen.auth/registration_new.html.heex
+++ b/priv/templates/phx.gen.auth/registration_new.html.heex
@@ -15,8 +15,8 @@
       Oops, something went wrong! Please check the errors below.
     </.error>
 
-    <.input field={{f, :email}} type="email" label="Email" required />
-    <.input field={{f, :password}} type="password" label="Password" required />
+    <.input field={f[:email]} type="email" label="Email" required />
+    <.input field={f[:password]} type="password" label="Password" required />
 
     <:actions>
       <.button phx-disable-with="Creating account..." class="w-full">Create an account</.button>
diff --git a/priv/templates/phx.gen.auth/reset_password_edit.html.heex b/priv/templates/phx.gen.auth/reset_password_edit.html.heex
index 3dd5a966a..82727a142 100644
--- a/priv/templates/phx.gen.auth/reset_password_edit.html.heex
+++ b/priv/templates/phx.gen.auth/reset_password_edit.html.heex
@@ -8,9 +8,9 @@
       Oops, something went wrong! Please check the errors below.
     </.error>
 
-    <.input field={{f, :password}} type="password" label="New Password" required />
+    <.input field={f[:password]} type="password" label="New Password" required />
     <.input
-      field={{f, :password_confirmation}}
+      field={f[:password_confirmation]}
       type="password"
       label="Confirm new password"
       required
diff --git a/priv/templates/phx.gen.auth/reset_password_live.ex b/priv/templates/phx.gen.auth/reset_password_live.ex
index 5d411c1ab..05fc6f1f4 100644
--- a/priv/templates/phx.gen.auth/reset_password_live.ex
+++ b/priv/templates/phx.gen.auth/reset_password_live.ex
@@ -9,19 +9,17 @@ defmodule <%= inspect context.web_module %>.<%= inspect Module.concat(schema.web
       <.header class="text-center">Reset Password</.header>
 
       <.simple_form
-        :let={f}
-        for={@changeset}
         id="reset_password_form"
         phx-submit="reset_password"
         phx-change="validate"
       >
-        <.error :if={@changeset.action == :insert}>
+        <.error :if={@form.errors != []}>
           Oops, something went wrong! Please check the errors below.
         </.error>
 
-        <.input field={{f, :password}} type="password" label="New password" required />
+        <.input field={@form[:password]} type="password" label="New password" required />
         <.input
-          field={{f, :password_confirmation}}
+          field={@form[:password_confirmation]}
           type="password"
           label="Confirm new password"
           required
@@ -43,16 +41,16 @@ defmodule <%= inspect context.web_module %>.<%= inspect Module.concat(schema.web
   def mount(params, _session, socket) do
     socket = assign_<%= schema.singular %>_and_token(socket, params)
 
-    socket =
+    form_source =
       case socket.assigns do
         %{<%= schema.singular %>: <%= schema.singular %>} ->
-          assign(socket, :changeset, <%= inspect context.alias %>.change_<%= schema.singular %>_password(<%= schema.singular %>))
+          <%= inspect context.alias %>.change_<%= schema.singular %>_password(<%= schema.singular %>)
 
         _ ->
-          socket
+          %{}
       end
 
-    {:ok, socket, temporary_assigns: [changeset: nil]}
+    {:ok, assign_form(socket, form_source), temporary_assigns: [form: nil]}
   end
 
   # Do not log in the <%= schema.singular %> after reset password to avoid a
@@ -66,13 +64,13 @@ defmodule <%= inspect context.web_module %>.<%= inspect Module.concat(schema.web
          |> redirect(to: ~p"<%= schema.route_prefix %>/log_in")}
 
       {:error, changeset} ->
-        {:noreply, assign(socket, :changeset, Map.put(changeset, :action, :insert))}
+        {:noreply, assign_form(socket, Map.put(changeset, :action, :insert))}
     end
   end
 
   def handle_event("validate", %{"<%= schema.singular %>" => <%= schema.singular %>_params}, socket) do
     changeset = <%= inspect context.alias %>.change_<%= schema.singular %>_password(socket.assigns.<%= schema.singular %>, <%= schema.singular %>_params)
-    {:noreply, assign(socket, changeset: Map.put(changeset, :action, :validate))}
+    {:noreply, assign_form(socket, Map.put(changeset, :action, :validate))}
   end
 
   defp assign_<%= schema.singular %>_and_token(socket, %{"token" => token}) do
@@ -84,4 +82,8 @@ defmodule <%= inspect context.web_module %>.<%= inspect Module.concat(schema.web
       |> redirect(to: ~p"/")
     end
   end
+
+  defp assign_form(socket, %{} = source) do
+    assign(socket, :form, to_form(source, as: "<%= schema.singular %>"))
+  end
 end
diff --git a/priv/templates/phx.gen.auth/reset_password_new.html.heex b/priv/templates/phx.gen.auth/reset_password_new.html.heex
index c7968829d..3f17fc684 100644
--- a/priv/templates/phx.gen.auth/reset_password_new.html.heex
+++ b/priv/templates/phx.gen.auth/reset_password_new.html.heex
@@ -4,8 +4,8 @@
     <:subtitle>We'll send a password reset link to your inbox</:subtitle>
   </.header>
 
-  <.simple_form :let={f} for={:<%= schema.singular %>} action={~p"<%= schema.route_prefix %>/reset_password"}>
-    <.input field={{f, :email}} type="email" placeholder="Email" required />
+  <.simple_form :let={f} as={:<%= schema.singular %>} action={~p"<%= schema.route_prefix %>/reset_password"}>
+    <.input field={f[:email]} type="email" placeholder="Email" required />
     <:actions>
       <.button phx-disable-with="Sending..." class="w-full">
         Send password reset instructions
diff --git a/priv/templates/phx.gen.auth/session_new.html.heex b/priv/templates/phx.gen.auth/session_new.html.heex
index cfca8d795..98f6c5f4e 100644
--- a/priv/templates/phx.gen.auth/session_new.html.heex
+++ b/priv/templates/phx.gen.auth/session_new.html.heex
@@ -13,11 +13,11 @@
   <.simple_form :let={f} for={@conn} action={~p"<%= schema.route_prefix %>/log_in"} as={:<%= schema.singular %>}>
     <.error :if={@error_message}><%%= @error_message %></.error>
 
-    <.input field={{f, :email}} type="email" label="Email" required />
-    <.input field={{f, :password}} type="password" label="Password" required />
+    <.input field={f[:email]} type="email" label="Email" required />
+    <.input field={f[:password]} type="password" label="Password" required />
 
     <:actions :let={f}>
-      <.input field={{f, :remember_me}} type="checkbox" label="Keep me logged in" />
+      <.input field={f[:remember_me]} type="checkbox" label="Keep me logged in" />
       <.link href={~p"<%= schema.route_prefix %>/reset_password"} class="text-sm font-semibold">
         Forgot your password?
       </.link>
diff --git a/priv/templates/phx.gen.auth/settings_edit.html.heex b/priv/templates/phx.gen.auth/settings_edit.html.heex
index 96828ccaa..4a102ce1a 100644
--- a/priv/templates/phx.gen.auth/settings_edit.html.heex
+++ b/priv/templates/phx.gen.auth/settings_edit.html.heex
@@ -10,11 +10,11 @@
         Oops, something went wrong! Please check the errors below.
       </.error>
 
-      <.input field={{f, :action}} type="hidden" name="action" value="update_email" />
+      <.input field={f[:action]} type="hidden" name="action" value="update_email" />
 
-      <.input field={{f, :email}} type="email" label="Email" required />
+      <.input field={f[:email]} type="email" label="Email" required />
       <.input
-        field={{f, :current_password}}
+        field={f[:current_password]}
         name="current_password"
         type="password"
         label="Current Password"
@@ -37,18 +37,18 @@
         Oops, something went wrong! Please check the errors below.
       </.error>
 
-      <.input field={{f, :action}} type="hidden" name="action" value="update_password" />
+      <.input field={f[:action]} type="hidden" name="action" value="update_password" />
 
-      <.input field={{f, :password}} type="password" label="New password" required />
+      <.input field={f[:password]} type="password" label="New password" required />
       <.input
-        field={{f, :password_confirmation}}
+        field={f[:password_confirmation]}
         type="password"
         label="Confirm new password"
         required
       />
 
       <.input
-        field={{f, :current_password}}
+        field={f[:current_password]}
         name="current_password"
         type="password"
         label="Current password"
diff --git a/priv/templates/phx.gen.auth/settings_live.ex b/priv/templates/phx.gen.auth/settings_live.ex
index b1def1184..87031ca4b 100644
--- a/priv/templates/phx.gen.auth/settings_live.ex
+++ b/priv/templates/phx.gen.auth/settings_live.ex
@@ -8,20 +8,13 @@ defmodule <%= inspect context.web_module %>.<%= inspect Module.concat(schema.web
     <.header>Change Email</.header>
 
     <.simple_form
-      :let={f}
       id="email_form"
-      for={@email_changeset}
       phx-submit="update_email"
       phx-change="validate_email"
     >
-      <.error :if={@email_changeset.action == :insert}>
-        Oops, something went wrong! Please check the errors below.
-      </.error>
-
-      <.input field={{f, :email}} type="email" label="Email" required />
-
+      <.input field={@email_form[:email]} type="email" label="Email" required />
       <.input
-        field={{f, :current_password}}
+        field={@email_form[:current_password]}
         name="current_password"
         id="current_password_for_email"
         type="password"
@@ -37,25 +30,18 @@ defmodule <%= inspect context.web_module %>.<%= inspect Module.concat(schema.web
     <.header>Change Password</.header>
 
     <.simple_form
-      :let={f}
       id="password_form"
-      for={@password_changeset}
       action={~p"<%= schema.route_prefix %>/log_in?_action=password_updated"}
       method="post"
       phx-change="validate_password"
       phx-submit="update_password"
       phx-trigger-action={@trigger_submit}
     >
-      <.error :if={@password_changeset.action == :insert}>
-        Oops, something went wrong! Please check the errors below.
-      </.error>
-
-      <.input field={{f, :email}} type="hidden" value={@current_email} />
-
-      <.input field={{f, :password}} type="password" label="New password" required />
-      <.input field={{f, :password_confirmation}} type="password" label="Confirm new password" />
+      <.input field={@password_form[:email]} type="hidden" value={@current_email} />
+      <.input field={@password_form[:password]} type="password" label="New password" required />
+      <.input field={@password_form[:password_confirmation]} type="password" label="Confirm new password" />
       <.input
-        field={{f, :current_password}}
+        field={@password_form[:current_password]}
         name="current_password"
         type="password"
         label="Current password"
@@ -85,14 +71,16 @@ defmodule <%= inspect context.web_module %>.<%= inspect Module.concat(schema.web
 
   def mount(_params, _session, socket) do
     <%= schema.singular %> = socket.assigns.current_<%= schema.singular %>
+    email_changeset = <%= inspect context.alias %>.change_<%= schema.singular %>_email(<%= schema.singular %>)
+    password_changeset = <%= inspect context.alias %>.change_<%= schema.singular %>_password(<%= schema.singular %>)
 
     socket =
       socket
       |> assign(:current_password, nil)
       |> assign(:email_form_current_password, nil)
       |> assign(:current_email, <%= schema.singular %>.email)
-      |> assign(:email_changeset, <%= inspect context.alias %>.change_<%= schema.singular %>_email(<%= schema.singular %>))
-      |> assign(:password_changeset, <%= inspect context.alias %>.change_<%= schema.singular %>_password(<%= schema.singular %>))
+      |> assign(:email_form, to_form(email_changeset))
+      |> assign(:password_form, to_form(password_changeset))
       |> assign(:trigger_submit, false)
 
     {:ok, socket}
@@ -100,15 +88,13 @@ defmodule <%= inspect context.web_module %>.<%= inspect Module.concat(schema.web
 
   def handle_event("validate_email", params, socket) do
     %{"current_password" => password, "<%= schema.singular %>" => <%= schema.singular %>_params} = params
-    email_changeset = <%= inspect context.alias %>.change_<%= schema.singular %>_email(socket.assigns.current_<%= schema.singular %>, <%= schema.singular %>_params)
-
-    socket =
-      assign(socket,
-        email_changeset: Map.put(email_changeset, :action, :validate),
-        email_form_current_password: password
-      )
+    email_form =
+      socket.assigns.current_<%= schema.singular %>
+      |> <%= inspect context.alias %>.change_<%= schema.singular %>_email(<%= schema.singular %>_params)
+      |> Map.put(:action, :validate)
+      |> to_form()
 
-    {:noreply, socket}
+    {:noreply, assign(socket, email_form: email_form, email_form_current_password: password)}
   end
 
   def handle_event("update_email", params, socket) do
@@ -124,21 +110,22 @@ defmodule <%= inspect context.web_module %>.<%= inspect Module.concat(schema.web
         )
 
         info = "A link to confirm your email change has been sent to the new address."
-        {:noreply, put_flash(socket, :info, info)}
+        {:noreply, socket |> put_flash(:info, info) |> assign(email_form_current_password: nil)}
 
       {:error, changeset} ->
-        {:noreply, assign(socket, :email_changeset, Map.put(changeset, :action, :insert))}
+        {:noreply, assign(socket, :email_form, to_form(Map.put(changeset, :action, :insert)))}
     end
   end
 
   def handle_event("validate_password", params, socket) do
     %{"current_password" => password, "<%= schema.singular %>" => <%= schema.singular %>_params} = params
-    password_changeset = <%= inspect context.alias %>.change_<%= schema.singular %>_password(socket.assigns.current_<%= schema.singular %>, <%= schema.singular %>_params)
+    password_form =
+      socket.assigns.current_<%= schema.singular %>
+      |> <%= inspect context.alias %>.change_<%= schema.singular %>_password(<%= schema.singular %>_params)
+      |> Map.put(:action, :validate)
+      |> to_form()
 
-    {:noreply,
-     socket
-     |> assign(:password_changeset, Map.put(password_changeset, :action, :validate))
-     |> assign(:current_password, password)}
+    {:noreply, assign(socket, password_form: password_form, current_password: password)}
   end
 
   def handle_event("update_password", params, socket) do
@@ -147,15 +134,15 @@ defmodule <%= inspect context.web_module %>.<%= inspect Module.concat(schema.web
 
     case <%= inspect context.alias %>.update_<%= schema.singular %>_password(<%= schema.singular %>, password, <%= schema.singular %>_params) do
       {:ok, <%= schema.singular %>} ->
-        socket =
-          socket
-          |> assign(:trigger_submit, true)
-          |> assign(:password_changeset, <%= inspect context.alias %>.change_<%= schema.singular %>_password(<%= schema.singular %>, <%= schema.singular %>_params))
+        password_form =
+          <%= schema.singular %>
+          |> <%= inspect context.alias %>.change_<%= schema.singular %>_password(<%= schema.singular %>_params)
+          |> to_form()
 
-        {:noreply, socket}
+        {:noreply, assign(socket, trigger_submit: true, password_form: password_form)}
 
       {:error, changeset} ->
-        {:noreply, assign(socket, :password_changeset, changeset)}
+        {:noreply, assign(socket, password_form: to_form(changeset))}
     end
   end
 end
diff --git a/priv/templates/phx.gen.live/core_components.ex b/priv/templates/phx.gen.live/core_components.ex
index ca10a7add..d1ed11e32 100644
--- a/priv/templates/phx.gen.live/core_components.ex
+++ b/priv/templates/phx.gen.live/core_components.ex
@@ -77,7 +77,7 @@ defmodule <%= @web_namespace %>.CoreComponents do
                   phx-click={hide_modal(@on_cancel, @id)}
                   type="button"
                   class="-m-3 flex-none p-3 opacity-20 hover:opacity-40"
-                  aria-label=<%= if @gettext do %>{gettext("close")}<% else %>"close"<% end %>
+                  aria-label={gettext("close")}
                 >
                   <Heroicons.x_mark solid class="h-5 w-5 stroke-current" />
                 </button>
@@ -166,7 +166,7 @@ defmodule <%= @web_namespace %>.CoreComponents do
         :if={@close}
         type="button"
         class="group absolute top-2 right-1 p-2"
-        aria-label=<%= if @gettext do %>{gettext("close")}<% else %>"close"<% end %>
+        aria-label={gettext("close")}
       >
         <Heroicons.x_mark solid class="h-5 w-5 stroke-current opacity-40 group-hover:opacity-70" />
       </button>
@@ -174,14 +174,41 @@ defmodule <%= @web_namespace %>.CoreComponents do
     """
   end
 
+  @doc """
+  Shows the flash group with standard titles and content.
+
+  ## Examples
+
+      <.flash_group flash={@flash} />
+  """
+  attr :flash, :map, required: true, doc: "the map of flash messages"
+
+  def flash_group(assigns) do
+    ~H"""
+    <.flash kind={:info} title="Success!" flash={@flash} />
+    <.flash kind={:error} title="Error!" flash={@flash} />
+    <.flash
+      id="disconnected"
+      kind={:error}
+      title="We can't find the internet"
+      close={false}
+      autoshow={false}
+      phx-disconnected={show("#disconnected")}
+      phx-connected={hide("#disconnected")}
+    >
+      Attempting to reconnect <Heroicons.arrow_path class="ml-1 w-3 h-3 inline animate-spin" />
+    </.flash>
+    """
+  end
+
   @doc """
   Renders a simple form.
 
   ## Examples
 
-      <.simple_form :let={f} for={:user} phx-change="validate" phx-submit="save">
-        <.input field={{f, :email}} label="Email"/>
-        <.input field={{f, :username}} label="Username" />
+      <.simple_form phx-change="validate" phx-submit="save">
+        <.input field={@form[:email]} label="Email"/>
+        <.input field={@form[:username]} label="Username" />
         <:actions>
           <.button>Save</.button>
         </:actions>
@@ -189,7 +216,6 @@ defmodule <%= @web_namespace %>.CoreComponents do
   """
   attr :for, :any, default: nil, doc: "the datastructure for the form"
   attr :as, :any, default: nil, doc: "the server side parameter to collect all input under"
-
   attr :rest, :global,
     include: ~w(autocomplete name rel action enctype method novalidate target),
     doc: "the arbitrary HTML attributes to apply to the form tag"
@@ -249,10 +275,10 @@ defmodule <%= @web_namespace %>.CoreComponents do
 
   ## Examples
 
-      <.input field={{f, :email}} type="email" />
+      <.input field={@form[:email]} type="email" />
       <.input name="my-input" errors={["oh no!"]} />
   """
-  attr :id, :any
+  attr :id, :any, default: nil
   attr :name, :any
   attr :label, :string, default: nil
 
@@ -262,7 +288,7 @@ defmodule <%= @web_namespace %>.CoreComponents do
                range radio search select tel text textarea time url week)
 
   attr :value, :any
-  attr :field, :any, doc: "a %Phoenix.HTML.Form{}/field name tuple, for example: {f, :email}"
+  attr :field, :any, doc: "a %Phoenix.HTML.Form{}/field name tuple, for example: @form[:email]"
   attr :errors, :list
   attr :checked, :boolean, doc: "the checked flag for checkbox inputs"
   attr :prompt, :string, default: nil, doc: "the prompt for select inputs"
@@ -272,21 +298,17 @@ defmodule <%= @web_namespace %>.CoreComponents do
                                    pattern placeholder readonly required rows size step)
   slot :inner_block
 
-  def input(%{field: {f, field}} = assigns) do
+  def input(%{field: %Phoenix.HTML.FormField{} = field} = assigns) do
     assigns
-    |> assign(field: nil)
-    |> assign_new(:name, fn ->
-      name = Phoenix.HTML.Form.input_name(f, field)
-      if assigns.multiple, do: name <> "[]", else: name
-    end)
-    |> assign_new(:id, fn -> Phoenix.HTML.Form.input_id(f, field) end)
-    |> assign_new(:value, fn -> format_input_value(Phoenix.HTML.Form.input_value(f, field)) end)
-    |> assign_new(:errors, fn -> translate_errors(f.errors || [], field) end)
+    |> assign(field: nil, id: assigns.id || field.id)
+    |> assign_new(:name, fn -> if assigns.multiple, do: field.name <> "[]", else: field.name end)
+    |> assign_new(:value, fn -> Phoenix.HTML.Form.normalize_value(assigns.type, field.value) end)
+    |> assign_new(:errors, fn -> Enum.map(field.errors, &translate_error(&1)) end)
     |> input()
   end
 
   def input(%{type: "checkbox"} = assigns) do
-    assigns = assign_new(assigns, :checked, fn -> input_equals?(assigns.value, "true") end)
+    assigns = assign_new(assigns, :checked, fn -> assigns.value == "true" end)
 
     ~H"""
     <label phx-feedback-for={@name} class="flex items-center gap-4 text-sm leading-6 text-zinc-600">
@@ -332,14 +354,14 @@ defmodule <%= @web_namespace %>.CoreComponents do
         id={@id || @name}
         name={@name}
         class={[
-          input_border(@errors),
           "mt-2 block min-h-[6rem] w-full rounded-lg border-zinc-300 py-[7px] px-[11px]",
           "text-zinc-900 focus:border-zinc-400 focus:outline-none focus:ring-4 focus:ring-zinc-800/5 sm:text-sm sm:leading-6",
-          "phx-no-feedback:border-zinc-300 phx-no-feedback:focus:border-zinc-400 phx-no-feedback:focus:ring-zinc-800/5"
+          "phx-no-feedback:border-zinc-300 phx-no-feedback:focus:border-zinc-400 phx-no-feedback:focus:ring-zinc-800/5",
+          "border-zinc-300 focus:border-zinc-400 focus:ring-zinc-800/5",
+          @errors != [] && "border-rose-400 focus:border-rose-400 focus:ring-rose-400/10"
         ]}
         {@rest}
-      >
-    <%%= @value %></textarea>
+      ><%%= @value %></textarea>
       <.error :for={msg <- @errors}><%%= msg %></.error>
     </div>
     """
@@ -355,10 +377,11 @@ defmodule <%= @web_namespace %>.CoreComponents do
         id={@id || @name}
         value={@value}
         class={[
-          input_border(@errors),
           "mt-2 block w-full rounded-lg border-zinc-300 py-[7px] px-[11px]",
           "text-zinc-900 focus:outline-none focus:ring-4 sm:text-sm sm:leading-6",
-          "phx-no-feedback:border-zinc-300 phx-no-feedback:focus:border-zinc-400 phx-no-feedback:focus:ring-zinc-800/5"
+          "phx-no-feedback:border-zinc-300 phx-no-feedback:focus:border-zinc-400 phx-no-feedback:focus:ring-zinc-800/5",
+          "border-zinc-300 focus:border-zinc-400 focus:ring-zinc-800/5",
+          @errors != [] && "border-rose-400 focus:border-rose-400 focus:ring-rose-400/10"
         ]}
         {@rest}
       />
@@ -367,12 +390,6 @@ defmodule <%= @web_namespace %>.CoreComponents do
     """
   end
 
-  defp input_border([] = _errors),
-    do: "border-zinc-300 focus:border-zinc-400 focus:ring-zinc-800/5"
-
-  defp input_border([_ | _] = _errors),
-    do: "border-rose-400 focus:border-rose-400 focus:ring-rose-400/10"
-
   @doc """
   Renders a label.
   """
@@ -453,13 +470,14 @@ defmodule <%= @web_namespace %>.CoreComponents do
         <thead class="text-left text-[0.8125rem] leading-6 text-zinc-500">
           <tr>
             <th :for={col <- @col} class="p-0 pb-4 pr-6 font-normal"><%%= col[:label] %></th>
-            <th class="relative p-0 pb-4"><span class="sr-only"><%= if @gettext do %><%%= gettext("Actions") %><% else %>Actions<% end %></span></th>
+            <th class="relative p-0 pb-4"><span class="sr-only"><%%= gettext("Actions") %></span></th>
           </tr>
         </thead>
         <tbody class="relative divide-y divide-zinc-100 border-t border-zinc-200 text-sm leading-6 text-zinc-700">
           <tr
             :for={row <- @rows}
             id={"#{@id}-#{Phoenix.Param.to_param(row)}"}
+            badclass="relative group hover:bg-zinc-50"
             class="group hover:bg-zinc-50"
           >
             <td
@@ -638,14 +656,4 @@ defmodule <%= @web_namespace %>.CoreComponents do
   def translate_errors(errors, field) when is_list(errors) do
     for {^field, {msg, opts}} <- errors, do: translate_error({msg, opts})
   end
-
-  defp input_equals?(val1, val2) do
-    Phoenix.HTML.html_escape(val1) == Phoenix.HTML.html_escape(val2)
-  end
-
-  defp format_input_value(%struct{} = value) when struct in [NaiveDateTime, DateTime] do
-    Calendar.strftime(value, "%Y-%m-%dT%H:%M")
-  end
-
-  defp format_input_value(other), do: other
 end
diff --git a/priv/templates/phx.gen.live/form_component.ex b/priv/templates/phx.gen.live/form_component.ex
index ed4976706..fe1b47a5e 100644
--- a/priv/templates/phx.gen.live/form_component.ex
+++ b/priv/templates/phx.gen.live/form_component.ex
@@ -13,8 +13,6 @@ defmodule <%= inspect context.web_module %>.<%= inspect Module.concat(schema.web
       </.header>
 
       <.simple_form
-        :let={f}
-        for={@changeset}
         id="<%= schema.singular %>-form"
         phx-target={@myself}
         phx-change="validate"
@@ -36,7 +34,7 @@ defmodule <%= inspect context.web_module %>.<%= inspect Module.concat(schema.web
     {:ok,
      socket
      |> assign(assigns)
-     |> assign(:changeset, changeset)}
+     |> assign_form(changeset)}
   end
 
   @impl true
@@ -46,7 +44,7 @@ defmodule <%= inspect context.web_module %>.<%= inspect Module.concat(schema.web
       |> <%= inspect context.alias %>.change_<%= schema.singular %>(<%= schema.singular %>_params)
       |> Map.put(:action, :validate)
 
-    {:noreply, assign(socket, :changeset, changeset)}
+    {:noreply, assign_form(socket, changeset)}
   end
 
   def handle_event("save", %{"<%= schema.singular %>" => <%= schema.singular %>_params}, socket) do
@@ -62,7 +60,7 @@ defmodule <%= inspect context.web_module %>.<%= inspect Module.concat(schema.web
          |> push_navigate(to: socket.assigns.navigate)}
 
       {:error, %Ecto.Changeset{} = changeset} ->
-        {:noreply, assign(socket, :changeset, changeset)}
+        {:noreply, assign_form(socket, changeset)}
     end
   end
 
@@ -75,7 +73,11 @@ defmodule <%= inspect context.web_module %>.<%= inspect Module.concat(schema.web
          |> push_navigate(to: socket.assigns.navigate)}
 
       {:error, %Ecto.Changeset{} = changeset} ->
-        {:noreply, assign(socket, changeset: changeset)}
+        {:noreply, assign_form(socket, changeset)}
     end
   end
+
+  defp assign_form(socket, %Ecto.Changeset{} = changeset) do
+    assign(socket, :form, to_form(changeset))
+  end
 end


Test patch:
diff --git a/test/mix/tasks/phx.gen.auth_test.exs b/test/mix/tasks/phx.gen.auth_test.exs
index bf9b5fc28..3d90b050d 100644
--- a/test/mix/tasks/phx.gen.auth_test.exs
+++ b/test/mix/tasks/phx.gen.auth_test.exs
@@ -525,7 +525,7 @@ defmodule Mix.Tasks.Phx.Gen.AuthTest do
 
       assert_file("lib/my_app_web/controllers/warehouse/user_confirmation_html/new.html.heex", fn file ->
         assert file =~
-                 ~S|<.simple_form :let={f} for={:user} action={~p"/warehouse/users/confirm"}>|
+                 ~S|<.simple_form :let={f} as={:user} action={~p"/warehouse/users/confirm"}>|
 
         assert file =~
                  ~S|<.link href={~p"/warehouse/users/register"}>Register</.link>|
@@ -599,7 +599,7 @@ defmodule Mix.Tasks.Phx.Gen.AuthTest do
         "lib/my_app_web/controllers/warehouse/user_reset_password_html/new.html.heex",
         fn file ->
           assert file =~
-                   ~S|<.simple_form :let={f} for={:user} action={~p"/warehouse/users/reset_password"}>|
+                   ~S|<.simple_form :let={f} as={:user} action={~p"/warehouse/users/reset_password"}>|
         end
       )
 
diff --git a/test/mix/tasks/phx.gen.html_test.exs b/test/mix/tasks/phx.gen.html_test.exs
index 435df6ee4..fee20c177 100644
--- a/test/mix/tasks/phx.gen.html_test.exs
+++ b/test/mix/tasks/phx.gen.html_test.exs
@@ -156,30 +156,30 @@ defmodule Mix.Tasks.Phx.Gen.HtmlTest do
 
       for filename <- ["new.html.heex", "edit.html.heex"] do
         assert_file("lib/phoenix_web/controllers/post_html/#{filename}", fn file ->
-          assert file =~ ~s(<.input field={{f, :title}} type="text")
-          assert file =~ ~s(<.input field={{f, :votes}} type="number")
-          assert file =~ ~s(<.input field={{f, :cost}} type="number" label="Cost" step="any")
+          assert file =~ ~s(<.input field={f[:title]} type="text")
+          assert file =~ ~s(<.input field={f[:votes]} type="number")
+          assert file =~ ~s(<.input field={f[:cost]} type="number" label="Cost" step="any")
           assert file =~ """
             <.input
-              field={{f, :tags}}
+              field={f[:tags]}
               type="select"
               multiple
           """
-          assert file =~ ~s(<.input field={{f, :popular}} type="checkbox")
-          assert file =~ ~s(<.input field={{f, :drafted_at}} type="datetime-local")
-          assert file =~ ~s(<.input field={{f, :published_at}} type="datetime-local")
-          assert file =~ ~s(<.input field={{f, :deleted_at}} type="datetime-local")
-          assert file =~ ~s(<.input field={{f, :announcement_date}} type="date")
-          assert file =~ ~s(<.input field={{f, :alarm}} type="time")
-          assert file =~ ~s(<.input field={{f, :secret}} type="text" label="Secret" />)
+          assert file =~ ~s(<.input field={f[:popular]} type="checkbox")
+          assert file =~ ~s(<.input field={f[:drafted_at]} type="datetime-local")
+          assert file =~ ~s(<.input field={f[:published_at]} type="datetime-local")
+          assert file =~ ~s(<.input field={f[:deleted_at]} type="datetime-local")
+          assert file =~ ~s(<.input field={f[:announcement_date]} type="date")
+          assert file =~ ~s(<.input field={f[:alarm]} type="time")
+          assert file =~ ~s(<.input field={f[:secret]} type="text" label="Secret" />)
           assert file =~ """
             <.input
-              field={{f, :status}}
+              field={f[:status]}
               type="select"
           """
           assert file =~ ~s|Ecto.Enum.values(Phoenix.Blog.Post, :status)|
 
-          refute file =~ ~s(<.input field={{f, :user_id}})
+          refute file =~ ~s(<.input field={f[:user_id]})
         end)
       end
 
diff --git a/test/mix/tasks/phx.gen.live_test.exs b/test/mix/tasks/phx.gen.live_test.exs
index 32fb35cb2..1f651b536 100644
--- a/test/mix/tasks/phx.gen.live_test.exs
+++ b/test/mix/tasks/phx.gen.live_test.exs
@@ -99,31 +99,31 @@ defmodule Mix.Tasks.Phx.Gen.LiveTest do
       end
 
       assert_file "lib/phoenix_web/live/post_live/form_component.ex", fn file ->
-        assert file =~ ~s(<.simple_form\n        :let={f}\n        for={@changeset}\n)
-        assert file =~ ~s(<.input field={{f, :title}} type="text")
-        assert file =~ ~s(<.input field={{f, :votes}} type="number")
-        assert file =~ ~s(<.input field={{f, :cost}} type="number" label="Cost" step="any")
+        assert file =~ ~s(<.simple_form)
+        assert file =~ ~s(<.input field={@form[:title]} type="text")
+        assert file =~ ~s(<.input field={@form[:votes]} type="number")
+        assert file =~ ~s(<.input field={@form[:cost]} type="number" label="Cost" step="any")
         assert file =~ """
                 <.input
-                  field={{f, :tags}}
+                  field={@form[:tags]}
                   type="select"
                   multiple
         """
-        assert file =~ ~s(<.input field={{f, :popular}} type="checkbox")
-        assert file =~ ~s(<.input field={{f, :drafted_at}} type="datetime-local")
-        assert file =~ ~s(<.input field={{f, :published_at}} type="datetime-local")
-        assert file =~ ~s(<.input field={{f, :deleted_at}} type="datetime-local")
-        assert file =~ ~s(<.input field={{f, :announcement_date}} type="date")
-        assert file =~ ~s(<.input field={{f, :alarm}} type="time")
-        assert file =~ ~s(<.input field={{f, :secret}} type="text" label="Secret" />)
+        assert file =~ ~s(<.input field={@form[:popular]} type="checkbox")
+        assert file =~ ~s(<.input field={@form[:drafted_at]} type="datetime-local")
+        assert file =~ ~s(<.input field={@form[:published_at]} type="datetime-local")
+        assert file =~ ~s(<.input field={@form[:deleted_at]} type="datetime-local")
+        assert file =~ ~s(<.input field={@form[:announcement_date]} type="date")
+        assert file =~ ~s(<.input field={@form[:alarm]} type="time")
+        assert file =~ ~s(<.input field={@form[:secret]} type="text" label="Secret" />)
         assert file =~ """
                 <.input
-                  field={{f, :status}}
+                  field={@form[:status]}
                   type="select"
         """
         assert file =~ ~s|Ecto.Enum.values(Phoenix.Blog.Post, :status)|
 
-        refute file =~ ~s(<.input field={{f, :user_id}})
+        refute file =~ ~s(<.input field={@form[:user_id]})
       end
 
       assert_file "test/phoenix_web/live/post_live_test.exs", fn file ->
diff --git a/test/phoenix/verified_routes_test.exs b/test/phoenix/verified_routes_test.exs
index 7b0cea1c7..095c0c9b7 100644
--- a/test/phoenix/verified_routes_test.exs
+++ b/test/phoenix/verified_routes_test.exs
@@ -33,7 +33,7 @@ defmodule Phoenix.VerifiedRoutesTest do
     get "/posts/:id/info", PostController, :show
     get "/posts/file/*file", PostController, :file
     get "/posts/skip", PostController, :skip
-    get "/should-warn/*all", PostController, :all, warn_on_verify: false
+    get "/should-warn/*all", PostController, :all, warn_on_verify: true
 
     scope "/", host: "users." do
       post "/host_users/:id/info", UserController, :create