In the videos we implemented a navbar with a working toggle button for a mobile menu. Lets revisit this part in the navbar module again:
defp toggle_menu do
%JS{}
|> JS.toggle_class("hidden", to: "#menu")
|> JS.toggle_attribute({"aria-expanded", "true", "false"}, to: "#menu-button")
end
We applied this function to a <button>
element via an attribute phx-click
:
<button
id="menu-button"
type="button"
class={...}
aria-controls="menu"
aria-expanded="false"
phx-click={toggle_menu()}
>
...
</button>
This doesn't look like it but this function actually creates JavaScript code. Looking at the button element via the Webinspector we see something like this:
<button
id="menu-button"
type="button"
phx-click="[["toggle_class",{"names":["hidden"],"to":"#menu"}],["toggle_attr",{"to":"#menu-button","attr":["aria-expanded","true","false"]}]]"
class={...}
aria-controls="menu"
aria-expanded="false"
>
...
</button>
If we replace all the "
html-entities with "
its a bit easier to decipher that message:
[["toggle_class",{"names":["hidden"],"to":"#menu"}],["toggle_attr",{"to":"#menu-button","attr":["aria-expanded","true","false"]}]]
Upon clicking the button this string will be converted in a JS function and exectued on the fly! We ended up only writing 3
lines of code compared to the 172 lines that Flowbite needed to achieve the same functionality. In all fairness there are some optionals that we skipped but I hope you start to see how LiveView can reduce the workload for us Full Stack Web Developers. And the JS library is only a small part of LiveView. You will be more excited once you hear what else we can do with it.
Objective 1 Navbar Implementation
Implement the navbar layout like I showed in the video and ensure its used in all three pages (/planets
, /courses
, /
).
Make sure sample links from the Flowbite navbar are replaced with links to your pages:
<.link href={~p"/"} class="...">
Home
</.link>
In the video I have had an issue with the ~p" "
sigil not being recognized.
In the video i attempted to add
use Phoenix.VerifiedRoutes,
endpoint: AppWeb.Endpoint,
router: AppWeb.Router,
statics: AppWeb.static_paths()
to AppWeb.Layouts
instead of the navbar component (AppWeb.Components.UI.Navbar
).
Enabling the verified routes system in the correct module will resolve the issue.
You will earn the point if you have a working navbar hamburger menu that is not depending on the Flowbite (or any other JavaScript module). It must be implemented using the Phoenix.LiveView.JS
module.
Objective 2-4 Modal component
Its time to create our first functional component with Javascript behavior ourselves. For the purpose of this example you can assume we will only ever create and display a single modal on any given point in time. The reason for this is that an id must be unique per page:
<div id="unique" />
We need the id to identify and show/hide the modal component thus this predicament. We will later revisit the modal again and not only deal with with the uniqueness problem, but also add functionality such as being able to open/close a modal from the server side, not just the client.
Please create a modal UI Component. I have started it for you:
defmodule AppWeb.Components.UI.Modal do
use Phoenix.Component
alias Phoenix.LiveView.JS
@doc """
Renders a Flowbite [Modal](https://flowbite.com/docs/components/modal).
## Examples
<.modal heading="Great Modal" backdrop="static" small>
This is my fantastic modal.
</.modal>
"""
attr :backdrop, :string,
default: "dynamic",
values: ~w(dynamic static),
doc: "Choose between static or dynamic to prevent closing the modal when clicking outside."
attr :heading, :string, required: true, doc: "Will be displayed left to the close button."
attr :small, :boolean, default: false,
doc: "If true, restricts the width of the modal according to the small modal variant in the Flowbite library. Will use default to the "large" modal variant otherwise"
slot :inner_block, required: true
def modal(assigns) do
# TODO
~H"""
"""
end
def open_modal do
# TODO
end
def close_modal do
# TODO
end
end
Try to not to just feed this whole beast to AI at once. Try to implement the features step by step yourself and ask AI only once you get stuck on specifics. Like humans it can get confused easily when you ask too much of it at once. In case it doesn't get confused you will have learned little and will be bored for the rest of the lecture.
If you are a seasoned web developer already you might be thinking, "Wait a minute Alex, what nonsense. For a modal we should be using a <dialog>
element!" I totally agree with you and a dialog is a better choice for multiple reasons. You can ask AI what those are. A few advanced features such as animating the backdrop are however not yet fully supported by all browsers making a <div
element just a little more versatile for now. There is a good chance that in less than half a year I might revise my stance on that. As I said in the past, web development is, and has always been, rapidly changing.
For the modal you will get
- a point if the modal is an exact visual match to the flowbite default component. You don't need to copy those buttons inside the modal (cancel, I accept) and please remember the modal content should come through your inner-block and heading.
- 2 points if ALL attributes of the functional component (see code above) are propery implemented and working as they are supposed to
- 2 points for if can demonstrate on a page that clicking a button opens the modal and clicking on the x (and outside the modal on dynamic modals) closes the modal.
Objective 5 Testing
You will fullfill this objective if you implemented the navbar and modal as a UI components in your UI library and have created test modules for them that bring your test coverage of both modules to 100%.
When testing elixir code, ChatGPT really shines. This was the response when I asked it to test my Navbar module:
defmodule AppWeb.Components.UI.NavbarTest do
use AppWeb.ConnCase, async: true
import Phoenix.LiveViewTest
import Phoenix.Component
alias AppWeb.Components.UI.Navbar
test "renders navbar correctly" do
assigns = %{}
html = render_component(&Navbar.navbar/1, assigns)
assert html =~ "<nav"
assert html =~ "Flowbite"
assert html =~ "Home"
assert html =~ "Courses"
end
test "menu button has correct attributes" do
assigns = %{}
html = render_component(&Navbar.navbar/1, assigns)
assert html =~ "id=\"menu-button\""
assert html =~ "aria-controls=\"menu\""
assert html =~ "aria-expanded=\"false\""
end
test "menu toggle JS actions" do
js = Navbar.send(:toggle_menu)
assert js.actions |> Enum.any?(fn {_, action} -> action.to == "#menu" end)
assert js.actions |> Enum.any?(fn {_, action} -> action.to == "#menu-button" end)
end
test "menu close JS actions" do
js = Navbar.send(:close_menu)
assert js.actions |> Enum.any?(fn {_, action} -> action.to == "#menu" end)
assert js.actions |> Enum.any?(fn {_, action} -> action.to == "#menu-button" end)
end
end
I am showing this example for two reasons:
- AI can be a fantastic time saver to testing
- The AI knowledge base is sometimes outdated and we can't trust it to do its job properly.
Always verify tests make sense and work.
In this case ChatGPT tried to combine a popular testing function send
with our code in a way that made little sense so we need to eventually fix testing the button click ourself.
The Testing Function Components part of the documentation is a greater source than my writeup could possibly be so you should read more about it.
Without having a full live view (yet) it is however difficult to test click and link behavior. So I just removed the last two tests related to JS for now.
However when testing functional components with multiple variants always make sure to test all variants. In this case dont forget to test "small modals" and/or "static" modals.