Repository: phoenixframework/phoenix
Pull request #5900 (2024-08-14 08:44:31)

Simplified live generator
Simplifies the `phx.gen.live` generator with the following changes:

* Replaces FormComponent with FormLive to handle `/new` and `/:id/edit` routes.
* Removes the `:id/show/edit` route. FormLive accepts a `?return_to=index|show` param to negotiate live navigation after successful form submit.
* Removes the `<.modal>` component and its helper functions as they are no longer used.


Patch:
diff --git a/guides/asset_management.md b/guides/asset_management.md
index 0533bac02..b789ee177 100644
--- a/guides/asset_management.md
+++ b/guides/asset_management.md
@@ -76,7 +76,7 @@ If you need to reference other directories, you need to update the arguments abo
 
 Phoenix's default configuration of `esbuild` (via the Elixir wrapper) does not allow you to use [esbuild plugins](https://esbuild.github.io/plugins/). If you want to use an esbuild plugin, for example to compile SASS files to CSS, you can replace the default build system with a custom build script.
 
-The following is an example of a custom build using esbuild via Node.JS. First of all, you'll need to install Node.js in development and make it available for your production build step.
+The following is an example of a custom build using esbuild via Node.js. First of all, you'll need to install Node.js in development and make it available for your production build step.
 
 Then you'll need to add `esbuild` to your Node.js packages and the Phoenix packages. Inside the `assets` directory, run:
 
diff --git a/guides/authentication/mix_phx_gen_auth.md b/guides/authentication/mix_phx_gen_auth.md
index a5776f39a..952cca8d1 100644
--- a/guides/authentication/mix_phx_gen_auth.md
+++ b/guides/authentication/mix_phx_gen_auth.md
@@ -94,7 +94,7 @@ Note that whenever the password changes (either via reset password or directly),
 
 ### User Enumeration attacks
 
-A user enumeration attack allows someone to check if an email is registered in the application. The generated authentication code does not attempt to protect from such checks. For instance, when you register an account, if the email is already registered, the code will notify the user the email is already registered.
+A user enumeration attack allows someone to check if an email is registered in the application. The generated authentication code does not attempt to protect from such attacks. For instance, when you register an account, if the email is already registered, the code will notify the user the email is already registered.
 
 If your application is sensitive to enumeration attacks, you need to implement your own workflows, which tends to be very different from most applications, as you need to carefully balance security and user experience.
 
diff --git a/guides/real_time/channels.md b/guides/real_time/channels.md
index 99259abd0..f2b12fb23 100644
--- a/guides/real_time/channels.md
+++ b/guides/real_time/channels.md
@@ -410,7 +410,7 @@ Now our `conn.assigns` contains the `current_user` and `user_token`.
 
 ### Step 2 - Pass the Token to the JavaScript
 
-Next, we need to pass this token to JavaScript. We can do so inside a script tag in `lib/hello_web/components/layouts/app.html.heex` right above the app.js script, as follows:
+Next, we need to pass this token to JavaScript. We can do so inside a script tag in `lib/hello_web/components/layouts/root.html.heex` right above the app.js script, as follows:
 
 ```heex
 <script>window.userToken = "<%= assigns[:user_token] %>";</script>
diff --git a/installer/templates/phx_web/components/core_components.ex b/installer/templates/phx_web/components/core_components.ex
index cb4a7e074..1f382c5fb 100644
--- a/installer/templates/phx_web/components/core_components.ex
+++ b/installer/templates/phx_web/components/core_components.ex
@@ -3,8 +3,8 @@ defmodule <%= @web_namespace %>.CoreComponents do
   Provides core UI components.
 
   At first glance, this module may seem daunting, but its goal is to provide
-  core building blocks for your application, such as modals, tables, and
-  forms. The components consist mostly of markup and are well-documented
+  core building blocks for your application, such as tables, forms, and
+  inputs. The components consist mostly of markup and are well-documented
   with doc strings and declarative assigns. You may customize and style
   them in any way you want, based on your application growth and needs.
 
@@ -19,76 +19,6 @@ defmodule <%= @web_namespace %>.CoreComponents do
   alias Phoenix.LiveView.JS<%= if @gettext do %>
   import <%= @web_namespace %>.Gettext<% end %>
 
-  @doc """
-  Renders a modal.
-
-  ## Examples
-
-      <.modal id="confirm-modal">
-        This is a modal.
-      </.modal>
-
-  JS commands may be passed to the `:on_cancel` to configure
-  the closing/cancel event, for example:
-
-      <.modal id="confirm" on_cancel={JS.navigate(~p"/posts")}>
-        This is another modal.
-      </.modal>
-
-  """
-  attr :id, :string, required: true
-  attr :show, :boolean, default: false
-  attr :on_cancel, JS, default: %JS{}
-  slot :inner_block, required: true
-
-  def modal(assigns) do
-    ~H"""
-    <div
-      id={@id}
-      phx-mounted={@show && show_modal(@id)}
-      phx-remove={hide_modal(@id)}
-      data-cancel={JS.exec(@on_cancel, "phx-remove")}
-      class="relative z-50 hidden"
-    >
-      <div id={"#{@id}-bg"} class="bg-zinc-50/90 fixed inset-0 transition-opacity" aria-hidden="true" />
-      <div
-        class="fixed inset-0 overflow-y-auto"
-        aria-labelledby={"#{@id}-title"}
-        aria-describedby={"#{@id}-description"}
-        role="dialog"
-        aria-modal="true"
-        tabindex="0"
-      >
-        <div class="flex min-h-full items-center justify-center">
-          <div class="w-full max-w-3xl p-4 sm:p-6 lg:py-8">
-            <.focus_wrap
-              id={"#{@id}-container"}
-              phx-window-keydown={JS.exec("data-cancel", to: "##{@id}")}
-              phx-key="escape"
-              phx-click-away={JS.exec("data-cancel", to: "##{@id}")}
-              class="shadow-zinc-700/10 ring-zinc-700/10 relative hidden rounded-2xl bg-white p-14 shadow-lg ring-1 transition"
-            >
-              <div class="absolute top-6 right-5">
-                <button
-                  phx-click={JS.exec("data-cancel", to: "##{@id}")}
-                  type="button"
-                  class="-m-3 flex-none p-3 opacity-20 hover:opacity-40"
-                  aria-label=<%= maybe_heex_attr_gettext.("close", @gettext) %>
-                >
-                  <.icon name="hero-x-mark-solid" class="h-5 w-5" />
-                </button>
-              </div>
-              <div id={"#{@id}-content"}>
-                <%%= render_slot(@inner_block) %>
-              </div>
-            </.focus_wrap>
-          </div>
-        </div>
-      </div>
-    </div>
-    """
-  end
-
   @doc """
   Renders flash notices.
 
@@ -620,31 +550,6 @@ defmodule <%= @web_namespace %>.CoreComponents do
     )
   end
 
-  def show_modal(js \\ %JS{}, id) when is_binary(id) do
-    js
-    |> JS.show(to: "##{id}")
-    |> JS.show(
-      to: "##{id}-bg",
-      time: 300,
-      transition: {"transition-all ease-out duration-300", "opacity-0", "opacity-100"}
-    )
-    |> show("##{id}-container")
-    |> JS.add_class("overflow-hidden", to: "body")
-    |> JS.focus_first(to: "##{id}-content")
-  end
-
-  def hide_modal(js \\ %JS{}, id) do
-    js
-    |> JS.hide(
-      to: "##{id}-bg",
-      transition: {"transition-all ease-in duration-200", "opacity-100", "opacity-0"}
-    )
-    |> hide("##{id}-container")
-    |> JS.hide(to: "##{id}", transition: {"block", "block", "hidden"})
-    |> JS.remove_class("overflow-hidden", to: "body")
-    |> JS.pop_focus()
-  end
-
   @doc """
   Translates an error message using gettext.
   """<%= if @gettext do %>
diff --git a/lib/mix/phoenix/schema.ex b/lib/mix/phoenix/schema.ex
index 8c22ea6c2..4216d65f5 100644
--- a/lib/mix/phoenix/schema.ex
+++ b/lib/mix/phoenix/schema.ex
@@ -210,7 +210,7 @@ defmodule Mix.Phoenix.Schema do
   @doc """
   Converts the given value to map format when it is a date, time, datetime or naive_datetime.
 
-  Since `form_component.html.heex` generated by the live generator uses selects for dates and/or
+  Since `form.html.heex` generated by the live generator uses selects for dates and/or
   times, fixtures must use map format for those fields in order to submit the live form.
   """
   def live_form_value(%Date{} = date), do: Calendar.strftime(date, "%Y-%m-%d")
diff --git a/lib/mix/tasks/phx.gen.live.ex b/lib/mix/tasks/phx.gen.live.ex
index 825b27190..59a51514d 100644
--- a/lib/mix/tasks/phx.gen.live.ex
+++ b/lib/mix/tasks/phx.gen.live.ex
@@ -19,9 +19,9 @@ defmodule Mix.Tasks.Phx.Gen.Live do
   types.  See `mix help phx.gen.schema` for more information on attributes.
 
   When this command is run for the first time, a `Components` module will be
-  created if it does not exist, along with the resource level LiveViews and
-  components, including `UserLive.Index`, `UserLive.Show`, and
-  `UserLive.FormComponent` modules for the new resource.
+  created if it does not exist, along with the resource level LiveViews,
+  including `UserLive.Index`, `UserLive.Show`, and `UserLive.Form` modules for
+  the new resource.
 
   > Note: A resource may also be split
   > over distinct contexts (such as `Accounts.User` and `Payments.User`).
@@ -32,8 +32,8 @@ defmodule Mix.Tasks.Phx.Gen.Live do
     * a schema in `lib/app/accounts/user.ex`, with a `users` table
     * a LiveView in `lib/app_web/live/user_live/show.ex`
     * a LiveView in `lib/app_web/live/user_live/index.ex`
-    * a LiveComponent in `lib/app_web/live/user_live/form_component.ex`
-    * a helpers module in `lib/app_web/live/live_helpers.ex` with a modal
+    * a LiveView in `lib/app_web/live/user_live/form.ex`
+    * a components module in `lib/app_web/components/core_components.ex`
 
   After file generation is complete, there will be output regarding required
   updates to the `lib/app_web/router.ex` file.
@@ -41,11 +41,9 @@ defmodule Mix.Tasks.Phx.Gen.Live do
       Add the live routes to your browser scope in lib/app_web/router.ex:
 
         live "/users", UserLive.Index, :index
-        live "/users/new", UserLive.Index, :new
-        live "/users/:id/edit", UserLive.Index, :edit
-
+        live "/users/new", UserLive.Form, :new
         live "/users/:id", UserLive.Show, :show
-        live "/users/:id/show/edit", UserLive.Show, :edit
+        live "/users/:id/edit", UserLive.Form, :edit
 
   ## The context app
 
@@ -147,9 +145,7 @@ defmodule Mix.Tasks.Phx.Gen.Live do
     [
       {:eex, "show.ex", Path.join(web_live, "show.ex")},
       {:eex, "index.ex", Path.join(web_live, "index.ex")},
-      {:eex, "form_component.ex", Path.join(web_live, "form_component.ex")},
-      {:eex, "index.html.heex", Path.join(web_live, "index.html.heex")},
-      {:eex, "show.html.heex", Path.join(web_live, "show.html.heex")},
+      {:eex, "form.ex", Path.join(web_live, "form.ex")},
       {: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"])}
@@ -262,10 +258,9 @@ defmodule Mix.Tasks.Phx.Gen.Live do
   defp live_route_instructions(schema) do
     [
       ~s|live "/#{schema.plural}", #{inspect(schema.alias)}Live.Index, :index\n|,
-      ~s|live "/#{schema.plural}/new", #{inspect(schema.alias)}Live.Index, :new\n|,
-      ~s|live "/#{schema.plural}/:id/edit", #{inspect(schema.alias)}Live.Index, :edit\n\n|,
+      ~s|live "/#{schema.plural}/new", #{inspect(schema.alias)}Live.Form, :new\n|,
       ~s|live "/#{schema.plural}/:id", #{inspect(schema.alias)}Live.Show, :show\n|,
-      ~s|live "/#{schema.plural}/:id/show/edit", #{inspect(schema.alias)}Live.Show, :edit|
+      ~s|live "/#{schema.plural}/:id/edit", #{inspect(schema.alias)}Live.Form, :edit|
     ]
   end
 
diff --git a/priv/templates/phx.gen.live/core_components.ex b/priv/templates/phx.gen.live/core_components.ex
index cb4a7e074..1f382c5fb 100644
--- a/priv/templates/phx.gen.live/core_components.ex
+++ b/priv/templates/phx.gen.live/core_components.ex
@@ -3,8 +3,8 @@ defmodule <%= @web_namespace %>.CoreComponents do
   Provides core UI components.
 
   At first glance, this module may seem daunting, but its goal is to provide
-  core building blocks for your application, such as modals, tables, and
-  forms. The components consist mostly of markup and are well-documented
+  core building blocks for your application, such as tables, forms, and
+  inputs. The components consist mostly of markup and are well-documented
   with doc strings and declarative assigns. You may customize and style
   them in any way you want, based on your application growth and needs.
 
@@ -19,76 +19,6 @@ defmodule <%= @web_namespace %>.CoreComponents do
   alias Phoenix.LiveView.JS<%= if @gettext do %>
   import <%= @web_namespace %>.Gettext<% end %>
 
-  @doc """
-  Renders a modal.
-
-  ## Examples
-
-      <.modal id="confirm-modal">
-        This is a modal.
-      </.modal>
-
-  JS commands may be passed to the `:on_cancel` to configure
-  the closing/cancel event, for example:
-
-      <.modal id="confirm" on_cancel={JS.navigate(~p"/posts")}>
-        This is another modal.
-      </.modal>
-
-  """
-  attr :id, :string, required: true
-  attr :show, :boolean, default: false
-  attr :on_cancel, JS, default: %JS{}
-  slot :inner_block, required: true
-
-  def modal(assigns) do
-    ~H"""
-    <div
-      id={@id}
-      phx-mounted={@show && show_modal(@id)}
-      phx-remove={hide_modal(@id)}
-      data-cancel={JS.exec(@on_cancel, "phx-remove")}
-      class="relative z-50 hidden"
-    >
-      <div id={"#{@id}-bg"} class="bg-zinc-50/90 fixed inset-0 transition-opacity" aria-hidden="true" />
-      <div
-        class="fixed inset-0 overflow-y-auto"
-        aria-labelledby={"#{@id}-title"}
-        aria-describedby={"#{@id}-description"}
-        role="dialog"
-        aria-modal="true"
-        tabindex="0"
-      >
-        <div class="flex min-h-full items-center justify-center">
-          <div class="w-full max-w-3xl p-4 sm:p-6 lg:py-8">
-            <.focus_wrap
-              id={"#{@id}-container"}
-              phx-window-keydown={JS.exec("data-cancel", to: "##{@id}")}
-              phx-key="escape"
-              phx-click-away={JS.exec("data-cancel", to: "##{@id}")}
-              class="shadow-zinc-700/10 ring-zinc-700/10 relative hidden rounded-2xl bg-white p-14 shadow-lg ring-1 transition"
-            >
-              <div class="absolute top-6 right-5">
-                <button
-                  phx-click={JS.exec("data-cancel", to: "##{@id}")}
-                  type="button"
-                  class="-m-3 flex-none p-3 opacity-20 hover:opacity-40"
-                  aria-label=<%= maybe_heex_attr_gettext.("close", @gettext) %>
-                >
-                  <.icon name="hero-x-mark-solid" class="h-5 w-5" />
-                </button>
-              </div>
-              <div id={"#{@id}-content"}>
-                <%%= render_slot(@inner_block) %>
-              </div>
-            </.focus_wrap>
-          </div>
-        </div>
-      </div>
-    </div>
-    """
-  end
-
   @doc """
   Renders flash notices.
 
@@ -620,31 +550,6 @@ defmodule <%= @web_namespace %>.CoreComponents do
     )
   end
 
-  def show_modal(js \\ %JS{}, id) when is_binary(id) do
-    js
-    |> JS.show(to: "##{id}")
-    |> JS.show(
-      to: "##{id}-bg",
-      time: 300,
-      transition: {"transition-all ease-out duration-300", "opacity-0", "opacity-100"}
-    )
-    |> show("##{id}-container")
-    |> JS.add_class("overflow-hidden", to: "body")
-    |> JS.focus_first(to: "##{id}-content")
-  end
-
-  def hide_modal(js \\ %JS{}, id) do
-    js
-    |> JS.hide(
-      to: "##{id}-bg",
-      transition: {"transition-all ease-in duration-200", "opacity-100", "opacity-0"}
-    )
-    |> hide("##{id}-container")
-    |> JS.hide(to: "##{id}", transition: {"block", "block", "hidden"})
-    |> JS.remove_class("overflow-hidden", to: "body")
-    |> JS.pop_focus()
-  end
-
   @doc """
   Translates an error message using gettext.
   """<%= if @gettext do %>
diff --git a/priv/templates/phx.gen.live/form.ex b/priv/templates/phx.gen.live/form.ex
new file mode 100644
index 000000000..25627a4e3
--- /dev/null
+++ b/priv/templates/phx.gen.live/form.ex
@@ -0,0 +1,93 @@
+defmodule <%= inspect context.web_module %>.<%= inspect Module.concat(schema.web_namespace, schema.alias) %>Live.Form do
+  use <%= inspect context.web_module %>, :live_view
+
+  alias <%= inspect context.module %>
+  alias <%= inspect schema.module %>
+
+  @impl true
+  def render(assigns) do
+    ~H"""
+    <.header>
+      <%%= @page_title %>
+      <:subtitle>Use this form to manage <%= schema.singular %> records in your database.</:subtitle>
+    </.header>
+
+    <.simple_form for={@form} id="<%= schema.singular %>-form" phx-change="validate" phx-submit="save">
+<%= Mix.Tasks.Phx.Gen.Html.indent_inputs(inputs, 6) %>
+      <:actions>
+        <.button phx-disable-with="Saving...">Save <%= schema.human_singular %></.button>
+      </:actions>
+    </.simple_form>
+
+    <.back navigate={return_path(@return_to, @<%= schema.singular %>)}>Back</.back>
+    """
+  end
+
+  @impl true
+  def mount(params, _session, socket) do
+    {:ok,
+     socket
+     |> assign(:return_to, return_to(params["return_to"]))
+     |> apply_action(socket.assigns.live_action, params)}
+  end
+
+  defp return_to("show"), do: "show"
+  defp return_to(_), do: "index"
+
+  defp apply_action(socket, :edit, %{"id" => id}) do
+    <%= schema.singular %> = <%= inspect context.alias %>.get_<%= schema.singular %>!(id)
+
+    socket
+    |> assign(:page_title, "Edit <%= schema.human_singular %>")
+    |> assign(:<%= schema.singular %>, <%= schema.singular %>)
+    |> assign(:form, to_form(<%= inspect context.alias %>.change_<%= schema.singular %>(<%= schema.singular %>)))
+  end
+
+  defp apply_action(socket, :new, _params) do
+    <%= schema.singular %> = %<%= inspect schema.alias %>{}
+
+    socket
+    |> assign(:page_title, "New <%= schema.human_singular %>")
+    |> assign(:<%= schema.singular %>, <%= schema.singular %>)
+    |> assign(:form, to_form(<%= inspect context.alias %>.change_<%= schema.singular %>(<%= schema.singular %>)))
+  end
+
+  @impl true
+  def handle_event("validate", %{"<%= schema.singular %>" => <%= schema.singular %>_params}, socket) do
+    changeset = <%= inspect context.alias %>.change_<%= schema.singular %>(socket.assigns.<%= schema.singular %>, <%= schema.singular %>_params)
+    {:noreply, assign(socket, form: to_form(changeset, action: :validate))}
+  end
+
+  def handle_event("save", %{"<%= schema.singular %>" => <%= schema.singular %>_params}, socket) do
+    save_<%= schema.singular %>(socket, socket.assigns.live_action, <%= schema.singular %>_params)
+  end
+
+  defp save_<%= schema.singular %>(socket, :edit, <%= schema.singular %>_params) do
+    case <%= inspect context.alias %>.update_<%= schema.singular %>(socket.assigns.<%= schema.singular %>, <%= schema.singular %>_params) do
+      {:ok, <%= schema.singular %>} ->
+        {:noreply,
+         socket
+         |> put_flash(:info, "<%= schema.human_singular %> updated successfully")
+         |> push_navigate(to: return_path(socket.assigns.return_to, <%= schema.singular %>))}
+
+      {:error, %Ecto.Changeset{} = changeset} ->
+        {:noreply, assign(socket, form: to_form(changeset))}
+    end
+  end
+
+  defp save_<%= schema.singular %>(socket, :new, <%= schema.singular %>_params) do
+    case <%= inspect context.alias %>.create_<%= schema.singular %>(<%= schema.singular %>_params) do
+      {:ok, <%= schema.singular %>} ->
+        {:noreply,
+         socket
+         |> put_flash(:info, "<%= schema.human_singular %> created successfully")
+         |> push_navigate(to: return_path(socket.assigns.return_to, <%= schema.singular %>))}
+
+      {:error, %Ecto.Changeset{} = changeset} ->
+        {:noreply, assign(socket, form: to_form(changeset))}
+    end
+  end
+
+  defp return_path("index", _<%= schema.singular %>), do: ~p"<%= schema.route_prefix %>"
+  defp return_path("show", <%= schema.singular %>), do: ~p"<%= schema.route_prefix %>/#{<%= schema.singular %>}"
+end
diff --git a/priv/templates/phx.gen.live/form_component.ex b/priv/templates/phx.gen.live/form_component.ex
deleted file mode 100644
index e3ab17cb2..000000000
--- a/priv/templates/phx.gen.live/form_component.ex
+++ /dev/null
@@ -1,82 +0,0 @@
-defmodule <%= inspect context.web_module %>.<%= inspect Module.concat(schema.web_namespace, schema.alias) %>Live.FormComponent do
-  use <%= inspect context.web_module %>, :live_component
-
-  alias <%= inspect context.module %>
-
-  @impl true
-  def render(assigns) do
-    ~H"""
-    <div>
-      <.header>
-        <%%= @title %>
-        <:subtitle>Use this form to manage <%= schema.singular %> records in your database.</:subtitle>
-      </.header>
-
-      <.simple_form
-        for={@form}
-        id="<%= schema.singular %>-form"
-        phx-target={@myself}
-        phx-change="validate"
-        phx-submit="save"
-      >
-<%= Mix.Tasks.Phx.Gen.Html.indent_inputs(inputs, 8) %>
-        <:actions>
-          <.button phx-disable-with="Saving...">Save <%= schema.human_singular %></.button>
-        </:actions>
-      </.simple_form>
-    </div>
-    """
-  end
-
-  @impl true
-  def update(%{<%= schema.singular %>: <%= schema.singular %>} = assigns, socket) do
-    {:ok,
-     socket
-     |> assign(assigns)
-     |> assign_new(:form, fn ->
-       to_form(<%= inspect context.alias %>.change_<%= schema.singular %>(<%= schema.singular %>))
-     end)}
-  end
-
-  @impl true
-  def handle_event("validate", %{"<%= schema.singular %>" => <%= schema.singular %>_params}, socket) do
-    changeset = <%= inspect context.alias %>.change_<%= schema.singular %>(socket.assigns.<%= schema.singular %>, <%= schema.singular %>_params)
-    {:noreply, assign(socket, form: to_form(changeset, action: :validate))}
-  end
-
-  def handle_event("save", %{"<%= schema.singular %>" => <%= schema.singular %>_params}, socket) do
-    save_<%= schema.singular %>(socket, socket.assigns.action, <%= schema.singular %>_params)
-  end
-
-  defp save_<%= schema.singular %>(socket, :edit, <%= schema.singular %>_params) do
-    case <%= inspect context.alias %>.update_<%= schema.singular %>(socket.assigns.<%= schema.singular %>, <%= schema.singular %>_params) do
-      {:ok, <%= schema.singular %>} ->
-        notify_parent({:saved, <%= schema.singular %>})
-
-        {:noreply,
-         socket
-         |> put_flash(:info, "<%= schema.human_singular %> updated successfully")
-         |> push_patch(to: socket.assigns.patch)}
-
-      {:error, %Ecto.Changeset{} = changeset} ->
-        {:noreply, assign(socket, form: to_form(changeset))}
-    end
-  end
-
-  defp save_<%= schema.singular %>(socket, :new, <%= schema.singular %>_params) do
-    case <%= inspect context.alias %>.create_<%= schema.singular %>(<%= schema.singular %>_params) do
-      {:ok, <%= schema.singular %>} ->
-        notify_parent({:saved, <%= schema.singular %>})
-
-        {:noreply,
-         socket
-         |> put_flash(:info, "<%= schema.human_singular %> created successfully")
-         |> push_patch(to: socket.assigns.patch)}
-
-      {:error, %Ecto.Changeset{} = changeset} ->
-        {:noreply, assign(socket, form: to_form(changeset))}
-    end
-  end
-
-  defp notify_parent(msg), do: send(self(), {__MODULE__, msg})
-end
diff --git a/priv/templates/phx.gen.live/index.ex b/priv/templates/phx.gen.live/index.ex
index b4658eeca..d6055a669 100644
--- a/priv/templates/phx.gen.live/index.ex
+++ b/priv/templates/phx.gen.live/index.ex
@@ -2,39 +2,49 @@ defmodule <%= inspect context.web_module %>.<%= inspect Module.concat(schema.web
   use <%= inspect context.web_module %>, :live_view
 
   alias <%= inspect context.module %>
-  alias <%= inspect schema.module %>
 
   @impl true
-  def mount(_params, _session, socket) do
-    {:ok, stream(socket, :<%= schema.collection %>, <%= inspect context.alias %>.list_<%= schema.plural %>())}
+  def render(assigns) do
+    ~H"""
+    <.header>
+      Listing <%= schema.human_plural %>
+      <:actions>
+        <.link navigate={~p"<%= schema.route_prefix %>/new"}>
+          <.button>New <%= schema.human_singular %></.button>
+        </.link>
+      </:actions>
+    </.header>
+
+    <.table
+      id="<%= schema.plural %>"
+      rows={@streams.<%= schema.collection %>}
+      row_click={fn {_id, <%= schema.singular %>} -> JS.navigate(~p"<%= schema.route_prefix %>/#{<%= schema.singular %>}") end}
+    ><%= for {k, _} <- schema.attrs do %>
+      <:col :let={{_id, <%= schema.singular %>}} label="<%= Phoenix.Naming.humanize(Atom.to_string(k)) %>"><%%= <%= schema.singular %>.<%= k %> %></:col><% end %>
+      <:action :let={{_id, <%= schema.singular %>}}>
+        <div class="sr-only">
+          <.link navigate={~p"<%= schema.route_prefix %>/#{<%= schema.singular %>}"}>Show</.link>
+        </div>
+        <.link navigate={~p"<%= schema.route_prefix %>/#{<%= schema.singular %>}/edit"}>Edit</.link>
+      </:action>
+      <:action :let={{id, <%= schema.singular %>}}>
+        <.link
+          phx-click={JS.push("delete", value: %{id: <%= schema.singular %>.id}) |> hide("##{id}")}
+          data-confirm="Are you sure?"
+        >
+          Delete
+        </.link>
+      </:action>
+    </.table>
+    """
   end
 
   @impl true
-  def handle_params(params, _url, socket) do
-    {:noreply, apply_action(socket, socket.assigns.live_action, params)}
-  end
-
-  defp apply_action(socket, :edit, %{"id" => id}) do
-    socket
-    |> assign(:page_title, "Edit <%= schema.human_singular %>")
-    |> assign(:<%= schema.singular %>, <%= inspect context.alias %>.get_<%= schema.singular %>!(id))
-  end
-
-  defp apply_action(socket, :new, _params) do
-    socket
-    |> assign(:page_title, "New <%= schema.human_singular %>")
-    |> assign(:<%= schema.singular %>, %<%= inspect schema.alias %>{})
-  end
-
-  defp apply_action(socket, :index, _params) do
-    socket
-    |> assign(:page_title, "Listing <%= schema.human_plural %>")
-    |> assign(:<%= schema.singular %>, nil)
-  end
-
-  @impl true
-  def handle_info({<%= inspect context.web_module %>.<%= inspect Module.concat(schema.web_namespace, schema.alias) %>Live.FormComponent, {:saved, <%= schema.singular %>}}, socket) do
-    {:noreply, stream_insert(socket, :<%= schema.collection %>, <%= schema.singular %>)}
+  def mount(_params, _session, socket) do
+    {:ok,
+     socket
+     |> assign(:page_title, "Listing <%= schema.human_plural %>")
+     |> stream(:<%= schema.collection %>, <%= inspect context.alias %>.list_<%= schema.plural %>())}
   end
 
   @impl true
diff --git a/priv/templates/phx.gen.live/index.html.heex b/priv/templates/phx.gen.live/index.html.heex
deleted file mode 100644
index 872db4d36..000000000
--- a/priv/templates/phx.gen.live/index.html.heex
+++ /dev/null
@@ -1,41 +0,0 @@
-<.header>
-  Listing <%= schema.human_plural %>
-  <:actions>
-    <.link patch={~p"<%= schema.route_prefix %>/new"}>
-      <.button>New <%= schema.human_singular %></.button>
-    </.link>
-  </:actions>
-</.header>
-
-<.table
-  id="<%= schema.plural %>"
-  rows={@streams.<%= schema.collection %>}
-  row_click={fn {_id, <%= schema.singular %>} -> JS.navigate(~p"<%= schema.route_prefix %>/#{<%= schema.singular %>}") end}
-><%= for {k, _} <- schema.attrs do %>
-  <:col :let={{_id, <%= schema.singular %>}} label="<%= Phoenix.Naming.humanize(Atom.to_string(k)) %>"><%%= <%= schema.singular %>.<%= k %> %></:col><% end %>
-  <:action :let={{_id, <%= schema.singular %>}}>
-    <div class="sr-only">
-      <.link navigate={~p"<%= schema.route_prefix %>/#{<%= schema.singular %>}"}>Show</.link>
-    </div>
-    <.link patch={~p"<%= schema.route_prefix %>/#{<%= schema.singular %>}/edit"}>Edit</.link>
-  </:action>
-  <:action :let={{id, <%= schema.singular %>}}>
-    <.link
-      phx-click={JS.push("delete", value: %{id: <%= schema.singular %>.id}) |> hide("##{id}")}
-      data-confirm="Are you sure?"
-    >
-      Delete
-    </.link>
-  </:action>
-</.table>
-
-<.modal :if={@live_action in [:new, :edit]} id="<%= schema.singular %>-modal" show on_cancel={JS.patch(~p"<%= schema.route_prefix %>")}>
-  <.live_component
-    module={<%= inspect context.web_module %>.<%= inspect Module.concat(schema.web_namespace, schema.alias) %>Live.FormComponent}
-    id={@<%= schema.singular %>.id || :new}
-    title={@page_title}
-    action={@live_action}
-    <%= schema.singular %>={@<%= schema.singular %>}
-    patch={~p"<%= schema.route_prefix %>"}
-  />
-</.modal>
diff --git a/priv/templates/phx.gen.live/show.ex b/priv/templates/phx.gen.live/show.ex
index b75badea6..6d6e1ac27 100644
--- a/priv/templates/phx.gen.live/show.ex
+++ b/priv/templates/phx.gen.live/show.ex
@@ -3,6 +3,27 @@ defmodule <%= inspect context.web_module %>.<%= inspect Module.concat(schema.web
 
   alias <%= inspect context.module %>
 
+  @impl true
+  def render(assigns) do
+    ~H"""
+    <.header>
+      <%= schema.human_singular %> <%%= @<%= schema.singular %>.id %>
+      <:subtitle>This is a <%= schema.singular %> record from your database.</:subtitle>
+      <:actions>
+        <.link navigate={~p"<%= schema.route_prefix %>/#{@<%= schema.singular %>}/edit?return_to=show"}>
+          <.button>Edit <%= schema.singular %></.button>
+        </.link>
+      </:actions>
+    </.header>
+
+    <.list><%= for {k, _} <- schema.attrs do %>
+      <:item title="<%= Phoenix.Naming.humanize(Atom.to_string(k)) %>"><%%= @<%= schema.singular %>.<%= k %> %></:item><% end %>
+    </.list>
+
+    <.back navigate={~p"<%= schema.route_prefix %>"}>Back to <%= schema.plural %></.back>
+    """
+  end
+
   @impl true
   def mount(_params, _session, socket) do
     {:ok, socket}
@@ -12,10 +33,7 @@ defmodule <%= inspect context.web_module %>.<%= inspect Module.concat(schema.web
   def handle_params(%{"id" => id}, _, socket) do
     {:noreply,
      socket
-     |> assign(:page_title, page_title(socket.assigns.live_action))
+     |> assign(:page_title, "Show <%= schema.human_singular %>")
      |> assign(:<%= schema.singular %>, <%= inspect context.alias %>.get_<%= schema.singular %>!(id))}
   end
-
-  defp page_title(:show), do: "Show <%= schema.human_singular %>"
-  defp page_title(:edit), do: "Edit <%= schema.human_singular %>"
 end
diff --git a/priv/templates/phx.gen.live/show.html.heex b/priv/templates/phx.gen.live/show.html.heex
deleted file mode 100644
index f3628a209..000000000
--- a/priv/templates/phx.gen.live/show.html.heex
+++ /dev/null
@@ -1,26 +0,0 @@
-<.header>
-  <%= schema.human_singular %> <%%= @<%= schema.singular %>.id %>
-  <:subtitle>This is a <%= schema.singular %> record from your database.</:subtitle>
-  <:actions>
-    <.link patch={~p"<%= schema.route_prefix %>/#{@<%= schema.singular %>}/show/edit"} phx-click={JS.push_focus()}>
-      <.button>Edit <%= schema.singular %></.button>
-    </.link>
-  </:actions>
-</.header>
-
-<.list><%= for {k, _} <- schema.attrs do %>
-  <:item title="<%= Phoenix.Naming.humanize(Atom.to_string(k)) %>"><%%= @<%= schema.singular %>.<%= k %> %></:item><% end %>
-</.list>
-
-<.back navigate={~p"<%= schema.route_prefix %>"}>Back to <%= schema.plural %></.back>
-
-<.modal :if={@live_action == :edit} id="<%= schema.singular %>-modal" show on_cancel={JS.patch(~p"<%= schema.route_prefix %>/#{@<%= schema.singular %>}")}>
-  <.live_component
-    module={<%= inspect context.web_module %>.<%= inspect Module.concat(schema.web_namespace, schema.alias) %>Live.FormComponent}
-    id={@<%= schema.singular %>.id}
-    title={@page_title}
-    action={@live_action}
-    <%= schema.singular %>={@<%= schema.singular %>}
-    patch={~p"<%= schema.route_prefix %>/#{@<%= schema.singular %>}"}
-  />
-</.modal>
diff --git a/priv/templates/phx.gen.socket/socket.js b/priv/templates/phx.gen.socket/socket.js
index dd178f483..80e8453e2 100644
--- a/priv/templates/phx.gen.socket/socket.js
+++ b/priv/templates/phx.gen.socket/socket.js
@@ -32,7 +32,7 @@ let socket = new Socket("/socket", {params: {token: window.userToken}})
 //     end
 //
 // Now you need to pass this token to JavaScript. You can do so
-// inside a script tag in "<%= web_prefix %>/templates/layout/app.html.heex":
+// inside a script tag in "<%= web_prefix %>/templates/layout/root.html.heex":
 //
 //     <script>window.userToken = "<%%= assigns[:user_token] %>";</script>
 //


Test patch:
diff --git a/integration_test/test/code_generation/app_with_defaults_test.exs b/integration_test/test/code_generation/app_with_defaults_test.exs
index 9796a983e..57d069f79 100644
--- a/integration_test/test/code_generation/app_with_defaults_test.exs
+++ b/integration_test/test/code_generation/app_with_defaults_test.exs
@@ -130,11 +130,9 @@ defmodule Phoenix.Integration.CodeGeneration.AppWithDefaultsTest do
               pipe_through [:browser]
 
               live "/posts", PostLive.Index, :index
-              live "/posts/new", PostLive.Index, :new
-              live "/posts/:id/edit", PostLive.Index, :edit
-
+              live "/posts/new", PostLive.Form, :new
               live "/posts/:id", PostLive.Show, :show
-              live "/posts/:id/show/edit", PostLive.Show, :edit
+              live "/posts/:id/edit", PostLive.Form, :edit
             end
           """)
         end)
@@ -158,11 +156,9 @@ defmodule Phoenix.Integration.CodeGeneration.AppWithDefaultsTest do
               pipe_through [:browser]
 
               live "/posts", PostLive.Index, :index
-              live "/posts/new", PostLive.Index, :new
-              live "/posts/:id/edit", PostLive.Index, :edit
-
+              live "/posts/new", PostLive.Form, :new
               live "/posts/:id", PostLive.Show, :show
-              live "/posts/:id/show/edit", PostLive.Show, :edit
+              live "/posts/:id/edit", PostLive.Form, :edit
             end
           """)
         end)
diff --git a/integration_test/test/code_generation/app_with_mssql_adapter_test.exs b/integration_test/test/code_generation/app_with_mssql_adapter_test.exs
index 7d6126dd5..96ca203de 100644
--- a/integration_test/test/code_generation/app_with_mssql_adapter_test.exs
+++ b/integration_test/test/code_generation/app_with_mssql_adapter_test.exs
@@ -69,11 +69,9 @@ defmodule Phoenix.Integration.CodeGeneration.AppWithMSSQLAdapterTest do
               pipe_through [:browser]
 
               live "/posts", PostLive.Index, :index
-              live "/posts/new", PostLive.Index, :new
-              live "/posts/:id/edit", PostLive.Index, :edit
-
+              live "/posts/new", PostLive.Form, :new
               live "/posts/:id", PostLive.Show, :show
-              live "/posts/:id/show/edit", PostLive.Show, :edit
+              live "/posts/:id/edit", PostLive.Form, :edit
             end
           """)
         end)
diff --git a/integration_test/test/code_generation/app_with_mysql_adapter_test.exs b/integration_test/test/code_generation/app_with_mysql_adapter_test.exs
index f349bee77..0886d421f 100644
--- a/integration_test/test/code_generation/app_with_mysql_adapter_test.exs
+++ b/integration_test/test/code_generation/app_with_mysql_adapter_test.exs
@@ -69,11 +69,9 @@ defmodule Phoenix.Integration.CodeGeneration.AppWithMySqlAdapterTest do
               pipe_through [:browser]
 
               live "/posts", PostLive.Index, :index
-              live "/posts/new", PostLive.Index, :new
-              live "/posts/:id/edit", PostLive.Index, :edit
-
+              live "/posts/new", PostLive.Form, :new
               live "/posts/:id", PostLive.Show, :show
-              live "/posts/:id/show/edit", PostLive.Show, :edit
+              live "/posts/:id/edit", PostLive.Form, :edit
             end
           """)
         end)
diff --git a/integration_test/test/code_generation/app_with_sqlite3_adapter.exs b/integration_test/test/code_generation/app_with_sqlite3_adapter.exs
index d748d81bd..4576a6cfc 100644
--- a/integration_test/test/code_generation/app_with_sqlite3_adapter.exs
+++ b/integration_test/test/code_generation/app_with_sqlite3_adapter.exs
@@ -69,11 +69,9 @@ defmodule Phoenix.Integration.CodeGeneration.AppWithSQLite3AdapterTest do
               pipe_through [:browser]
 
               live "/posts", PostLive.Index, :index
-              live "/posts/new", PostLive.Index, :new
-              live "/posts/:id/edit", PostLive.Index, :edit
-
+              live "/posts/new", PostLive.Form, :new
               live "/posts/:id", PostLive.Show, :show
-              live "/posts/:id/show/edit", PostLive.Show, :edit
+              live "/posts/:id/edit", PostLive.Form, :edit
             end
           """)
         end)
diff --git a/integration_test/test/code_generation/umbrella_app_with_defaults_test.exs b/integration_test/test/code_generation/umbrella_app_with_defaults_test.exs
index ee7148db3..9dc71ba64 100644
--- a/integration_test/test/code_generation/umbrella_app_with_defaults_test.exs
+++ b/integration_test/test/code_generation/umbrella_app_with_defaults_test.exs
@@ -136,11 +136,9 @@ defmodule Phoenix.Integration.CodeGeneration.UmbrellaAppWithDefaultsTest do
               pipe_through [:browser]
 
               live "/posts", PostLive.Index, :index
-              live "/posts/new", PostLive.Index, :new
-              live "/posts/:id/edit", PostLive.Index, :edit
-
+              live "/posts/new", PostLive.Form, :new
               live "/posts/:id", PostLive.Show, :show
-              live "/posts/:id/show/edit", PostLive.Show, :edit
+              live "/posts/:id/edit", PostLive.Form, :edit
             end
           """)
         end)
@@ -165,11 +163,9 @@ defmodule Phoenix.Integration.CodeGeneration.UmbrellaAppWithDefaultsTest do
               pipe_through [:browser]
 
               live "/posts", PostLive.Index, :index
-              live "/posts/new", PostLive.Index, :new
-              live "/posts/:id/edit", PostLive.Index, :edit
-
+              live "/posts/new", PostLive.Form, :new
               live "/posts/:id", PostLive.Show, :show
-              live "/posts/:id/show/edit", PostLive.Show, :edit
+              live "/posts/:id/edit", PostLive.Form, :edit
             end
           """)
         end)
diff --git a/priv/templates/phx.gen.auth/confirmation_live_test.exs b/priv/templates/phx.gen.auth/confirmation_live_test.exs
index a206a394f..fc3112882 100644
--- a/priv/templates/phx.gen.auth/confirmation_live_test.exs
+++ b/priv/templates/phx.gen.auth/confirmation_live_test.exs
@@ -29,7 +29,7 @@ defmodule <%= inspect context.web_module %>.<%= inspect Module.concat(schema.web
         lv
         |> form("#confirmation_form")
         |> render_submit()
-        |> follow_redirect(conn, "/")
+        |> follow_redirect(conn, ~p"/")
 
       assert {:ok, conn} = result
 
@@ -47,7 +47,7 @@ defmodule <%= inspect context.web_module %>.<%= inspect Module.concat(schema.web
         lv
         |> form("#confirmation_form")
         |> render_submit()
-        |> follow_redirect(conn, "/")
+        |> follow_redirect(conn, ~p"/")
 
       assert {:ok, conn} = result
 
@@ -65,7 +65,7 @@ defmodule <%= inspect context.web_module %>.<%= inspect Module.concat(schema.web
         lv
         |> form("#confirmation_form")
         |> render_submit()
-        |> follow_redirect(conn, "/")
+        |> follow_redirect(conn, ~p"/")
 
       assert {:ok, conn} = result
       refute Phoenix.Flash.get(conn.assigns.flash, :error)
diff --git a/priv/templates/phx.gen.auth/forgot_password_live_test.exs b/priv/templates/phx.gen.auth/forgot_password_live_test.exs
index a249a694e..4ad1455f6 100644
--- a/priv/templates/phx.gen.auth/forgot_password_live_test.exs
+++ b/priv/templates/phx.gen.auth/forgot_password_live_test.exs
@@ -39,7 +39,7 @@ defmodule <%= inspect context.web_module %>.<%= inspect Module.concat(schema.web
         lv
         |> form("#reset_password_form", <%= schema.singular %>: %{"email" => <%= schema.singular %>.email})
         |> render_submit()
-        |> follow_redirect(conn, "/")
+        |> follow_redirect(conn, ~p"/")
 
       assert Phoenix.Flash.get(conn.assigns.flash, :info) =~ "If your email is in our system"
 
@@ -54,7 +54,7 @@ defmodule <%= inspect context.web_module %>.<%= inspect Module.concat(schema.web
         lv
         |> form("#reset_password_form", <%= schema.singular %>: %{"email" => "unknown@example.com"})
         |> render_submit()
-        |> follow_redirect(conn, "/")
+        |> follow_redirect(conn, ~p"/")
 
       assert Phoenix.Flash.get(conn.assigns.flash, :info) =~ "If your email is in our system"
       assert Repo.all(<%= inspect context.alias %>.<%= inspect schema.alias %>Token) == []
diff --git a/priv/templates/phx.gen.auth/login_live_test.exs b/priv/templates/phx.gen.auth/login_live_test.exs
index 4555254b9..6b9d14268 100644
--- a/priv/templates/phx.gen.auth/login_live_test.exs
+++ b/priv/templates/phx.gen.auth/login_live_test.exs
@@ -18,7 +18,7 @@ defmodule <%= inspect context.web_module %>.<%= inspect Module.concat(schema.web
         conn
         |> log_in_<%= schema.singular %>(<%= schema.singular %>_fixture())
         |> live(~p"<%= schema.route_prefix %>/log_in")
-        |> follow_redirect(conn, "/")
+        |> follow_redirect(conn, ~p"/")
 
       assert {:ok, _conn} = result
     end
diff --git a/priv/templates/phx.gen.auth/registration_live_test.exs b/priv/templates/phx.gen.auth/registration_live_test.exs
index 170d5ff60..1d1246819 100644
--- a/priv/templates/phx.gen.auth/registration_live_test.exs
+++ b/priv/templates/phx.gen.auth/registration_live_test.exs
@@ -17,7 +17,7 @@ defmodule <%= inspect context.web_module %>.<%= inspect Module.concat(schema.web
         conn
         |> log_in_<%= schema.singular %>(<%= schema.singular %>_fixture())
         |> live(~p"<%= schema.route_prefix %>/register")
-        |> follow_redirect(conn, "/")
+        |> follow_redirect(conn, ~p"/")
 
       assert {:ok, _conn} = result
     end
@@ -48,7 +48,7 @@ defmodule <%= inspect context.web_module %>.<%= inspect Module.concat(schema.web
       assert redirected_to(conn) == ~p"/"
 
       # Now do a logged in request and assert on the menu
-      conn = get(conn, "/")
+      conn = get(conn, ~p"/")
       response = html_response(conn, 200)
       assert response =~ email
       assert response =~ "Settings"
diff --git a/priv/templates/phx.gen.live/live_test.exs b/priv/templates/phx.gen.live/live_test.exs
index 7da5490c7..7fce16ea0 100644
--- a/priv/templates/phx.gen.live/live_test.exs
+++ b/priv/templates/phx.gen.live/live_test.exs
@@ -26,20 +26,23 @@ defmodule <%= inspect context.web_module %>.<%= inspect Module.concat(schema.web
     test "saves new <%= schema.singular %>", %{conn: conn} do
       {:ok, index_live, _html} = live(conn, ~p"<%= schema.route_prefix %>")
 
-      assert index_live |> element("a", "New <%= schema.human_singular %>") |> render_click() =~
-               "New <%= schema.human_singular %>"
+      assert {:ok, form_live, _} =
+               index_live
+               |> element("a", "New <%= schema.human_singular %>")
+               |> render_click()
+               |> follow_redirect(conn, ~p"<%= schema.route_prefix %>/new")
 
-      assert_patch(index_live, ~p"<%= schema.route_prefix %>/new")
+      assert render(form_live) =~ "New <%= schema.human_singular %>"
 
-      assert index_live
+      assert form_live
              |> form("#<%= schema.singular %>-form", <%= schema.singular %>: @invalid_attrs)
              |> render_change() =~ "<%= Mix.Phoenix.Schema.failed_render_change_message(schema) %>"
 
-      assert index_live
-             |> form("#<%= schema.singular %>-form", <%= schema.singular %>: @create_attrs)
-             |> render_submit()
-
-      assert_patch(index_live, ~p"<%= schema.route_prefix %>")
+      assert {:ok, index_live, _html} =
+               form_live
+               |> form("#<%= schema.singular %>-form", <%= schema.singular %>: @create_attrs)
+               |> render_submit()
+               |> follow_redirect(conn, ~p"<%= schema.route_prefix %>")
 
       html = render(index_live)
       assert html =~ "<%= schema.human_singular %> created successfully"<%= if schema.string_attr do %>
@@ -49,20 +52,23 @@ defmodule <%= inspect context.web_module %>.<%= inspect Module.concat(schema.web
     test "updates <%= schema.singular %> in listing", %{conn: conn, <%= schema.singular %>: <%= schema.singular %>} do
       {:ok, index_live, _html} = live(conn, ~p"<%= schema.route_prefix %>")
 
-      assert index_live |> element("#<%= schema.plural %>-#{<%= schema.singular %>.id} a", "Edit") |> render_click() =~
-               "Edit <%= schema.human_singular %>"
+      assert {:ok, form_live, _html} =
+               index_live
+               |> element("#<%= schema.plural %>-#{<%= schema.singular %>.id} a", "Edit")
+               |> render_click()
+               |> follow_redirect(conn, ~p"<%= schema.route_prefix %>/#{<%= schema.singular %>}/edit")
 
-      assert_patch(index_live, ~p"<%= schema.route_prefix %>/#{<%= schema.singular %>}/edit")
+      assert render(form_live) =~ "Edit <%= schema.human_singular %>"
 
-      assert index_live
+      assert form_live
              |> form("#<%= schema.singular %>-form", <%= schema.singular %>: @invalid_attrs)
              |> render_change() =~ "<%= Mix.Phoenix.Schema.failed_render_change_message(schema) %>"
 
-      assert index_live
-             |> form("#<%= schema.singular %>-form", <%= schema.singular %>: @update_attrs)
-             |> render_submit()
-
-      assert_patch(index_live, ~p"<%= schema.route_prefix %>")
+      assert {:ok, index_live, _html} =
+               form_live
+               |> form("#<%= schema.singular %>-form", <%= schema.singular %>: @update_attrs)
+               |> render_submit()
+               |> follow_redirect(conn, ~p"<%= schema.route_prefix %>")
 
       html = render(index_live)
       assert html =~ "<%= schema.human_singular %> updated successfully"<%= if schema.string_attr do %>
@@ -87,23 +93,26 @@ defmodule <%= inspect context.web_module %>.<%= inspect Module.concat(schema.web
       assert html =~ <%= schema.singular %>.<%= schema.string_attr %><% end %>
     end
 
-    test "updates <%= schema.singular %> within modal", %{conn: conn, <%= schema.singular %>: <%= schema.singular %>} do
+    test "updates <%= schema.singular %> and returns to show", %{conn: conn, <%= schema.singular %>: <%= schema.singular %>} do
       {:ok, show_live, _html} = live(conn, ~p"<%= schema.route_prefix %>/#{<%= schema.singular %>}")
 
-      assert show_live |> element("a", "Edit") |> render_click() =~
-               "Edit <%= schema.human_singular %>"
+      assert {:ok, form_live, _} =
+               show_live
+               |> element("a", "Edit")
+               |> render_click()
+               |> follow_redirect(conn, ~p"<%= schema.route_prefix %>/#{<%= schema.singular %>}/edit?return_to=show")
 
-      assert_patch(show_live, ~p"<%= schema.route_prefix %>/#{<%= schema.singular %>}/show/edit")
+      assert render(form_live) =~ "Edit <%= schema.human_singular %>"
 
-      assert show_live
+      assert form_live
              |> form("#<%= schema.singular %>-form", <%= schema.singular %>: @invalid_attrs)
              |> render_change() =~ "<%= Mix.Phoenix.Schema.failed_render_change_message(schema) %>"
 
-      assert show_live
-             |> form("#<%= schema.singular %>-form", <%= schema.singular %>: @update_attrs)
-             |> render_submit()
-
-      assert_patch(show_live, ~p"<%= schema.route_prefix %>/#{<%= schema.singular %>}")
+      assert {:ok, show_live, _html} =
+               form_live
+               |> form("#<%= schema.singular %>-form", <%= schema.singular %>: @update_attrs)
+               |> render_submit()
+               |> follow_redirect(conn, ~p"<%= schema.route_prefix %>/#{<%= schema.singular %>}")
 
       html = render(show_live)
       assert html =~ "<%= schema.human_singular %> updated successfully"<%= if schema.string_attr do %>
diff --git a/test/mix/tasks/phx.gen.live_test.exs b/test/mix/tasks/phx.gen.live_test.exs
index 20108f396..e67f57a30 100644
--- a/test/mix/tasks/phx.gen.live_test.exs
+++ b/test/mix/tasks/phx.gen.live_test.exs
@@ -80,8 +80,8 @@ defmodule Mix.Tasks.Phx.Gen.LiveTest do
         assert file =~ "defmodule PhoenixWeb.PostLive.Show"
       end
 
-      assert_file "lib/phoenix_web/live/post_live/form_component.ex", fn file ->
-        assert file =~ "defmodule PhoenixWeb.PostLive.FormComponent"
+      assert_file "lib/phoenix_web/live/post_live/form.ex", fn file ->
+        assert file =~ "defmodule PhoenixWeb.PostLive.Form"
       end
 
       assert [path] = Path.wildcard("priv/repo/migrations/*_create_posts.exs")
@@ -91,24 +91,24 @@ defmodule Mix.Tasks.Phx.Gen.LiveTest do
         assert file =~ "create unique_index(:posts, [:slug])"
       end
 
-      assert_file "lib/phoenix_web/live/post_live/index.html.heex", fn file ->
-        assert file =~ ~S|~p"/posts"|
+      assert_file "lib/phoenix_web/live/post_live/index.ex", fn file ->
+        assert file =~ ~S|~p"/posts/#{post}"|
       end
 
-      assert_file "lib/phoenix_web/live/post_live/show.html.heex", fn file ->
+      assert_file "lib/phoenix_web/live/post_live/show.ex", fn file ->
         assert file =~ ~S|~p"/posts"|
       end
 
-      assert_file "lib/phoenix_web/live/post_live/form_component.ex", fn file ->
+      assert_file "lib/phoenix_web/live/post_live/form.ex", fn file ->
         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={@form[:tags]}
-                  type="select"
-                  multiple
+              <.input
+                field={@form[:tags]}
+                type="select"
+                multiple
         """
         assert file =~ ~s(<.input field={@form[:popular]} type="checkbox")
         assert file =~ ~s(<.input field={@form[:drafted_at]} type="datetime-local")
@@ -119,9 +119,9 @@ defmodule Mix.Tasks.Phx.Gen.LiveTest do
         assert file =~ ~s(<.input field={@form[:secret]} type="text" label="Secret" />)
         refute file =~ ~s(<field={@form[:metadata]})
         assert file =~ """
-                <.input
-                  field={@form[:status]}
-                  type="select"
+              <.input
+                field={@form[:status]}
+                type="select"
         """
         assert file =~ ~s|Ecto.Enum.values(Phoenix.Blog.Post, :status)|
 
@@ -133,7 +133,7 @@ defmodule Mix.Tasks.Phx.Gen.LiveTest do
         assert file =~ ~S|~p"/posts"|
         assert file =~ ~S|~p"/posts/new"|
         assert file =~ ~S|~p"/posts/#{post}"|
-        assert file =~ ~S|~p"/posts/#{post}/show/edit"|
+        assert file =~ ~S|~p"/posts/#{post}/edit"|
       end
 
       send self(), {:mix_shell_input, :yes?, true}
@@ -159,8 +159,8 @@ defmodule Mix.Tasks.Phx.Gen.LiveTest do
         assert file =~ "defmodule PhoenixWeb.CommentLive.Show"
       end
 
-      assert_file "lib/phoenix_web/live/comment_live/form_component.ex", fn file ->
-        assert file =~ "defmodule PhoenixWeb.CommentLive.FormComponent"
+      assert_file "lib/phoenix_web/live/comment_live/form.ex", fn file ->
+        assert file =~ "defmodule PhoenixWeb.CommentLive.Form"
       end
 
       assert_receive {:mix_shell, :info, ["""
@@ -168,11 +168,9 @@ defmodule Mix.Tasks.Phx.Gen.LiveTest do
       Add the live routes to your browser scope in lib/phoenix_web/router.ex:
 
           live "/comments", CommentLive.Index, :index
-          live "/comments/new", CommentLive.Index, :new
-          live "/comments/:id/edit", CommentLive.Index, :edit
-
+          live "/comments/new", CommentLive.Form, :new
           live "/comments/:id", CommentLive.Show, :show
-          live "/comments/:id/show/edit", CommentLive.Show, :edit
+          live "/comments/:id/edit", CommentLive.Form, :edit
       """]}
 
       assert_receive({:mix_shell, :info, ["""
@@ -228,8 +226,8 @@ defmodule Mix.Tasks.Phx.Gen.LiveTest do
         assert file =~ "defmodule PhoenixWeb.Blog.PostLive.Show"
       end
 
-      assert_file "lib/phoenix_web/live/blog/post_live/form_component.ex", fn file ->
-        assert file =~ "defmodule PhoenixWeb.Blog.PostLive.FormComponent"
+      assert_file "lib/phoenix_web/live/blog/post_live/form.ex", fn file ->
+        assert file =~ "defmodule PhoenixWeb.Blog.PostLive.Form"
       end
 
       assert [path] = Path.wildcard("priv/repo/migrations/*_create_posts.exs")
@@ -238,24 +236,21 @@ defmodule Mix.Tasks.Phx.Gen.LiveTest do
         assert file =~ "add :title, :string"
       end
 
-      assert_file "lib/phoenix_web/live/blog/post_live/index.html.heex", fn file ->
-        assert file =~ ~S|~p"/blog/posts"|
+      assert_file "lib/phoenix_web/live/blog/post_live/index.ex", fn file ->
         assert file =~ ~S|~p"/blog/posts/#{post}/edit"|
         assert file =~ ~S|~p"/blog/posts/new"|
         assert file =~ ~S|~p"/blog/posts/#{post}"|
       end
 
-      assert_file "lib/phoenix_web/live/blog/post_live/show.html.heex", fn file ->
+      assert_file "lib/phoenix_web/live/blog/post_live/show.ex", fn file ->
         assert file =~ ~S|~p"/blog/posts"|
-        assert file =~ ~S|~p"/blog/posts/#{@post}"|
-        assert file =~ ~S|~p"/blog/posts/#{@post}/show/edit"|
+        assert file =~ ~S|~p"/blog/posts/#{@post}/edit?return_to=show"|
       end
 
       assert_file "test/phoenix_web/live/blog/post_live_test.exs", fn file ->
         assert file =~ ~S|~p"/blog/posts"|
         assert file =~ ~S|~p"/blog/posts/new"|
-        assert file =~ ~S|~p"/blog/posts/#{post}"|
-        assert file =~ ~S|~p"/blog/posts/#{post}/show/edit"|
+        assert file =~ ~S|~p"/blog/posts/#{post}/edit"|
       end
 
       assert_receive {:mix_shell, :info, ["""
@@ -267,11 +262,9 @@ defmodule Mix.Tasks.Phx.Gen.LiveTest do
             ...
 
             live "/posts", PostLive.Index, :index
-            live "/posts/new", PostLive.Index, :new
-            live "/posts/:id/edit", PostLive.Index, :edit
-
+            live "/posts/new", PostLive.Form, :new
             live "/posts/:id", PostLive.Show, :show
-            live "/posts/:id/show/edit", PostLive.Show, :edit
+            live "/posts/:id/edit", PostLive.Form, :edit
           end
       """]}
     end
@@ -287,10 +280,8 @@ defmodule Mix.Tasks.Phx.Gen.LiveTest do
 
       assert_file "lib/phoenix_web/live/post_live/index.ex"
       assert_file "lib/phoenix_web/live/post_live/show.ex"
-      assert_file "lib/phoenix_web/live/post_live/form_component.ex"
+      assert_file "lib/phoenix_web/live/post_live/form.ex"
 
-      assert_file "lib/phoenix_web/live/post_live/index.html.heex"
-      assert_file "lib/phoenix_web/live/post_live/show.html.heex"
       assert_file "test/phoenix_web/live/post_live_test.exs"
     end
   end
@@ -305,10 +296,8 @@ defmodule Mix.Tasks.Phx.Gen.LiveTest do
 
       assert_file "lib/phoenix_web/live/post_live/index.ex"
       assert_file "lib/phoenix_web/live/post_live/show.ex"
-      assert_file "lib/phoenix_web/live/post_live/form_component.ex"
+      assert_file "lib/phoenix_web/live/post_live/form.ex"
 
-      assert_file "lib/phoenix_web/live/post_live/index.html.heex"
-      assert_file "lib/phoenix_web/live/post_live/show.html.heex"
       assert_file "test/phoenix_web/live/post_live_test.exs"
     end
   end
@@ -325,10 +314,8 @@ defmodule Mix.Tasks.Phx.Gen.LiveTest do
 
       assert_file "lib/phoenix_web/live/comment_live/index.ex"
       assert_file "lib/phoenix_web/live/comment_live/show.ex"
-      assert_file "lib/phoenix_web/live/comment_live/form_component.ex"
+      assert_file "lib/phoenix_web/live/comment_live/form.ex"
 
-      assert_file "lib/phoenix_web/live/comment_live/index.html.heex"
-      assert_file "lib/phoenix_web/live/comment_live/show.html.heex"
       assert_file "test/phoenix_web/live/comment_live_test.exs"
     end
   end
@@ -341,17 +328,16 @@ defmodule Mix.Tasks.Phx.Gen.LiveTest do
       assert_file "lib/phoenix/tracker/series.ex"
 
       assert_file "lib/phoenix_web/live/series_live/index.ex", fn file ->
-        assert file =~ "stream(socket, :series_collection, Tracker.list_series())"
+        assert file =~ "|> stream(:series_collection, Tracker.list_series())"
       end
 
       assert_file "lib/phoenix_web/live/series_live/show.ex"
-      assert_file "lib/phoenix_web/live/series_live/form_component.ex"
+      assert_file "lib/phoenix_web/live/series_live/form.ex"
 
-      assert_file "lib/phoenix_web/live/series_live/index.html.heex", fn file ->
+      assert_file "lib/phoenix_web/live/series_live/index.ex", fn file ->
         assert file =~ "@streams.series_collection"
       end
 
-      assert_file "lib/phoenix_web/live/series_live/show.html.heex"
       assert_file "test/phoenix_web/live/series_live_test.exs"
     end
   end
@@ -391,9 +377,9 @@ defmodule Mix.Tasks.Phx.Gen.LiveTest do
           assert file =~ "use PhoenixWeb, :live_view"
         end
 
-        assert_file "lib/phoenix_web/live/user_live/form_component.ex", fn file ->
-          assert file =~ "defmodule PhoenixWeb.UserLive.FormComponent"
-          assert file =~ "use PhoenixWeb, :live_component"
+        assert_file "lib/phoenix_web/live/user_live/form.ex", fn file ->
+          assert file =~ "defmodule PhoenixWeb.UserLive.Form"
+          assert file =~ "use PhoenixWeb, :live_view"
         end
 
         assert_file "test/phoenix_web/live/user_live_test.exs", fn file ->
@@ -434,9 +420,9 @@ defmodule Mix.Tasks.Phx.Gen.LiveTest do
           assert file =~ "use Phoenix, :live_view"
         end
 
-        assert_file "lib/phoenix/live/user_live/form_component.ex", fn file ->
-          assert file =~ "defmodule Phoenix.UserLive.FormComponent"
-          assert file =~ "use Phoenix, :live_component"
+        assert_file "lib/phoenix/live/user_live/form.ex", fn file ->
+          assert file =~ "defmodule Phoenix.UserLive.Form"
+          assert file =~ "use Phoenix, :live_view"
         end
 
         assert_file "test/phoenix/live/user_live_test.exs", fn file ->