evao

evao

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?

Showing Posts 1 to 10

mindok

mindok

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.

evao

evao OP

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

mindok

mindok

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

evao

evao OP

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

mindok

mindok

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.

evao

evao OP

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.

mindok

mindok

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 - Issues · phoenixframework/phoenix_live_view · GitHub, but I’m not sure it’s needed - there are many high profile sites in production using the default.

steffend

steffend

Phoenix Core Team

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.

Phxie

Phxie

  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.

evao

evao OP

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.

— All posts loaded —

Where Next? Top

Trending in Questions Top

RSP87
I’m working on a project that simulates the bumbl example in the programming phoenix book. It acts almost like an email client. We have a...
New
nseaSeb
Hello, I know there is an approach for handling lists that allows for optimized traversal, but I can’t recall the specific method (somet...
New
brecabral
Documentation While reading the Scoped Routes section, I noticed that the documentation currently refers to a problem without explainin...
New
RemyXRenard
I’m seeing that a list inside a Kino.DataTable will be interpreted as a charlist, even if the Kino.configure() is set to charlists: :as_l...
New
velrest
So my question is quite simple and i have found no conclusive answer on forum, google or AI. Should we use :erlang.float for Integer to ...
New
samoloth
Hi, I’ve just set up an application with ash_authentication. There is only magic link strategy for now, so there is no confirmation add o...
New
asweet-confluent
I recently noticed that Elixir’s Logger defaults its primary log level to :debug when no :logger, :level application configuration is pre...
New

Other Trending Topics Top

JesseHerrick
Hey, I’m Jesse and I’m the main contributor behind Dexter, a full-featured, lightning-fast Elixir LSP optimized for large codebases. It s...
New
mudasobwa
I am happy to introduce the very α version of the new programming language compiled to BEAM. Welcome Cure. It has literally three kille...
New
marciok
Hi there! We created Gust: A task orchestrator inspired by Airflow. For those who have never heard about Aiflow, it’s a Python-based wor...
New
mhanberg
Hi everyone! The first release candidate for the Expert language server project is now available! We’ve published a press release detai...
New
jimsynz
Beam Bots (or just BB for short) is a framework for building fault-tolerant robotics applications in Elixir using familiar OTP patterns. ...
New
Dmk
Xamal is a deployment tool for Elixir apps that deploys native releases to bare metal servers over SSH. It’s a port of GitHub - basecamp/...
New

Latest on Elixir Forum

We're in Beta

About us Mission Statement

Options

Thread Display Mode




Thread Preview

Skip Thread Previews