Repository: phoenixframework/phoenix
Pull request #5253 (2023-02-04 10:23:05)

Use topbar 2.0.0 in project generator
The topbar delay functionality was added upstream in https://github.com/buunguyen/topbar/pull/15.

With this change phoenix once again packages the same code as upstream which means it's easier to move from the generated code to using a dedicated js package manager like `npm` or `yarn`.

There should be no functional difference.


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/templates/phx_assets/app.js b/installer/templates/phx_assets/app.js
index 8464c5555..ec7623990 100644
--- a/installer/templates/phx_assets/app.js
+++ b/installer/templates/phx_assets/app.js
@@ -27,7 +27,7 @@ import "phoenix_html"
 
 // Show progress bar on live navigation and form submits
 <%= @live_comment %>topbar.config({barColors: {0: "#29d"}, shadowColor: "rgba(0, 0, 0, .3)"})
-<%= @live_comment %>window.addEventListener("phx:page-loading-start", _info => topbar.delayedShow(200))
+<%= @live_comment %>window.addEventListener("phx:page-loading-start", _info => topbar.show(200))
 <%= @live_comment %>window.addEventListener("phx:page-loading-stop", _info => topbar.hide())
 
 // connect if there are any LiveViews on the page
diff --git a/installer/templates/phx_assets/topbar.js b/installer/templates/phx_assets/topbar.js
index 4176ede1e..41957274d 100644
--- a/installer/templates/phx_assets/topbar.js
+++ b/installer/templates/phx_assets/topbar.js
@@ -1,9 +1,7 @@
 /**
  * @license MIT
- * topbar 1.0.0, 2021-01-06
- * Modifications:
- *   - add delayedShow(time) (2022-09-21)
- * http://buunguyen.github.io/topbar
+ * topbar 2.0.0, 2023-02-04
+ * https://buunguyen.github.io/topbar
  * Copyright (c) 2021 Buu Nguyen
  */
 (function (window, document) {
@@ -98,26 +96,26 @@
         for (var key in opts)
           if (options.hasOwnProperty(key)) options[key] = opts[key];
       },
-      delayedShow: function(time) {
+      show: function (delay) {
         if (showing) return;
-        if (delayTimerId) return;
-        delayTimerId = setTimeout(() => topbar.show(), time);
-      },
-      show: function () {
-        if (showing) return;
-        showing = true;
-        if (fadeTimerId !== null) window.cancelAnimationFrame(fadeTimerId);
-        if (!canvas) createCanvas();
-        canvas.style.opacity = 1;
-        canvas.style.display = "block";
-        topbar.progress(0);
-        if (options.autoRun) {
-          (function loop() {
-            progressTimerId = window.requestAnimationFrame(loop);
-            topbar.progress(
-              "+" + 0.05 * Math.pow(1 - Math.sqrt(currentProgress), 2)
-            );
-          })();
+        if (delay) {
+          if (delayTimerId) return;
+          delayTimerId = setTimeout(() => topbar.show(), delay);
+        } else  {
+          showing = true;
+          if (fadeTimerId !== null) window.cancelAnimationFrame(fadeTimerId);
+          if (!canvas) createCanvas();
+          canvas.style.opacity = 1;
+          canvas.style.display = "block";
+          topbar.progress(0);
+          if (options.autoRun) {
+            (function loop() {
+              progressTimerId = window.requestAnimationFrame(loop);
+              topbar.progress(
+                "+" + 0.05 * Math.pow(1 - Math.sqrt(currentProgress), 2)
+              );
+            })();
+          }
         }
       },
       progress: function (to) {
diff --git a/installer/templates/phx_web/components/core_components.ex b/installer/templates/phx_web/components/core_components.ex
index ca10a7add..43aa2ff7c 100644
--- a/installer/templates/phx_web/components/core_components.ex
+++ b/installer/templates/phx_web/components/core_components.ex
@@ -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>
@@ -249,21 +276,23 @@ 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
+  attr :value, :any
 
   attr :type, :string,
     default: "text",
     values: ~w(checkbox color date datetime-local email file hidden month number password
                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 :errors, :list
+  attr :field, Phoenix.HTML.FormField,
+    doc: "a form field struct retrieved from the form, for example: @form[:email]"
+
+  attr :errors, :list, default: []
   attr :checked, :boolean, doc: "the checked flag for checkbox inputs"
   attr :prompt, :string, default: nil, doc: "the prompt for select inputs"
   attr :options, :list, doc: "the options to pass to Phoenix.HTML.Form.options_for_select/2"
@@ -272,36 +301,36 @@ 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(:errors, Enum.map(field.errors, &translate_error(&1)))
+    |> assign_new(:name, fn -> if assigns.multiple, do: field.name <> "[]", else: field.name end)
+    |> assign_new(:value, fn -> field.value end)
     |> input()
   end
 
-  def input(%{type: "checkbox"} = assigns) do
-    assigns = assign_new(assigns, :checked, fn -> input_equals?(assigns.value, "true") end)
+  def input(%{type: "checkbox", value: value} = assigns) do
+    assigns =
+      assign_new(assigns, :checked, fn -> Phoenix.HTML.Form.normalize_value("checkbox", value) end)
 
     ~H"""
-    <label phx-feedback-for={@name} class="flex items-center gap-4 text-sm leading-6 text-zinc-600">
-      <input type="hidden" name={@name} value="false" />
-      <input
-        type="checkbox"
-        id={@id || @name}
-        name={@name}
-        value="true"
-        checked={@checked}
-        class="rounded border-zinc-300 text-zinc-900 focus:ring-zinc-900"
-        {@rest}
-      />
-      <%%= @label %>
-    </label>
+    <div phx-feedback-for={@name}>
+      <label class="flex items-center gap-4 text-sm leading-6 text-zinc-600">
+        <input type="hidden" name={@name} value="false" />
+        <input
+          type="checkbox"
+          id={@id || @name}
+          name={@name}
+          value="true"
+          checked={@checked}
+          class="rounded border-zinc-300 text-zinc-900 focus:ring-zinc-900"
+          {@rest}
+        />
+        <%%= @label %>
+      </label>
+      <.error :for={msg <- @errors}><%%= msg %></.error>
+    </div>
     """
   end
 
@@ -332,14 +361,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>
+      ><%%= Phoenix.HTML.Form.normalize_value("textarea", @value) %></textarea>
       <.error :for={msg <- @errors}><%%= msg %></.error>
     </div>
     """
@@ -353,12 +382,13 @@ defmodule <%= @web_namespace %>.CoreComponents do
         type={@type}
         name={@name}
         id={@id || @name}
-        value={@value}
+        value={Phoenix.HTML.Form.normalize_value(@type, @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 +397,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.
   """
@@ -437,7 +461,8 @@ defmodule <%= @web_namespace %>.CoreComponents do
       </.table>
   """
   attr :id, :string, required: true
-  attr :row_click, :any, default: nil
+  attr :row_id, :any, default: nil, doc: "the function for generating the row id"
+  attr :row_click, :any, default: nil, doc: "the function for handling phx-click on each row"
   attr :rows, :list, required: true
 
   slot :col, required: true do
@@ -459,7 +484,7 @@ defmodule <%= @web_namespace %>.CoreComponents do
         <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)}"}
+            id={@row_id && @row_id.(row)}
             class="group hover:bg-zinc-50"
           >
             <td
@@ -638,14 +663,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..f439e6a58 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,25 @@ 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 %>")
+
+    if changeset.valid? do
+      assign(socket, form: form, check_errors: false)
+    else
+      assign(socket, form: form)
+    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..43aa2ff7c 100644
--- a/priv/templates/phx.gen.live/core_components.ex
+++ b/priv/templates/phx.gen.live/core_components.ex
@@ -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>
@@ -249,21 +276,23 @@ 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
+  attr :value, :any
 
   attr :type, :string,
     default: "text",
     values: ~w(checkbox color date datetime-local email file hidden month number password
                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 :errors, :list
+  attr :field, Phoenix.HTML.FormField,
+    doc: "a form field struct retrieved from the form, for example: @form[:email]"
+
+  attr :errors, :list, default: []
   attr :checked, :boolean, doc: "the checked flag for checkbox inputs"
   attr :prompt, :string, default: nil, doc: "the prompt for select inputs"
   attr :options, :list, doc: "the options to pass to Phoenix.HTML.Form.options_for_select/2"
@@ -272,36 +301,36 @@ 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(:errors, Enum.map(field.errors, &translate_error(&1)))
+    |> assign_new(:name, fn -> if assigns.multiple, do: field.name <> "[]", else: field.name end)
+    |> assign_new(:value, fn -> field.value end)
     |> input()
   end
 
-  def input(%{type: "checkbox"} = assigns) do
-    assigns = assign_new(assigns, :checked, fn -> input_equals?(assigns.value, "true") end)
+  def input(%{type: "checkbox", value: value} = assigns) do
+    assigns =
+      assign_new(assigns, :checked, fn -> Phoenix.HTML.Form.normalize_value("checkbox", value) end)
 
     ~H"""
-    <label phx-feedback-for={@name} class="flex items-center gap-4 text-sm leading-6 text-zinc-600">
-      <input type="hidden" name={@name} value="false" />
-      <input
-        type="checkbox"
-        id={@id || @name}
-        name={@name}
-        value="true"
-        checked={@checked}
-        class="rounded border-zinc-300 text-zinc-900 focus:ring-zinc-900"
-        {@rest}
-      />
-      <%%= @label %>
-    </label>
+    <div phx-feedback-for={@name}>
+      <label class="flex items-center gap-4 text-sm leading-6 text-zinc-600">
+        <input type="hidden" name={@name} value="false" />
+        <input
+          type="checkbox"
+          id={@id || @name}
+          name={@name}
+          value="true"
+          checked={@checked}
+          class="rounded border-zinc-300 text-zinc-900 focus:ring-zinc-900"
+          {@rest}
+        />
+        <%%= @label %>
+      </label>
+      <.error :for={msg <- @errors}><%%= msg %></.error>
+    </div>
     """
   end
 
@@ -332,14 +361,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>
+      ><%%= Phoenix.HTML.Form.normalize_value("textarea", @value) %></textarea>
       <.error :for={msg <- @errors}><%%= msg %></.error>
     </div>
     """
@@ -353,12 +382,13 @@ defmodule <%= @web_namespace %>.CoreComponents do
         type={@type}
         name={@name}
         id={@id || @name}
-        value={@value}
+        value={Phoenix.HTML.Form.normalize_value(@type, @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 +397,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.
   """
@@ -437,7 +461,8 @@ defmodule <%= @web_namespace %>.CoreComponents do
       </.table>
   """
   attr :id, :string, required: true
-  attr :row_click, :any, default: nil
+  attr :row_id, :any, default: nil, doc: "the function for generating the row id"
+  attr :row_click, :any, default: nil, doc: "the function for handling phx-click on each row"
   attr :rows, :list, required: true
 
   slot :col, required: true do
@@ -459,7 +484,7 @@ defmodule <%= @web_namespace %>.CoreComponents do
         <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)}"}
+            id={@row_id && @row_id.(row)}
             class="group hover:bg-zinc-50"
           >
             <td
@@ -638,14 +663,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
diff --git a/priv/templates/phx.gen.live/index.html.heex b/priv/templates/phx.gen.live/index.html.heex
index e58fb4a19..95f659170 100644
--- a/priv/templates/phx.gen.live/index.html.heex
+++ b/priv/templates/phx.gen.live/index.html.heex
@@ -7,7 +7,12 @@
   </:actions>
 </.header>
 
-<.table id="<%= schema.plural %>" rows={@<%= schema.collection %>} row_click={&JS.navigate(~p"<%= schema.route_prefix %>/#{&1}")}><%= for {k, _} <- schema.attrs do %>
+<.table
+  id="<%= schema.plural %>"
+  rows={@<%= schema.collection %>}
+  row_id={&"<%= schema.plural %>-#{&1.id}"}
+  row_click={&JS.navigate(~p"<%= schema.route_prefix %>/#{&1}")}
+><%= for {k, _} <- schema.attrs do %>
   <:col :let={<%= schema.singular %>} label="<%= Phoenix.Naming.humanize(Atom.to_string(k)) %>"><%%= <%= schema.singular %>.<%= k %> %></:col><% end %>
   <:action :let={<%= schema.singular %>}>
     <div class="sr-only">


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