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.