Layouts & Navbar

DANA 325 D R A P M H B G Q S I W J T X C U V K N E F L O

Objectives

not yet graded
  • Your Navbar should be implemented as a function component as part of your UI library. Your App layout should use it via <navbar /> and your pages should use the default app layout. (1 Point)

  • The modal is an exact visual match to the flowbite default component. (1 Point)

  • The modal supports all given attributes (2 Points)

  • Clicking on the x button or outside the modal closes the modal. Clicking on a button with the attribute phx-click="open_modal()" will open it. (2 Points)

  • 100% Test Coverage on both navbar and modal components. Make sure you are actually creating test modules, not just placing the components on a page. You don't need to test JavaScript behavior such as the clicking the toggle menu / modal button. (1 Point)

Deliverables from Mo, 01/27

not yet graded
  • Controller contains a module attribute '@courses' that correctly forwards appropriate course data from at least the last two semesters to a controller action courses/2 (1 Point)

  • All Courses are displayed in form of a table when visiting /courses. They must not be hard-coded into the template. (2 Points)

  • Visiting /courses/spring_2025 and /courses/fall_2024 yields different courses in the table (2 Points)

  • Your controller action must be guarded to only allow slugs for semesters you have implemented data for. (1 Point)

  • Your table uses some tailwind css classes such as colors, padding or borders for styling. (1 Point)

Deliverables from We, 01/29

not yet graded
  • lists planets or planet names either sorted by name or default order (1 Point)

  • lists planet by id if n is a valid id, it show information on that planet (1 Point)

  • should randomly select a planet and show its data (1 Point)

  • You managed to display a TestCoverage report of your project. (1 Point)

  • TestCoverage report should indicate 100% test coverage for the PlanetController (3 Points)

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="[[&quot;toggle_class&quot;,{&quot;names&quot;:[&quot;hidden&quot;],&quot;to&quot;:&quot;#menu&quot;}],[&quot;toggle_attr&quot;,{&quot;to&quot;:&quot;#menu-button&quot;,&quot;attr&quot;:[&quot;aria-expanded&quot;,&quot;true&quot;,&quot;false&quot;]}]]"
  class={...}
  aria-controls="menu"
  aria-expanded="false"
>
  ...
</button>

If we replace all the &quot 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:

  1. AI can be a fantastic time saver to testing
  2. 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.

Copyright © 2025 Alexander Fuchsberger, Bucknell University. All rights reserved.