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.