Phoenix.Router.NoRouteError no route found for POST when submitting live view form

I am trying to migrate an app that uses controllers to live view. I have a form component that I believe handles the submit event and navigates to a new page. However, when I try to submit the form, I get this error:

Phoenix.Router.NoRouteError at POST /posts/new
no route found for POST /posts/new (MicroblogWeb.Router)

Available routes
  GET  /                           MicroblogWeb.PostLive.Index nil
  GET  /posts/new                  MicroblogWeb.PostLive.New nil
  GET  /dev/dashboard/css-:md5     Phoenix.LiveDashboard.Assets :css
  GET  /dev/dashboard/js-:md5      Phoenix.LiveDashboard.Assets :js
  GET  /dev/dashboard              Phoenix.LiveDashboard.PageLive :home
  GET  /dev/dashboard/:page        Phoenix.LiveDashboard.PageLive :page
  GET  /dev/dashboard/:node/:page  Phoenix.LiveDashboard.PageLive :page
  *    /dev/mailbox                Plug.Swoosh.MailboxPreview []

My form component:

defmodule MicroblogWeb.PostLive.FormComponent do
  use MicroblogWeb, :live_component

  alias Microblog.Feed

  @impl true
  def render(assigns) do
    ~H"""
    <div>
      <.simple_form
        :let={f}
        for={@form}
        id="post-form"
        autocomplete="off"
        novalidate
        aria-labelledby="post-form-heading"
        data-phx-target={@myself}
        data-phx-change="validate"
        data-phx-submit="save"
        class={[
          "bg-background text-foreground",
          "space-y-fl-xs px-fl-sm-lg py-fl-xs mx-auto max-w-xl rounded-sm"
        ]}
      >
        <.page_heading id="post-form-heading">{@title}</.page_heading>
        <.error :if={@form.action}>{gettext("Oops something went wrong!")}</.error>
        <.textarea_field
          field={f[:body]}
          variant="outline"
          rows="3"
          label={gettext("Text")}
          label_class="sr-only"
          placeholder="Tell 'em how you really feel"
          maxlength="280"
          content_sizing
        />
        <:actions>
          <.button variant="default" color="primary" data-phx-disable-with={gettext("Posting…")}>
            {gettext("Post")}
          </.button>
        </:actions>
      </.simple_form>
    </div>
    """
  end

  @impl true
  def mount(socket) do
    {:ok, assign(socket, :submit_attempted, false)}
  end

  @impl true
  def update(%{post: post} = assigns, socket) do
    {:ok,
     socket
     |> assign(assigns)
     |> assign_new(:form, fn ->
       to_form(Feed.change_post(post))
     end)}
  end

  @impl true
  def handle_event("validate", %{"post" => post_params}, socket) do
    if socket.assigns.submit_attempted do
      changeset = Feed.change_post(socket.assigns.post, post_params)
      {:noreply, assign(socket, form: to_form(changeset, action: :validate))}
    else
      {:noreply, socket}
    end
  end

  def handle_event("save", %{"post" => post_params}, socket) do
    save_post(socket, socket.assigns.action, post_params)
  end

  defp save_post(socket, :edit, post_params) do
    case Feed.update_post(socket.assigns.post, post_params) do
      {:ok, _post} ->
        {:noreply,
         socket
         |> put_flash(:success, gettext("Post updated successfully"))
         |> push_navigate(to: socket.assigns.navigate)}

      {:error, %Ecto.Changeset{} = changeset} ->
        {:noreply, assign(socket, form: to_form(changeset), submit_attempted: true)}
    end
  end

  defp save_post(socket, :new, post_params) do
    case Feed.create_post(post_params) do
      {:ok, _post} ->
        {:noreply,
         socket
         |> put_flash(:success, gettext("Post created successfully"))
         |> push_navigate(to: socket.assigns.navigate)}

      {:error, %Ecto.Changeset{} = changeset} ->
        {:noreply, assign(socket, form: to_form(changeset))}
    end
  end
end

I have read that this happens when the event is not handled, but to the best of my knowledge I am handling the event. What is causing the NoRouteError?

Hi @evao, and welcome to the community.

I’m assuming this is a LiveView. Where you have data-phx-target, data-phx-change and data-phx-submit, you should remove the data- bit - your events aren’t actually being called and it’s reverting to a standard HTML form action.

I changed the binding prefix

let liveSocket = new LiveSocket("/live", Socket, {
  longPollFallbackMs: 2500,
  params: { _csrf_token: csrfToken },
  bindingPrefix: "data-phx",
  hooks: Hooks,
});

The data-phx prefix works in other places

Have you put some IO.inspect calls into your event handlers to confirm they are getting called?

I just tried it and it’s not reaching the events. Am I declaring them wrong?

data-phx-hook works fine, so I would assume that data-phx-submit would also be ok

They look ok. Is there any reason you are changing the binding prefix? It might be worth putting it back to the defaults temporarily just to see whether there’s a bug in its implementation.

I wanted to use valid HTML attributes. I have a different error when I remove the prefix:

[error] GenServer #PID<0.762.0> terminating
** (FunctionClauseError) no function clause matching in MicroblogWeb.PostLive.FormComponent.handle_event/3
    (microblog 0.1.0) lib/microblog_web/live/post_live/form_component.ex:63: MicroblogWeb.PostLive.FormComponent.handle_event("validate", %{"_target" => ["body"], "_unused_body" => "", "body" => ""}, #Phoenix.LiveView.Socket<id: "phx-GCvirN4MwgkuBQFE", endpoint: MicroblogWeb.Endpoint, view: MicroblogWeb.PostLive.New, parent_pid: nil, root_pid: #PID<0.762.0>, router: MicroblogWeb.Router, assigns: %{id: :new, title: "New Post", form: %Phoenix.HTML.Form{source: #Ecto.Changeset<action: nil, changes: %{}, errors: [body: {"can't be blank", [validation: :required]}], data: #Microblog.Feed.Post<>, valid?: false, ...>, impl: Phoenix.HTML.FormData.Ecto.Changeset, id: "post", name: "post", data: %Microblog.Feed.Post{__meta__: #Ecto.Schema.Metadata<:built, "posts">, id: nil, client_id: nil, body: nil, inserted_at: nil, updated_at: nil}, action: nil, hidden: [], params: %{}, errors: [], options: [method: "post"], index: nil}, action: :new, post: %Microblog.Feed.Post{__meta__: #Ecto.Schema.Metadata<:built, "posts">, id: nil, client_id: nil, body: nil, inserted_at: nil, updated_at: nil}, __changed__: %{}, flash: %{}, navigate: "/", submit_attempted: false, myself: %Phoenix.LiveComponent.CID{cid: 1}}, transport_pid: #PID<0.754.0>, ...>)
    (phoenix_live_view 1.0.5) lib/phoenix_live_view/channel.ex:741: anonymous fn/4 in Phoenix.LiveView.Channel.inner_component_handle_event/4
    (telemetry 1.3.0) microblog/deps/telemetry/src/telemetry.erl:324: :telemetry.span/3
    (phoenix_live_view 1.0.5) lib/phoenix_live_view/diff.ex:209: Phoenix.LiveView.Diff.write_component/4
    (phoenix_live_view 1.0.5) lib/phoenix_live_view/channel.ex:662: Phoenix.LiveView.Channel.component_handle/4
    (stdlib 6.2) gen_server.erl:2345: :gen_server.try_handle_info/3
    (stdlib 6.2) gen_server.erl:2433: :gen_server.handle_msg/6
    (stdlib 6.2) proc_lib.erl:329: :proc_lib.init_p_do_apply/3
Last message: %Phoenix.Socket.Message{topic: "lv:phx-GCvirN4MwgkuBQFE", event: "event", payload: %{"cid" => 1, "event" => "validate", "type" => "form", "uploads" => %{}, "value" => "_unused_body=&body=&_target=body"}, ref: "14", join_ref: "13"}
State: %{socket: #Phoenix.LiveView.Socket<id: "phx-GCvirN4MwgkuBQFE", endpoint: MicroblogWeb.Endpoint, view: MicroblogWeb.PostLive.New, parent_pid: nil, root_pid: #PID<0.762.0>, router: MicroblogWeb.Router, assigns: %{post: %Microblog.Feed.Post{__meta__: #Ecto.Schema.Metadata<:built, "posts">, id: nil, client_id: nil, body: nil, inserted_at: nil, updated_at: nil}, __changed__: %{}, page_title: "New Post", flash: %{}, live_action: nil}, transport_pid: #PID<0.754.0>, ...>, components: {%{1 => {MicroblogWeb.PostLive.FormComponent, :new, %{id: :new, title: "New Post", form: %Phoenix.HTML.Form{source: #Ecto.Changeset<action: nil, changes: %{}, errors: [body: {"can't be blank", [validation: :required]}], data: #Microblog.Feed.Post<>, valid?: false, ...>, impl: Phoenix.HTML.FormData.Ecto.Changeset, id: "post", name: "post", data: %Microblog.Feed.Post{__meta__: #Ecto.Schema.Metadata<:built, "posts">, id: nil, client_id: nil, body: nil, inserted_at: nil, updated_at: nil}, action: nil, hidden: [], params: %{}, errors: [], options: [method: "post"], index: nil}, action: :new, post: %Microblog.Feed.Post{__meta__: #Ecto.Schema.Metadata<:built, "posts">, id: nil, client_id: nil, body: nil, inserted_at: nil, updated_at: nil}, __changed__: %{}, flash: %{}, navigate: "/", submit_attempted: false, myself: %Phoenix.LiveComponent.CID{cid: 1}}, %{lifecycle: %Phoenix.LiveView.Lifecycle{after_render: [], handle_async: [], handle_event: [], handle_info: [], handle_params: [], mount: []}, live_temp: %{}, root_view: MicroblogWeb.PostLive.New, children_cids: []}, {95577969494568919757140640622714239527, %{0 => {143281686686550630121361699462069330724, %{0 => {261852144772796047146034576453683994204, %{3 => {2046170258038412353872504792034081781, %{0 => {39695234410385134559962913879239432406, %{0 => {44934007175308861412236530815454140256, %{3 => {286774131093772453844937840763274687788, %{}}}}, 2 => {27245912341499259847747968325596008034, %{1 => {336018770072519701937415303602624617576, %{2 => {322855051686243342789229046833073683198, %{}}}}}}}}, 1 => 177227407147166681844803977528505039017}}}}}}}}}}, %{MicroblogWeb.PostLive.FormComponent => %{new: 1}}, 2}, topic: "lv:phx-GCvirN4MwgkuBQFE", serializer: Phoenix.Socket.V2.JSONSerializer, join_ref: "13", redirect_count: 0, upload_names: %{}, upload_pids: %{}}

So it looks like there’s a bug in the bindingPrefix implementation that doesn’t handle phx-submit.

I think the reason my events aren’t matching is that the textarea_field isn’t setting the correct name anymore.

Hi @evao, that error is easy to fix… take a look at your handle_event("validate"...) signature - the params structures don’t match - you’re receiving %{"_target" => ["body"], "_unused_body" => "", "body" => ""} but expecting %{"post" => post_params}. Changing %{"post" => post_params} to %{"body" => post_params} should give you what you want.

wrt bindingPrefix - yes, probably a good idea to log an issue on Github - GitHub · Where software is built, but I’m not sure it’s needed - there are many high profile sites in production using the default.

I just tried to reproduce this, but the bindingPrefix option works correct for phx-submit in my simple example:

Application.put_env(:sample, Example.Endpoint,
  http: [ip: {127, 0, 0, 1}, port: 5001],
  server: true,
  live_view: [signing_salt: "aaaaaaaa"],
  secret_key_base: String.duplicate("a", 64)
)

Mix.install(
  [
    {:plug_cowboy, "~> 2.5"},
    {:jason, "~> 1.0"},
    {:phoenix, "~> 1.7"},
    # please test your issue using the latest version of LV from GitHub!
    {:phoenix_live_view,
     github: "phoenixframework/phoenix_live_view", branch: "main", override: true}
  ]
)

# if you're trying to test a specific LV commit, it may be necessary to manually build
# the JS assets. To do this, uncomment the following lines:
# this needs mix and npm available in your path!
#
# path = Phoenix.LiveView.__info__(:compile)[:source] |> Path.dirname() |> Path.join("../")
# System.cmd("mix", ["deps.get"], cd: path, into: IO.binstream())
# System.cmd("npm", ["install"], cd: Path.join(path, "./assets"), into: IO.binstream())
# System.cmd("mix", ["assets.build"], cd: path, into: IO.binstream())

defmodule Example.ErrorView do
  def render(template, _), do: Phoenix.Controller.status_message_from_template(template)
end

defmodule Example.HomeLive do
  use Phoenix.LiveView, layout: {__MODULE__, :live}

  def mount(_params, _session, socket) do
    {:ok, assign(socket, :count, 0)}
  end

  def render("live.html", assigns) do
    ~H"""
    <script src="/assets/phoenix/phoenix.js">
    </script>
    <script src="/assets/phoenix_live_view/phoenix_live_view.js">
    </script>
    <%!-- uncomment to use enable tailwind --%>
    <%!-- <script src="https://cdn.tailwindcss.com"></script> --%>
    <script>
      let liveSocket = new window.LiveView.LiveSocket("/live", window.Phoenix.Socket, {bindingPrefix: "data-phx-"})
      liveSocket.connect()
    </script>
    <style>
      * { font-size: 1.1em; }
    </style>
    {@inner_content}
    """
  end

  def render(assigns) do
    ~H"""
    {@count}
    <button data-phx-click="inc">+</button>
    <button data-phx-click="dec">-</button>

    <form data-phx-change="validate" data-phx-submit="save">
      <input type="text" name="email" />
      <input type="text" name="username" />
      <button type="submit">Save</button>
    </form>
    """
  end

  def handle_event("inc", _params, socket) do
    {:noreply, assign(socket, :count, socket.assigns.count + 1)}
  end

  def handle_event("dec", _params, socket) do
    {:noreply, assign(socket, :count, socket.assigns.count - 1)}
  end

  def handle_event("validate", params, socket) do
    {:noreply, socket}
  end

  def handle_event("save", params, socket) do
    {:noreply, socket}
  end
end

defmodule Example.Router do
  use Phoenix.Router
  import Phoenix.LiveView.Router

  pipeline :browser do
    plug(:accepts, ["html"])
  end

  scope "/", Example do
    pipe_through(:browser)

    live("/", HomeLive, :index)
  end
end

defmodule Example.Endpoint do
  use Phoenix.Endpoint, otp_app: :sample
  socket("/live", Phoenix.LiveView.Socket)

  plug Plug.Static, from: {:phoenix, "priv/static"}, at: "/assets/phoenix"
  plug Plug.Static, from: {:phoenix_live_view, "priv/static"}, at: "/assets/phoenix_live_view"

  plug(Example.Router)
end

{:ok, _} = Supervisor.start_link([Example.Endpoint], strategy: :one_for_one)
Process.sleep(:infinity)

If you can find out how to reproduce, please open up an issue. We don’t have thorough tests for the bindingPrefix option, so it’s very possible that there are places where it doesn’t work.

I noticed that you set bindingPrefix: "data-phx", but it should be data-phx- with an extra dash at the end. Otherwise you’d need to use data-phxsubmit etc.

  defp save_post(socket, :edit, post_params) do
  defp save_post(socket, :new, post_params) do

Is this not just an issue based on the actions?

Your simple form doesn’t specify an action to take, but your save_post functions require actions.

When I was using the controller, the textarea name was set to “post[body]”, which pattern matched correctly. I used the generated live view, which seems to expect the same name, but does not set it correctly for some reason.

Thanks for catching the bindingPrefix issue. data-phx-submit is being triggered now.

I have removed the action, but I can’t test that it works until I get the textarea name right.