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.