Hi. I’ve been struggling with this issue through attempts at three different search tutorials/examples I’ve tried to replicate. This suggests to me that there’s something small but vital that I’m missing and/or doing wrong (I’ve included a list of the resources I’ve been referring to, below. I’ve also been crawling these forums and stackexchange.)
This is the first live module I’ve tried to add, but the rest of the site is working great. I’m very happy to have left Ruby in my rearview, and am enjoying Phoenix and Elixir a lot.
This is a really long post, but I suspect the answer is probably is a short one, maybe even just single line of code, or a few. It seems like I’m really close. The post is long because I am very new to Phoenix and Elixir, so I may be providing more detail than is needed to solve this (seems better than not providing enough info.)
I’m not an actual developer or engineer, but the solution will hopefully be easy for real devs to spot…
The Error (…er, warning. I can compile, just can’t access my new live module)
Might as well start with the errors seen in the phx server console. This shows what I see when I mix phx.server
, followed by what I see when I load the page (/plays
) where I’m trying to render the search_bar
(the controller for the page includes an ecto query, which is what you see there before the SearchBar errors):
...
==> mono_phoenix_v01
Compiling 32 files (.ex)
warning: MonoPhoenixV01Web.SearchBar.get_all/1 is undefined (module MonoPhoenixV01Web.SearchBar is not available or is yet to be defined)
lib/mono_phoenix_v01/search_bar_live.ex:21: MonoPhoenixV01Web.SearchBarLive.Index.load_search_bar/2
Generated mono_phoenix_v01 app
[notice] :alarm_handler: {:set, {{:disk_almost_full, '/usr/lib/wsl/drivers'}, []}}
[notice] :alarm_handler: {:set, {{:disk_almost_full, '/mnt/c'}, []}}
[info] Running MonoPhoenixV01Web.Endpoint with cowboy 2.9.0 at 0.0.0.0:4000 (http)
[info] Access MonoPhoenixV01Web.Endpoint at http://localhost:4000
[debug] Downloading esbuild from https://registry.npmjs.org/esbuild-linux-64/-/esbuild-linux-64-0.14.29.tgz
[watch] build finished, watching for changes...
[info] GET /plays/
[debug] Processing with MonoPhoenixV01Web.PlaysPageController.plays/2
Parameters: %{}
Pipelines: [:browser]
[debug] QUERY OK source="plays" db=0.4ms queue=0.4ms idle=1763.4ms
SELECT p0."title" FROM "plays" AS p0 GROUP BY p0."title" []
[info] Sent 500 in 40ms
[error] #PID<0.7371.0> running Phoenix.Endpoint.SyncCodeReloadPlug (connection #PID<0.7369.0>, stream id 1) terminated
Server: localhost:4000 (http)
Request: GET /plays/
** (exit) an exception was raised:
** (UndefinedFunctionError) function MonoPhoenixV01Web.SearchBarLive.__live__/0 is undefined (module MonoPhoenixV01Web.SearchBarLive is not available)
MonoPhoenixV01Web.SearchBarLive.__live__()
(phoenix_live_view 0.18.17) lib/phoenix_live_view/static.ex:257: Phoenix.LiveView.Static.load_live!/2
(phoenix_live_view 0.18.17) lib/phoenix_live_view/static.ex:91: Phoenix.LiveView.Static.render/3
(phoenix_live_view 0.18.17) lib/phoenix_component.ex:883: Phoenix.Component.live_render/3
(mono_phoenix_v01 0.1.0) lib/mono_phoenix_v01_web/templates/plays_page/plays.html.heex:13: anonymous fn/2 in MonoPhoenixV01Web.PlaysPageView."plays.html"/1
(phoenix_live_view 0.18.17) lib/phoenix_live_view/engine.ex:137: Phoenix.HTML.Safe.Phoenix.LiveView.Rendered.to_iodata/1
(phoenix_live_view 0.18.17) lib/phoenix_live_view/engine.ex:153: Phoenix.HTML.Safe.Phoenix.LiveView.Rendered.to_iodata/3
(phoenix 1.7.1) lib/phoenix/controller.ex:1005: anonymous fn/5 in Phoenix.Controller.template_render_to_iodata/4
(telemetry 1.2.1) /home/steven/webdev/elixir/mono_phoenix_v01/deps/telemetry/src/telemetry.erl:321: :telemetry.span/3
(phoenix 1.7.1) lib/phoenix/controller.ex:971: Phoenix.Controller.render_and_send/4
(mono_phoenix_v01 0.1.0) lib/mono_phoenix_v01_web/controllers/plays_page_controller.ex:1: MonoPhoenixV01Web.PlaysPageController.action/2
(mono_phoenix_v01 0.1.0) lib/mono_phoenix_v01_web/controllers/plays_page_controller.ex:1: MonoPhoenixV01Web.PlaysPageController.phoenix_controller_pipeline/2
(phoenix 1.7.1) lib/phoenix/router.ex:425: Phoenix.Router.__call__/5
(mono_phoenix_v01 0.1.0) lib/mono_phoenix_v01_web/endpoint.ex:1: MonoPhoenixV01Web.Endpoint.plug_builder_call/2
(mono_phoenix_v01 0.1.0) lib/plug/debugger.ex:136: MonoPhoenixV01Web.Endpoint."call (overridable 3)"/2
(mono_phoenix_v01 0.1.0) lib/mono_phoenix_v01_web/endpoint.ex:1: MonoPhoenixV01Web.Endpoint.call/2
(phoenix 1.7.1) lib/phoenix/endpoint/sync_code_reload_plug.ex:22: Phoenix.Endpoint.SyncCodeReloadPlug.do_call/4
(plug_cowboy 2.6.0) lib/plug/cowboy/handler.ex:11: Plug.Cowboy.Handler.init/2
(cowboy 2.9.0) /home/steven/webdev/elixir/mono_phoenix_v01/deps/cowboy/src/cowboy_handler.erl:37: :cowboy_handler.execute/2
(cowboy 2.9.0) /home/steven/webdev/elixir/mono_phoenix_v01/deps/cowboy/src/cowboy_stream_h.erl:306: :cowboy_stream_h.execute/3
And when I try to visit http://localhost:4000/search_bar
directly:
[info] GET /search_bar
[debug] Processing with MonoPhoenixV01Web.SearchBarLive.index/2
Parameters: %{}
Pipelines: [:browser]
[info] Sent 500 in 21ms
[error] #PID<0.7413.0> running Phoenix.Endpoint.SyncCodeReloadPlug (connection #PID<0.7411.0>, stream id 1) terminated
Server: localhost:4000 (http)
Request: GET /search_bar
** (exit) an exception was raised:
** (UndefinedFunctionError) function MonoPhoenixV01Web.SearchBarLive.__live__/0 is undefined (module MonoPhoenixV01Web.SearchBarLive is not available)
MonoPhoenixV01Web.SearchBarLive.__live__()
(phoenix_live_view 0.18.17) lib/phoenix_live_view/static.ex:257: Phoenix.LiveView.Static.load_live!/2
...
Theories I’ve cooked up, investigated, and am still unsure about:
-
Something I’m missing in my live view setup?
(a plug or socket I need to edit or add somewhere? something I need to add tomyapp_web.ex
? something else I need to add to the live search module?) -
Something I’ve missed in my upgrade from 1.6.15 to 1.7.1? (I do have support for verified routes installed and working, so feel free to include the
~p
sigil in any suggested solutions.) -
Something I’m misunderstanding about the naming conventions? (I’ve tried a lot of variants, trying to guess at ways at which I might be misunderstanding, may have thoroughly confused myself, lol)
-
Conflicting versions of deps?
-
Something that isn’t even on my radar yet? (my n00bism. pebkac)
-
2 or more of the above?
Resources I’ve been working from:
-
The tutorial I was working from when I created the SearchBarLive module (my version is seen in the next section, below):
Handling search form nicely with Phoenix LiveView - Michal (arathunku) , -
The pages I read before and during the upgrade from 1.6.15 to 1.7 (and to triple-check live configs since):
Upgrading to Phoenix 1.7 - ElixirCasts ,
phoenix-1.6.x-1.7-upgrade.md · GitHub ,
Phoenix Framework 1.7 — What’s New & Why It Matters , -
LiveView resources I’ve been reading and referring to:
Installation — Phoenix LiveView v0.18.15 ,
Getting Started with Phoenix LiveView ,
The "—live" option (phoenix 1.6.0.rc) does not seem to work properly ,
Search bar with find as you type autosuggestion along with a LiveView - #4 by msimonborg ,
In case it is relevant, the file tree for most of my app comes from this doc: Request life-cycle — Phoenix v1.6.15
I’ve since add a lib/myapp_web/live
folder (details in another section, below)
Popping the hood (versions, deps, contents of related files):
-
My
local
, and mystaging
site on heroku, are now onPhoenix 1.7.1
-
My
production
site ( https://www.shakespeare-monologues.org ) is still on1.6.15
. (It’ll get 1.7.1 when I’ve successfully added search.) -
Erlang/OTP 24
-
Elixir 1.14.1 (compiled with Erlang/OTP 24)
See the contents of mix.ex
, below, for the deps and versions.
The live dashboard installed by the 1.6.15 generator is still working correctly under v1.7.1 on my local, and all my other stuff is working, but this is the first live module I’ve tried to add.
Files
(please let me know if there are other relevant files I should post)
The first files below 3 were created by me while following the post at Handling search form nicely with Phoenix LiveView - Michal (arathunku) ,
-
lib/mono_phoenix_v01/search_bar_live.ex
:
defmodule MonoPhoenixV01Web.SearchBarLive.Index do
use MonoPhoenixV01Web, :live_view
alias MonoPhoenixV01Web.SearchBar
## socket assigns
@impl true
def mount(_params, _session, socket) do
{:ok, socket}
end
@impl true
def handle_params(params, _url, socket) do
query = params |> Map.get("query")
{:noreply, socket |> load_search_bar(query)}
end
def load_search_bar(socket, query) do
socket
|> assign(:query, query)
|> assign(:search_bar, SearchBar.get_all(query))
end
## render assigns
@impl true
def render(assigns) do
~L"""
<h3>Search results</h3>
<%= render_search_form(assigns) %> <%# added %>
<%= render_search_bar(assigns) %>
"""
end
## render the search form
def render_search_form(assigns) do
~L"""
<%= form_for :search, "#", [phx_submit: "search", phx_change: "search", id: "searchbar"], fn f -> %>
<%= label f, :search %>
<%= text_input f, :query, value: @query %>
<%= submit "Search" %>
<% end %>
"""
end
@impl true
def handle_event("search", %{"search" => %{"query" => query}}, socket) do
{:noreply, push_patch(socket, to: Routes.search_bar_path(socket, :index, query: query))}
end
## render the search results
def render_search_bar(assigns) do
~L"""
<div class="center-this">
<table class="monologue-list">
<tbody>
<%= for %{row: row} <- @search_bar do %>
<tr class="monologue_list">
<td class="{ (index.even? ? 'even' : 'odd') }">
<span class="monologue-playname"><%= row.play %></span> · <span class="monologue-actscene"><%= link to: raw(row.scene), method: :get, target: "_blank" do %><%= row.location %><% end %></span> ·
<span class="monologue-actscene"><%= row.style %></span>
<br />
<span class="monologue-character"><%= row.character %></span>
<br />
<div
class="monologue-firstline-table"
data-toggle="collapse"
data-target={"#collapse-" <> Integer.to_string(row.monologues)}
>
<%= row.firstline %>
</div>
<div
class="collapse multi-collapse monologue-show"
id={"collapse-" <> to_string(row.monologues)}
>
<br />
<%= raw(row.body) %>
<%= link to: raw(row.pdf), method: :get, target: "_blank", rel: "noopener" do %>
<img
src={Routes.static_path(@conn, "/images/pdf_file_icon_16x16.png")}
alt="Click for a double-spaced PDF of this monologue"
title="Click for a double-spaced PDF of this monologue"
/>
<% end %>
</div>
</td>
</tr>
<% end %>
</tbody>
</table>
</div>
"""
end
end
-
lib/mono_phoenix_v01_web/live/search_bar_live.html.heex
:
<.live_title prefix="Monologues from ">
<%= assigns[:page_title] || "#{hd(@rows).play} · Shakespeare's Monologues" %>
</.live_title>
<div>
</div>
<div class="accent-font">
<h3>Monologues matching your search:</h3>
<span font-size: 10px;>
Click on the 1st line, under the character's name, to see the full monologue. <a
href="#"
data-toggle="collapse"
data-target=".multi-collapse"
id="toggle-button"
>
<img
src="/images/ExpandAll.png"
id="toggle-image"
alt="Click to toggle text of all monologues on the page.
Reload the page to reset the toggle"
title="Click to toggle the text of all monologues on the page.
Reload the page to reset the toggle."
/>
</a>
</span>
</div>
<div>
<div class="center-this">
<table class="monologue-list">
<tbody>
<%= for row <- @rows do %>
<tr class="monologue_list">
<td class="{ (index.even? ? 'even' : 'odd') }">
<span class="monologue-playname"><%= row.play %></span> · <span class="monologue-actscene"><%= link to: raw(row.scene), method: :get, target: "_blank" do %><%= row.location %><% end %></span> ·
<span class="monologue-actscene"><%= row.style %></span>
<br />
<span class="monologue-character"><%= row.character %></span>
<br />
<div
class="monologue-firstline-table"
data-toggle="collapse"
data-target={"#collapse-" <> Integer.to_string(row.monologues)}
>
<%= row.firstline %>
</div>
<div
class="collapse multi-collapse monologue-show"
id={"collapse-" <> to_string(row.monologues)}
>
<br />
<%= raw(row.body) %>
<%= link to: raw(row.pdf), method: :get, target: "_blank", rel: "noopener" do %>
<img
src={Routes.static_path(@conn, "/images/pdf_file_icon_16x16.png")}
alt="Click for a double-spaced PDF of this monologue"
title="Click for a double-spaced PDF of this monologue"
/>
<% end %>
</div>
</td>
</tr>
<% end %>
</tbody>
</table>
</div>
</div>
</div>
<script>
const toggleButton = document.getElementById('toggle-button');
const toggleImage = document.getElementById('toggle-image');
toggleButton.addEventListener('click', () => {
toggleImage.classList.toggle('collapsed');
});
</script>
<style>
#toggle-image.collapsed {
content: url('/images/CollapseAll.png');
}
#toggle-image {
content: url('/images/ExpandAll.png');
}
</style>
- I’m trying to render it in
lib/mono_phoenix_v01_web/templates/plays_page/plays.html.heex
, thusly:
<div class="input-group accent-font">
<%= live_render(
@conn,
MonoPhoenixV01Web.SearchBarLive,
id: "searchbar"
) %>
</div>
-
mix.ex
(am I missing something needed for live modules to work?)
defmodule MonoPhoenixV01.MixProject do
use Mix.Project
def project do
[
app: :mono_phoenix_v01,
version: "0.1.0",
elixir: "~> 1.12",
elixirc_paths: elixirc_paths(Mix.env()),
# compilers: [] ++ Mix.compilers(), no longer needed in Elixir 1.14+
start_permanent: Mix.env() == :prod,
aliases: aliases(),
deps: deps()
]
end
# Configuration for the OTP application.
#
# Type `mix help compile.app` for more information.
def application do
[
mod: {MonoPhoenixV01.Application, []},
extra_applications: [:logger, :runtime_tools, :os_mon]
]
end
# Specifies which paths to compile per environment.
defp elixirc_paths(:test), do: ["lib", "test/support"]
defp elixirc_paths(_), do: ["lib"]
# Specifies your project dependencies.
#
# Type `mix help deps` for examples and options.
defp deps do
[
{: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.15"},
{:phoenix_live_dashboard, "~> 0.7.2"},
{:esbuild, "~> 0.4", 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}
]
end
# Aliases are shortcuts or tasks specific to the current project.
# For example, to install project dependencies and perform other setup tasks, run:
#
# $ mix setup
#
# See the documentation for `Mix` for more info on aliases.
defp aliases do
[
setup: ["deps.get", "ecto.setup"],
"ecto.setup": ["ecto.create", "ecto.migrate", "run priv/repo/seeds.exs"],
"ecto.reset": ["ecto.drop", "ecto.setup"],
test: ["ecto.create --quiet", "ecto.migrate --quiet", "test"],
"assets.deploy": ["esbuild default --minify", "phx.digest"]
]
end
end
-
router.ex
: (wondering if I’m missing anything here, too, wondering the same about the endpoint file below it)
defmodule MonoPhoenixV01Web.Router do
use MonoPhoenixV01Web, :router
import Redirect
pipeline :browser do
plug(:accepts, ["html"])
plug(:fetch_session)
plug(:fetch_live_flash)
plug(:put_root_layout, {MonoPhoenixV01Web.LayoutView, :root})
plug(:protect_from_forgery)
plug(:put_secure_browser_headers)
end
pipeline :api do
plug(:accepts, ["json"])
end
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("/search_bar", SearchBarLive, :index)
end
## redirects for deep links from other sites. Will not work inside a scope.
# redirect from /men/plays/123 to /men/123
# redirect from /men/plays/123 to /men/123
redirect("/men", "/mens", :permanent, preserve_query_string: true)
redirect("/women", "/womens", :permanent, preserve_query_string: true)
redirect("/men/plays/13", "/men/13", :permanent, preserve_query_string: true)
# omitted from this post: a long list of redirects like the above, for old legacy links on misc external referring sites
# Other scopes may use custom stacks.
# scope "/api", MonoPhoenixV01Web do
# pipe_through :api
# end
# Enables LiveDashboard only for development
#
# 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).
if Mix.env() in [:dev, :test] do
import Phoenix.LiveDashboard.Router
scope "/" do
pipe_through(:browser)
live_dashboard("/dashboard", metrics: MonoPhoenixV01Web.Telemetry)
end
end
# Enables the Swoosh mailbox preview in development.
#
# Note that preview only shows emails that were sent by the same
# node running the Phoenix server.
if Mix.env() == :dev do
scope "/dev" do
pipe_through(:browser)
forward("/mailbox", Plug.Swoosh.MailboxPreview)
end
end
end
-
lib/mono_phoenix_v01_web/endpoint.ex
:
defmodule MonoPhoenixV01Web.Endpoint do
use Phoenix.Endpoint, otp_app: :mono_phoenix_v01
# The session will be stored in the cookie and signed,
# this means its contents can be read but not tampered with.
# Set :encryption_salt if you would also like to encrypt it.
@session_options [
store: :cookie,
key: "_mono_phoenix_v01_key",
signing_salt: "s8druUxv"
]
socket("/live", Phoenix.LiveView.Socket, websocket: [connect_info: [session: @session_options]])
# Serve at "/" the static files from "priv/static" directory.
#
# You should set gzip to true if you are running phx.digest
# when deploying your static files in production.
plug(Plug.Static,
at: "/",
from: :mono_phoenix_v01,
gzip: false,
only: MonoPhoenixV01Web.static_paths()
)
# Code reloading can be explicitly enabled under the
# :code_reloader configuration of your endpoint.
if code_reloading? do
socket("/phoenix/live_reload/socket", Phoenix.LiveReloader.Socket)
plug(Phoenix.LiveReloader)
plug(Phoenix.CodeReloader)
plug(Phoenix.Ecto.CheckRepoStatus, otp_app: :mono_phoenix_v01)
end
plug(Phoenix.LiveDashboard.RequestLogger,
param_key: "request_logger",
cookie_key: "request_logger"
)
plug(Plug.RequestId)
plug(Plug.Telemetry, event_prefix: [:phoenix, :endpoint])
plug(Plug.Parsers,
parsers: [:urlencoded, :multipart, :json],
pass: ["*/*"],
json_decoder: Phoenix.json_library()
)
plug(Plug.MethodOverride)
plug(Plug.Head)
plug(Plug.Session, @session_options)
plug(MonoPhoenixV01Web.Router)
end
Thanks in advance for any help. I’ve been stuck on this for a couple of weeks, and am really looking forward to getting unblocked.