"unknown hook found for" and "Parameters: :not_mounted_at_router"

This is being discussed in another thread ( Real-time Search with Phoenix LiveView and Tailwind ) and the person who posted the example has been kindly trying to help me. But I’ve realized the issue I’m encountering is more likely to be an error I’ve made in my app, than something caused by his example code.

So, I thought I’d start a separate thread, especially in case this thread might help someone else who is wrestling with the same issue in the future.

I’m very new to Phoenix / Elixir (but loving it), and I’m not actually a developer, so don’t be shy about correcting me if I seem confused about anything, because I probably am. I’ve been googling, reading docs, reading tutorials, reading posts in these forums, reading answers on stackexchange etc, and trying changes based on what I’ve read, for 2 days trying to figure this out. So I’m sure I’ve confused myself in the process.

Here’s my current stumbling block:

The search tool is visible in the page, but unresponsive. Chrome’s dev console shows this when the page loads:

unknown hook found for "SearchBar"

You can see that in Chrome’s dev console by viewing this page on my staging site: https://mono-phoenix-staging.herokuapp.com/plays/ (I’ve only added the search bar to that page while trying to get it working.)

This is the output seen in the phx.server console logs when the page is loaded on my local (note the “Parameters: :not_mounted_at_router”):

[info] GET /plays
[debug] Processing with MonoPhoenixV01Web.PlaysPageController.plays/2
  Parameters: %{}
  Pipelines: [:browser]
[debug] QUERY OK source="plays" db=0.6ms queue=1.1ms idle=1963.7ms
SELECT p0."title" FROM "plays" AS p0 GROUP BY p0."title" []
[info] Sent 200 in 22ms
[info] CONNECTED TO Phoenix.LiveView.Socket in 18µs
  Transport: :websocket
  Serializer: Phoenix.Socket.V2.JSONSerializer
  Parameters: %{"_csrf_token" => "ABYkH2JAIDs0JDtKFXx_Uy0UESB5PTddhYbH5wuqLwQ0DIHgaqHy-gbo", "_live_referer" => "undefined", "_mounts" => "0", "_track_static" => %{"0" => "http://localhost:4000/assets/css/normalize.css", "1" => "http://localhost:4000/assets/css/tailwind.css", "2" => "http://localhost:4000/assets/app.css", "3" => "http://localhost:4000/assets/css/application.css"}, "vsn" => "2.0.0"}
[debug] MOUNT MonoPhoenixV01Web.SearchbarLive
  Parameters: :not_mounted_at_router
  Session: %{"_csrf_token" => "hOFWW7UJxSjzQ574LeYYTZU2"}
[debug] Replied in 165µs

Related configs

Yes, I’ve got both bootstrap and tailwind onboard. I’m hoping to use tailwind just for the search tool (for now) to avoid having to migrate all my css from boostrap to tailwind. So, I have this in config/config.exs:

config :tailwind,
  version: "3.2.4",
  default: [
    args: ~w(
    --config=tailwind.config.js
    --input=css/app.css
    --output=../priv/static/assets/css/tailwind.css
  ),
    cd: Path.expand("../assets", __DIR__)
  ]

And in assets/tailwind.config.js I’ve added:

  corePlugins: { preflight: false },
  prefix: 'tw-',

I’ve also removed @tailwind base; from assets/css/app.css and restored import "../css/app.css".

Also in assets/js/app.js, these live-related bits:

...
// Include phoenix_html to handle method=PUT/DELETE in forms and buttons.
import "phoenix_html"
// Establish Phoenix Socket and LiveView configuration.
import {Socket} from "phoenix"
import {LiveSocket} from "phoenix_live_view"
import topbar from "../vendor/topbar"

let csrfToken = document.querySelector("meta[name='csrf-token']").getAttribute("content")
let liveSocket = new LiveSocket("/live", Socket, {params: {_csrf_token: csrfToken}})
...
// connect if there are any LiveViews on the page
liveSocket.connect()

// expose liveSocket on window for web console debug logs and latency simulation:
liveSocket.enableDebug()
// >> liveSocket.enableLatencySim(1000)  // enabled for duration of browser session
// >> liveSocket.disableLatencySim()
window.liveSocket = liveSocket

Here are my current deps on my test branch (I’ve updated some since my last deploy to staging, but no behaviors were changed, and no new warnings or errors):

      {:phoenix, "~> 1.7.1", override: true},
      {:phoenix_ecto, "~> 4.4"},
      {:ecto_sql, "~> 3.6"},
      {:postgrex, ">= 0.0.0"},
      {:phoenix_html, "~> 3.0"},
      {:phoenix_view, "~> 2.0"},
      {:phoenix_live_reload, "~> 1.2", only: :dev},
      {:phoenix_live_view, "~> 0.18.17"},
      {:phoenix_live_dashboard, "~> 0.7.2"},
      {:esbuild, "~> 0.4", runtime: Mix.env() == :dev},
      {:tailwind, "~> 0.1.9", runtime: Mix.env() == :dev},
      {:swoosh, "~> 1.3"},
      {:telemetry_metrics, "~> 0.6"},
      {:telemetry_poller, "~> 1.0"},
      {:gettext, "~> 0.22.1"},
      {:jason, "~> 1.2"},
      {:plug_cowboy, "~> 2.5"},
      {:redirect, "~> 0.4.0"},
      {:html_assertion, "0.1.5", only: :test},
      {:floki, ">= 0.34.2", only: :test},
      {:credo, "~> 1.6", only: [:dev, :test], runtime: false}

And my routes from router.ex:

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

    get("/", StaticPageController, :home)
    get("/plays", PlaysPageController, :plays)
    get("/play/:playid", PlayPageController, :play)
    get("/mens", MensPageController, :mens)
    get("/men/:playid", MenplayPageController, :menplay)
    get("/womens", WomensPageController, :womens)
    get("/women/:playid", WomenplayPageController, :womenplay)
    get("/monologues/:monoid", MonologuesPageController, :monologues)
    get("/aboutus", StaticPageController, :aboutus)
    get("/faq", StaticPageController, :faq)
    get("/home", StaticPageController, :home)
    get("/links", StaticPageController, :links)
    get("/privacy", StaticPageController, :privacy)
    get("/maintenance", StaticPageController, :maintenance)
    get("/hello", PageController, :hello)
    get("/sandbox", PageController, :sandbox)
    live("/monofinds/:monofind", SearchbarLive, :show)
  end

My versions of the example files

I copied the files from caspg’s example here: Example of real-time search bar implementation in Phoenix LiveView and Tailwind. Working example on https://travelermap.net/parks/usa · GitHub

Here are my versions of those files:
lib/mono_phoenix_v01_web/live/searchbar_live.ex:

defmodule MonoPhoenixV01Web.SearchbarLive do
  use MonoPhoenixV01Web, :live_view

  alias Phoenix.LiveView.JS
  alias MonoPhoenixV01.Monofinds

  def mount(_params, _session, socket) do
    socket = assign(socket, monofinds: [])
    {:ok, socket, layout: false}
  end

  def handle_event("change", %{"search" => %{"query" => ""}}, socket) do
    socket = assign(socket, :monofinds, [])
    {:noreply, socket}
  end

  def handle_event("change", %{"search" => %{"query" => search_query}}, socket) do
    monofinds = Monofinds.search(search_query)
    socket = assign(socket, :monofinds, monofinds)

    {:noreply, socket}
  end

  def open_modal(js \\ %JS{}) do
    js
    |> JS.show(
      to: "#searchbox_container",
      transition:
        {"tw-transition tw-ease-out tw-duration-200", "tw-opacity-0 tw-scale-95",
         "tw-opacity-100 tw-scale-100"}
    )
    |> JS.show(
      to: "#searchbar-dialog",
      transition: {"tw-transition tw-ease-in tw-duration-100", "tw-opacity-0", "tw-opacity-100"}
    )
    |> JS.focus(to: "#search-input")
  end

  def hide_modal(js \\ %JS{}) do
    js
    |> JS.hide(
      to: "#searchbar-searchbox_container",
      transition:
        {"tw-transition tw-ease-in tw-duration-100", "tw-opacity-100 tw-scale-100",
         "tw-opacity-0 tw-scale-95"}
    )
    |> JS.hide(
      to: "#searchbar-dialog",
      transition: {"tw-transition tw-ease-in tw-duration-100", "tw-opacity-100", "tw-opacity-0"}
    )
  end
end

lib/mono_phoenix_v01_web/live/searchbar_live.html.heex:

<div class="block max-w-96 flex-auto">
  <button
    type="button"
    class="tw-hidden flex items-start text-left text-gray-500 bg-white hover:ring-gray-500 ring-gray-300 h-12 w-full items-center gap-1 rounded-md pl-2 pr-3 text-2xl ring-1 transition lg:flex focus:[&:not(:focus-visible)]:outline-none"
    phx-click={open_modal()}
  >
    <svg viewBox="0 0 20 20" fill="none" aria-hidden="true" class="h-7 w-7 stroke-current">
      <path
        stroke-linecap="round"
        stroke-linejoin="round"
        d="M12.01 12a4.25 4.25 0 1 0-6.02-6 4.25 4.25 0 0 0 6.02 6Zm0 0 3.24 3.25"
      >
      </path>
    </svg>
    Search for monologues...
  </button>
</div>

<div
  id="searchbar-dialog"
  class="hidden fixed inset-0 z-50"
  role="dialog"
  aria-modal="true"
  phx-window-keydown={hide_modal()}
  phx-key="escape"
>
  <div class="fixed inset-0 bg-zinc-400/25 backdrop-blur-sm opacity-100"></div>
  <div class="fixed inset-0 overflow-y-auto px-4 py-4 sm:py-20 sm:px-6 md:py-32 lg:px-8 lg:py-[15vh]">
    <div
      id="searchbox_container"
      class="mx-auto overflow-hidden rounded-lg bg-zinc-50 shadow-xl ring-zinc-900/7.5 md:max-w-xl opacity-100 scale-100"
      phx-hook="SearchBar"
    >
      <div
        role="combobox"
        aria-haspopup="listbox"
        phx-click-away={hide_modal()}
        aria-expanded={@monofinds != []}
      >
        <form action="" novalidate="" role="search" phx-change="change">
          <div class="group relative flex h-12">
            <svg
              viewBox="0 0 20 20"
              fill="none"
              aria-hidden="true"
              class="pointer-events-none absolute left-3 top-0 h-full w-5 stroke-zinc-500"
            >
              <path
                stroke-linecap="round"
                stroke-linejoin="round"
                d="M12.01 12a4.25 4.25 0 1 0-6.02-6 4.25 4.25 0 0 0 6.02 6Zm0 0 3.24 3.25"
              >
              </path>
            </svg>

            <input
              id="search-input"
              name="search[query]"
              class="flex-auto rounded-lg appearance-none bg-transparent pl-10 text-zinc-900 outline-none focus:outline-none border-slate-200 focus:border-slate-200 focus:ring-0 focus:shadow-none placeholder:text-zinc-500 focus:w-full focus:flex-none md:text-md [&::-webkit-search-cancel-button]:hidden [&::-webkit-search-decoration]:hidden [&::-webkit-search-results-button]:hidden [&::-webkit-search-results-decoration]:hidden pr-4"
              style={
                @monofinds != [] &&
                  "border-bottom-left-radius: 0; border-bottom-right-radius: 0; border-bottom: none"
              }
              aria-autocomplete="both"
              aria-controls="searchbox__results_list"
              autocomplete="off"
              autocorrect="off"
              autocapitalize="off"
              enterkeyhint="search"
              spellcheck="false"
              placeholder="Search for monologues"
              type="search"
              value=""
              tabindex="0"
            />
          </div>

          <ul
            :if={@monofinds != []}
            class="divide-y divide-slate-200 overflow-y-auto rounded-b-lg border-t border-slate-200 text-md leading-6"
            id="searchbox__results_list"
            role="listbox"
          >
            <%= for monofind <- @monofinds do %>
              <li id={"#{monofind.id}"}>
                <.link
                  navigate={~p"/monofinds/#{monofind.slug}"}
                  class="block p-4 hover:bg-slate-100 focus:outline-none focus:bg-slate-100 focus:text-sky-800"
                >
                  <%= monofind.body %>
                </.link>
              </li>
            <% end %>
          </ul>
        </form>
      </div>
    </div>
  </div>
</div>

lib/mono_phoenix_v01/monofinds.ex:

defmodule MonoPhoenixV01.Monofinds do
  @moduledoc """
  The search query thingy
  """
  import Ecto.Query, warn: false

  def search(search_query) do
    search_query = "%#{search_query}%"

    query =
      from(m in "monologues",
        where:
          ilike(m.body, ^search_query) or
            ilike(m.location, ^search_query) or
            ilike(m.character, ^search_query) or
            ilike(m.first_line, ^search_query) or
            ilike(m.style, ^search_query)
      )
      |> limit(15)

    rows = MonoPhoenixV01.Repo.all(query)

    rows
  end
end

lib/mono_phoenix_v01_web/live/SearchBar.ts:

// This is optional phoenix client hook. It allows to use key down and up to select results.

export const SearchBar = {
    mounted() {
      const searchBarContainer = (this as any).el as HTMLDivElement
      document.addEventListener('keydown', (event) => {
        if (event.key !== 'ArrowUp' && event.key !== 'ArrowDown') {
          return
        }
  
        const focusElement = document.querySelector(':focus') as HTMLElement
  
        if (!focusElement) {
          return
        }
  
        if (!searchBarContainer.contains(focusElement)) {
          return
        }
  
        event.preventDefault()
  
        const tabElements = document.querySelectorAll(
          '#search-input, #searchbox__results_list a',
        ) as NodeListOf<HTMLElement>
        const focusIndex = Array.from(tabElements).indexOf(focusElement)
        const tabElementsCount = tabElements.length - 1
  
        if (event.key === 'ArrowUp') {
          tabElements[focusIndex > 0 ? focusIndex - 1 : tabElementsCount].focus()
        }
  
        if (event.key === 'ArrowDown') {
          tabElements[focusIndex < tabElementsCount ? focusIndex + 1 : 0].focus()
        }
      })
    },
  }

If you noticed the tw-hidden class in the template, it’s because I can’t see the search bar in the page unless I do that. Hoping it will work correctly without the prefix when the other issues are resolved (but I have a lot of uncertainty around that, since I’ve never met tailwind until just a couple of days ago.)

Anyway, it seems like I’m really close, and just missing some basic element that an actual developer would be able to spot easily.

So, thanks in advance to anyone willing to help me get unstuck.

You’re getting Parameters: :not_mounted_at_router because your LV is not mounted at the top-level. Only top-level views have access to the params from the URL. This might or might not be a problem in your case.

What is certainly a problem is the unknown hook found for "SearchBar" message: this happens because the frontend can’t find the hook that you want to attach to your searchbox_container element.

You need to specify the hook when you create the livesocket:

let liveSocket = new LiveSocket("/live", Socket, {params: {_csrf_token: csrfToken}, hooks: {SearchBar}})
2 Likes

Thanks much @trisolaran . I’ll give that a try, but which file should I drop that into?

edit:

I dropped hooks: {SearchBar} into this line in assets/js/app.js:

let liveSocket = new LiveSocket("/live", Socket, {params: {_csrf_token: csrfToken}, hooks: {SearchBar}})

I got:

Rebuilding...

Done in 324ms.
[info] CONNECTED TO Phoenix.LiveView.Socket in 19µs
  Transport: :websocket
  Serializer: Phoenix.Socket.V2.JSONSerializer
  Parameters: %{"_csrf_token" => "BGMySwkGLBYyHyJ9MR8JXzYtXiEue0grJ-V3Z_GCq-l-FsfnZa0oD-eO", "_live_referer" => "undefined", "_mounts" => "2", "_track_static" => %{"0" => "http://localhost:4000/assets/css/normalize.css", "1" => "http://localhost:4000/assets/css/tailwind.css", "2" => "http://localhost:4000/assets/app.css", "3" => "http://localhost:4000/assets/css/application.css"}, "vsn" => "2.0.0"}
[debug] MOUNT MonoPhoenixV01Web.SearchbarLive
  Parameters: :not_mounted_at_router
  Session: %{"_csrf_token" => "NNdxSYkUC2NPwlo1lLnNjV-d"}
[debug] Replied in 6ms
[info] GET /plays/
[debug] Processing with MonoPhoenixV01Web.PlaysPageController.plays/2
  Parameters: %{}
  Pipelines: [:browser]
[debug] QUERY OK source="plays" db=0.5ms queue=0.7ms idle=1253.2ms
SELECT p0."title" FROM "plays" AS p0 GROUP BY p0."title" []
[info] Sent 200 in 23ms

And the error in Chrome’s dev console has a different wording:

Uncaught ReferenceError: SearchBar is not defined

Edit 2: I had the same “Which file?” question when I was reading JavaScript interoperability — Phoenix LiveView v0.18.17

@trisolaran, I was able to get rid of the unknown hook found by adding this to assets/js/app.js:

import {SearchBar} from "../../lib/mono_phoenix_v01_web/live/SearchBar"

let Hooks = {}
Hooks.SearchBar = SearchBar

let csrfToken = document.querySelector("meta[name='csrf-token']").getAttribute("content")
let liveSocket = new LiveSocket("/live", Socket, {params: {_csrf_token: csrfToken}, hooks: {SearchBar}})

Thanks for nudging me in that direction.

Could you please clarify what you meant by:

You’re getting Parameters: :not_mounted_at_router because your LV is not mounted at the top-level. Only top-level views have access to the params from the URL. This might or might not be a problem in your case.

I’m a bit of a n00b, so I don’t really know which files or code you’re referring to there. Thanks!

Aha. After getting it added to the router, I was able to get a new error after I changed the live_render call in the template to this:

  <div class="absolute top-24 w-5/6">
    <%= live_render(
      @conn,
      MonoPhoenixV01Web.SearchbarLive,
      id: "searchbar",
      container:
        {:div,
         class: "flex flex-wrap justify-between items-start text-left items-center lg:w-full"},
      router: MonoPhoenixV01Web.SearchbarLive
    ) %>
  </div>

That last line had previously been MonoPhoenixV01Web.SearchbarLive:

Now I’m off to chase the next error. Hopefully I’ll be able to solve this one without opening a new thread:

[info] CONNECTED TO Phoenix.LiveView.Socket in 29µs
  Transport: :websocket
  Serializer: Phoenix.Socket.V2.JSONSerializer
  Parameters: %{"_csrf_token" => "AXsSBG9kGTIAZVNOGFBNd3kXN3Q2PDdji4TS8SLxx694IezC5rn-bfbQ", "_live_referer" => "undefined", "_mounts" => "0", "_track_static" => %{"0" => "http://localhost:4000/assets/css/normalize.css", "1" => "http://localhost:4000/assets/css/tailwind.css", "2" => "http://localhost:4000/assets/app.css", "3" => "http://localhost:4000/assets/css/application.css"}, "vsn" => "2.0.0"}
[error] GenServer #PID<0.993.0> terminating
** (UndefinedFunctionError) function MonoPhoenixV01Web.SearchbarLive.__match_route__/3 is undefined or private
    (mono_phoenix_v01 0.1.0) MonoPhoenixV01Web.SearchbarLive.__match_route__(["plays"], "GET", "localhost")
    (phoenix 1.7.1) lib/phoenix/router.ex:1194: Phoenix.Router.route_info/4
...

Thanks again for your reply, @trisolaran !

1 Like

Maybe make your code open source and put it on GitHub? Will be easier to find help.

Thanks, I will eventually. I’ve moved to working on a different approach. I realized the modal isn’t a great fit for my use case (it’s perfect for your travelmap though, very cool!)

1 Like