LiveviewAppWeb.Router.Helpers.live_path/3 is undefined

I am combining the code in the book, Building Table Views with Phoenix LiveView on to that of the book, Programming Phoenix LiveView", to deepen my understanding of the books. The former uses some older version of LiveView, and the latter uses the latest.

I hope some advices from those who read the two. I don’t completely understand how routes are composed and work by the Router.

From user inputs in a live_component, SortingComponent, I need to assemble a new path using Route.live_path in the live_view module, Index, which is the parent of the live_component. Below is the error.

  1. ** (UndefinedFunctionError) function Routes.live_path/3 is undefined (module Routes is not available) - this error is shown when I extract a new path from user inputs onto a live component. There was a similar question about 2 years ago, link.
    Below is where error arose in the Index live_view module. I changed puch_patch to push_navigate.
def handle_info({:update, params}, socket) do
    # call handle_params
    handle_params(params, "dummy", socket)

    path = Routes.live_path(socket, __MODULE__, params)
    {:noreply, push_navigate(socket, to: path, replace: true)}
end

I also changed helpers: false to true, use Phoenix.Router, helpers: true and added alias of alias MarketWeb.Router.Helpers, as: Routes to the Index module, but not works.

** (ArgumentError) no action MarketWeb.ProductLive.Index for MarketWeb.Router.Helpers.live_path/3. The following actions/clauses are supported:

    live_path(conn_or_endpoint, MarketWeb.ProductLive, params \\ [])
    live_path(conn_or_endpoint, MarketWeb.WrongLive, params \\ [])
    (phoenix 1.7.2) lib/phoenix/router/helpers.ex:328: Phoenix.Router.Helpers.invalid_route_error/3
    (market 0.1.0) lib/market_web/live/product_live/index.ex:85: MarketWeb.ProductLive.Index.handle_info/2
    (phoenix_live_view 0.18.18) lib/phoenix_live_view/channel.ex:276: Phoenix.LiveView.Channel.handle_info/2

iex> Routes.__info__(:functions) shows live_path/2 and live_path/3. So there definately are routes.

Below are my phx.routes

❯ mix phx.routes
                          page_path  GET     /                                      MarketWeb.PageController :home
                live_dashboard_path  GET     /dev/dashboard                         Phoenix.LiveDashboard.PageLive :home
                live_dashboard_path  GET     /dev/dashboard/:page                   Phoenix.LiveDashboard.PageLive :page
                live_dashboard_path  GET     /dev/dashboard/:node/:page             Phoenix.LiveDashboard.PageLive :page
                                     *       /dev/mailbox                           Plug.Swoosh.MailboxPreview []
             user_registration_path  GET     /users/register                        MarketWeb.UserRegistrationLive :new
                    user_login_path  GET     /users/log_in                          MarketWeb.UserLoginLive :new
          user_forgot_password_path  GET     /users/reset_password                  MarketWeb.UserForgotPasswordLive :new
           user_reset_password_path  GET     /users/reset_password/:token           MarketWeb.UserResetPasswordLive :edit
                  user_session_path  POST    /users/log_in                          MarketWeb.UserSessionController :create
                 user_settings_path  GET     /users/settings                        MarketWeb.UserSettingsLive :edit
                 user_settings_path  GET     /users/settings/confirm_email/:token   MarketWeb.UserSettingsLive :confirm_email
                          live_path  GET     /guess                                 MarketWeb.WrongLive MarketWeb.WrongLive
                          live_path  GET     /                                      MarketWeb.ProductLive MarketWeb.ProductLive
                 product_index_path  GET     /products                              MarketWeb.ProductLive.Index :index
                 product_index_path  GET     /products/new                          MarketWeb.ProductLive.Index :new
                 product_index_path  GET     /products/:id/edit                     MarketWeb.ProductLive.Index :edit
                  product_show_path  GET     /products/:id                          MarketWeb.ProductLive.Show :show
                  product_show_path  GET     /products/:id/show/edit                MarketWeb.ProductLive.Show :edit
                  user_session_path  DELETE  /users/log_out                         MarketWeb.UserSessionController :delete
             user_confirmation_path  GET     /users/confirm/:token                  MarketWeb.UserConfirmationLive :edit
user_confirmation_instructions_path  GET     /users/confirm                         MarketWeb.UserConfirmationInstructionsLive :new
                          websocket  WS      /live/websocket                        Phoenix.LiveView.Socket
                           longpoll  GET     /live/longpoll                         Phoenix.LiveView.Socket
                           longpoll  POST    /live/longpoll                         Phoenix.LiveView.Socket
  1. Because Route.live_patch doesn’t work, I just tried to manually change the path by hand to see what happens, {:noreply, push_navigate(socket, to: "/?page=1&page_size=20&sort_by=id&sort_dir=desc", replace: true)} in the handle_info(:update, ...) function of the Index live_view module. Even in this case, the expected product sorting doesn’t works. Though there is no more errors, the web page of the new address shows nothing but the welcome index page.

Thank you for reading this long & tedious question.

Below are the index module I have a little bit modified the code from the book, Programming Phoenix LiveView, Beta 0.9, Part 1, to combine the SortingComponent of the book, Building Table Views with Phoenix LiveView .

defmodule MarketWeb.ProductLive.Index do
  use MarketWeb, :live_view

  alias MarketWeb.Router.Helpers, as: Routes

  alias Market.Catalog
  alias Market.Catalog.Product
  alias MarketWeb.Forms.SortingForm

  require Logger

  @impl true
  def mount(_params, _session, socket) do
    {:ok, stream(socket, :products, Catalog.list_products())}
    # {:ok, stream(socket, :products, [nil])}
  end

  @impl true
  def handle_params(params, _url, socket) do
    IO.inspect(params, label: "handle_params, params")
    socket =
      socket
      |> parse_params(params)
      # |> IO.inspect
      |> assign_products()

    {:noreply, apply_action(socket, socket.assigns.live_action, params)}
  end

  defp parse_params(socket, params) do
    with {:ok, sorting_opts} <- SortingForm.parse(params) do
      assign_sorting(socket, sorting_opts)
    else
      _error ->
        assign_sorting(socket)
    end
  end

  defp assign_sorting(socket, overrides \\ %{}) do
    opts = Map.merge(SortingForm.default_values(), overrides)
    assign(socket, :sorting, opts) # |> IO.inspect
  end

  # Update assign_products/1 like this:
  defp assign_products(socket) do
    %{sorting: sorting} = socket.assigns

    assign(socket, :products, Catalog.list_products(sorting))
  end

  defp apply_action(socket, :edit, %{"id" => id}) do
    socket
    |> assign(:page_title, "Edit Product")
    |> assign(:product, Catalog.get_product!(id))
  end

  defp apply_action(socket, :new, _params) do
    socket
    |> assign(:page_title, "New Product")
    |> assign(:product, %Product{})
  end

  defp apply_action(socket, :index, params) do
    IO.inspect(params, label: "apply_action, :index, params")
    socket
    |> assign(:page_title, "Listing Products")
    |> assign(:product, Catalog.list_products(params))
  end

  def handle_info({:update, params}, socket) do
    # IO.puts "ProductLive.handle_info({:update, params}, socket)"
    # IO.inspect(params, label: "params")

    # params = merge_and_sanitize_params(socket, params) # (1) delete nil values, (2) preserve old values in socket.assigns

    # IO.inspect(params, label: "params")

    # call handle_params
    Logger.info "handling query"
    IO.inspect(params, label: "params")
    handle_params(params, "dummy", socket)

    # path = Routes.live_path(socket, __MODULE__, params)
    # {:noreply, push_navigate(socket, to: path, replace: true)}
    {:noreply, push_navigate(socket, to: "/?page=1&page_size=20&sort_by=id&sort_dir=desc", replace: true)}
    # {:noreply, push_navigate(socket, to: "/products", replace: true)}
  end

  @impl true
  def handle_info({MarketWeb.ProductLive.FormComponent, {:saved, product}}, socket) do
    {:noreply, stream_insert(socket, :products, product)}
  end

  @impl true
  def handle_event("delete", %{"id" => id}, socket) do
    product = Catalog.get_product!(id)
    {:ok, _} = Catalog.delete_product(product)

    {:noreply, stream_delete(socket, :products, product)}
  end
end

I fixed a bug, and now “sorting” works. The problem is that the sorted result is not displayed. Below is the modified Catalog module from the book, Programming Phoenix LiveView, Part 1, to combine the sorting function of the other book.

defmodule Market.Catalog do
  @moduledoc """
  The Catalog context.
  """

  import Ecto.Query, warn: false
  alias Market.Repo

  alias Market.Catalog.Product

  @doc """
  Returns the list of products.

  ## Examples

      iex> list_products()
      [%Product{}, ...]

  """
  def list_products do
    # IO.inspect("list_products/0", label: "list_products")
    Repo.all(Product)
  end

  def list_products(opts) do
    IO.inspect(opts, label: "list_products, opts")
    from(m in Product)
    |> sort(opts)
    |> Repo.all()
    # |> IO.inspect
  end

  defp sort(query, %{sort_by: sort_by, sort_dir: sort_dir})
       when sort_by in [:id, :name, :distance, :sku, :stars, :unit_price] and
              sort_dir in [:asc, :desc] do
    # IO.inspect(query, label: "query")
    order_by(query, {^sort_dir, ^sort_by})
  end

  defp sort(query, _opts), do: query

  @doc """
  Gets a single product.

  Raises `Ecto.NoResultsError` if the Product does not exist.

  ## Examples

      iex> get_product!(123)
      %Product{}

      iex> get_product!(456)
      ** (Ecto.NoResultsError)

  """
  def get_product!(id), do: Repo.get!(Product, id)

  @doc """
  Creates a product.

  ## Examples

      iex> create_product(%{field: value})
      {:ok, %Product{}}

      iex> create_product(%{field: bad_value})
      {:error, %Ecto.Changeset{}}

  """
  def create_product(attrs \\ %{}) do
    %Product{}
    |> Product.changeset(attrs)
    |> Repo.insert()
  end

  @doc """
  Updates a product.

  ## Examples

      iex> update_product(product, %{field: new_value})
      {:ok, %Product{}}

      iex> update_product(product, %{field: bad_value})
      {:error, %Ecto.Changeset{}}

  """
  def update_product(%Product{} = product, attrs) do
    product
    |> Product.changeset(attrs)
    |> Repo.update()
  end

  @doc """
  Deletes a product.

  ## Examples

      iex> delete_product(product)
      {:ok, %Product{}}

      iex> delete_product(product)
      {:error, %Ecto.Changeset{}}

  """
  def delete_product(%Product{} = product) do
    Repo.delete(product)
  end

  @doc """
  Returns an `%Ecto.Changeset{}` for tracking product changes.

  ## Examples

      iex> change_product(product)
      %Ecto.Changeset{data: %Product{}}

  """
  def change_product(%Product{} = product, attrs \\ %{}) do
    # IO.inspect(product, label: "product")
    # IO.inspect(attrs, label: "attrs")
    Product.changeset(product, attrs)
  end
end

Below is router.ex

defmodule MarketWeb.Router do
  use MarketWeb, :router

  import MarketWeb.UserAuth

  pipeline :browser do
    plug(:accepts, ["html"])
    plug(:fetch_session)
    plug(:fetch_live_flash)
    plug(:put_root_layout, {MarketWeb.Layouts, :root})
    plug(:protect_from_forgery)
    plug(:put_secure_browser_headers)
    plug(:fetch_current_user)
  end

  pipeline :api do
    plug(:accepts, ["json"])
  end

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

    get("/", PageController, :home)
  end

  # Other scopes may use custom stacks.
  # scope "/api", MarketWeb do
  #   pipe_through :api
  # end

  # Enable LiveDashboard and Swoosh mailbox preview in development
  if Application.compile_env(:market, :dev_routes) do
    # If you want to use the LiveDashboard in production, you should put
    # it behind authentication and allow only admins to access it.
    # If your application does not have an admins-only section yet,
    # you can use Plug.BasicAuth to set up some basic authentication
    # as long as you are also using SSL (which you should anyway).
    import Phoenix.LiveDashboard.Router

    scope "/dev" do
      pipe_through(:browser)

      live_dashboard("/dashboard", metrics: MarketWeb.Telemetry)
      forward("/mailbox", Plug.Swoosh.MailboxPreview)
    end
  end

  ## Authentication routes

  scope "/", MarketWeb do
    pipe_through([:browser, :redirect_if_user_is_authenticated])

    live_session :redirect_if_user_is_authenticated,
      on_mount: [{MarketWeb.UserAuth, :redirect_if_user_is_authenticated}] do
      live("/users/register", UserRegistrationLive, :new)
      live("/users/log_in", UserLoginLive, :new)
      live("/users/reset_password", UserForgotPasswordLive, :new)
      live("/users/reset_password/:token", UserResetPasswordLive, :edit)
    end

    post("/users/log_in", UserSessionController, :create)
  end

  scope "/", MarketWeb do
    pipe_through([:browser, :require_authenticated_user])

    live_session :require_authenticated_user,
      on_mount: [{MarketWeb.UserAuth, :ensure_authenticated}] do
      live("/users/settings", UserSettingsLive, :edit)
      live("/users/settings/confirm_email/:token", UserSettingsLive, :confirm_email)
      live("/guess", WrongLive)

      live("/products", ProductLive.Index, :index)
      live("/products/new", ProductLive.Index, :new)
      live("/products/:id/edit", ProductLive.Index, :edit)
      live("/products/:id", ProductLive.Show, :show)
      live("/products/:id/show/edit", ProductLive.Show, :edit)
    end
  end

  scope "/", MarketWeb do
    pipe_through([:browser])

    delete("/users/log_out", UserSessionController, :delete)

    live_session :current_user,
      on_mount: [{MarketWeb.UserAuth, :mount_current_user}] do
      live("/users/confirm/:token", UserConfirmationLive, :edit)
      live("/users/confirm", UserConfirmationInstructionsLive, :new)
    end
  end
end