Create and Open a Modal in Phoenix 1.7

itizadz

Adz

Posted on June 27, 2023

Create and Open a Modal in Phoenix 1.7

Phoenix 1.7 came out this year with a whole host of exciting features, including verified routes and some great built-in Tailwind components. These components are a fantastic start, but they are not made to be a fully general design system. We should expect to modify components to fit our specific needs. However, knowing where to start can be difficult.

In this three-part series, we'll take a fresh Phoenix app and create a working UI using generated components.

In this part, we will add a modal to a page and open it on demand.

Let's get started!

The UI of Our Phoenix App — Setup

The UI we will implement is a list of data and two modals for that data: a create modal and an edit modal. Our example will be a spectacular pet shop called Petacular.

To begin, let's bootstrap the app. If you wish to write the command yourself, first ensure you have the latest Phoenix installer with:

mix archive.install hex phx_new
Enter fullscreen mode Exit fullscreen mode

Then run:

mix phx.new petacular
Enter fullscreen mode Exit fullscreen mode

This will bootstrap the project and generate the core components we'll use in our examples. To illustrate the various steps in this article, I have included a companion repo with commits for each step. This commit shows us everything that gets generated in the above command. Of particular interest is this module which contains all of the generated components we will be exploring.

The components use Tailwind CSS for styling and are passed attributes and/or slots, as appropriate. We will leverage a few below, but first, we will need a database.

Check out this commit for everything we need to get a db working with docker. This commit adds a migration and creates a few schemas we will use for our example: a list of pets and their preferences. Finally, you can see how this commit makes the home page a LiveView page and introduces some very basic styling.

What Is a Modal in Phoenix?

The initial homepage looks like this (found in lib/petacular_web/pages/home_live.ex):

@impl true
def render(assigns) do
  ~H"""
  <h1 class="font-semibold text-3xl mb-4">Pets</h1>
  <PetacularWeb.CoreComponents.button>
    Add New Pet +
  </PetacularWeb.CoreComponents.button>
  """
end
Enter fullscreen mode Exit fullscreen mode

I have included a built-in component from the generated PetacularWeb.CoreComponents module (found in lib/petacular_web/components/core_components.ex) — a button. The button is defined here and it is simple to use:

# in /lib/petacular_web/pages/home_live.ex
...
~H"""
    <PetacularWeb.CoreComponents.button>
      Add New Pet +
    </PetacularWeb.CoreComponents.button>
"""
...
Enter fullscreen mode Exit fullscreen mode

We would love for this to open a modal that contains a form for adding a pet. To do that, let's look at the modal in the PetacularWeb.CoreComponents module.

This is a function component, meaning it does not have a state. It's just a bundle of markup and styling. It has some attrs — for example, attr :show, :boolean, default: false, and one slot.

What Are attrs and Slots?

An attr defines data that's expected to pass. Some attrs are required, and some have a default value instead. A slot is space for nested HTML and can be named or not. In essence, slots let you provide your own markup and appear as children of a function component's element.

We can see the slot is called :inner_block, a special name for the modal. Everything inside of the <.modal> tags will be treated as the :inner_block slot. So, in the function docs, the modal component looks like this:

<.modal id="confirm-modal">
  This is a modal.
</.modal>
Enter fullscreen mode Exit fullscreen mode

The text This is a modal. is treated as the :inner_block. In contrast, we can name a slot. Then we designate the contents of that slot by putting content in between two tags that use the slot's name. For example, CoreComponents has a <.header> component with an optional :subtitle slot. To provide a subtitle, we do this:

<CoreComponents.header>
  This is my title.
  <:subtitle>
    This is my subtitle. There are many like it but this one is mine.
  </:subtitle>
</CoreComponents.header>
Enter fullscreen mode Exit fullscreen mode

Making the Modal Appear in Phoenix

The docs for the modal give us an indication of what the modal needs to look for. We need to provide an ID that is targeted by the hide_modal and show_modal functions. Let's look at how they work first. The show modal is defined like this:

# in lib/petacular_web/components/core_components.ex

def show_modal(js \\ %JS{}, id) when is_binary(id) do
  js
  |> JS.show(to: "##{id}")
  |> JS.show(
    to: "##{id}-bg",
    transition: {"transition-all transform ease-out duration-300", "opacity-0", "opacity-100"}
  )
  |> show("##{id}-container")
  |> JS.add_class("overflow-hidden", to: "body")
  |> JS.focus_first(to: "##{id}-content")
end

def show(js \\ %JS{}, selector) do
  JS.show(js,
    to: selector,
    transition:
      {"transition-all transform ease-out duration-300",
       "opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95",
       "opacity-100 translate-y-0 sm:scale-100"}
  )
end
Enter fullscreen mode Exit fullscreen mode

This uses the new(ish) JS module to execute simple JavaScript functions. It accepts and uses an ID to identify the element we want to show. Upon appearing, the show modal does some simple animations to ease it into view and focuses the page onto the first focus-able element, if there is one.

hide_modal does the same, but in reverse:

# in lib/petacular_web/components/core_components.ex

def hide_modal(js \\ %JS{}, id) do
  js
  |> JS.hide(
    to: "##{id}-bg",
    transition: {"transition-all transform ease-in duration-200", "opacity-100", "opacity-0"}
  )
  |> hide("##{id}-container")
  |> JS.hide(to: "##{id}", transition: {"block", "block", "hidden"})
  |> JS.remove_class("overflow-hidden", to: "body")
  |> JS.pop_focus()
end

def hide(js \\ %JS{}, selector) do
  JS.hide(js,
    to: selector,
    time: 200,
    transition:
      {"transition-all transform ease-in duration-200",
       "opacity-100 translate-y-0 sm:scale-100",
       "opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"}
  )
end
Enter fullscreen mode Exit fullscreen mode

Calling the show_modal Function

With a button click, we call the show_modal function to make the modal appear. To close the modal, we need a button on the modal itself that calls hide_modal. Luckily, this is implemented for us, so we don't need to worry about it.

From our function docs, we see that we need some content inside the modal. Like the button we used earlier, the modal uses an :inner_block slot. That means anything inside the modal tags will appear on the page as the :inner_block. We can keep this very simple. Something like the following will work for now:

# in /lib/petacular_web/pages/home_live.ex

<PetacularWeb.CoreComponents.modal id="create_modal">
  <h2>Add a pet.</h2>
</PetacularWeb.CoreComponents.modal>
Enter fullscreen mode Exit fullscreen mode

We can put this inside our homepage and have a click trigger the show_modal function by adding phx-click onto the button we used earlier. Usually, we set phx-click to a string, and clicking it sends an event to the backend using that string as the event name. But we may also provide a JS function or a chain of them:

# in /lib/petacular_web/pages/home_live.ex

<PetacularWeb.CoreComponents.button phx-click={
  PetacularWeb.CoreComponents.show_modal("create_modal")
}>
  Add New Pet +
</PetacularWeb.CoreComponents.button>
Enter fullscreen mode Exit fullscreen mode

Putting it all together, we end up with something like this:

# in /lib/petacular_web/pages/home_live.ex

~H"""
  <h1 class="font-semibold text-3xl mb-4">Pets</h1>

  <PetacularWeb.CoreComponents.modal id="create_modal">
    <h2>Add a pet.</h2>
  </PetacularWeb.CoreComponents.modal>

  <PetacularWeb.CoreComponents.button phx-click={
    PetacularWeb.CoreComponents.show_modal("create_modal")
  }>
    Add New Pet +
  </PetacularWeb.CoreComponents.button>
"""
Enter fullscreen mode Exit fullscreen mode

Now when you click the button, the modal will open! 🎉

See the full diff of changes.

Wrapping Up

In this post, we used Phoenix 1.7's generated core components to create a modal and open it.

In part two, we'll add the edit form.

Until then, happy coding!

P.S. If you'd like to read Elixir Alchemy posts as soon as they get off the press, subscribe to our Elixir Alchemy newsletter and never miss a single post!

💖 💪 🙅 🚩
itizadz
Adz

Posted on June 27, 2023

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related