Create and Open a Modal in Phoenix 1.7
Tags: elixir, phoenix
Published: 2023-06-27 11:00:00

Phoenix 1.7 came out this year with a whole host of exciting features, including [verified routes](https://hexdocs.pm/phoenix/Phoenix.VerifiedRoutes.html) and some great built-in [Tailwind](https://tailwindcss.com/) 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:

```sh
mix archive.install hex phx_new
```

Then run:

```sh
mix phx.new petacular
```

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](https://github.com/Adzz/petacular) with commits for each step. [This commit](https://github.com/Adzz/petacular/commit/9f4233394a23423c5a5b71e851c173d2debb0556) shows us everything that gets generated in the above command. Of particular interest is [this module](https://github.com/Adzz/petacular/blob/9f4233394a23423c5a5b71e851c173d2debb0556/lib/petacular_web/components/core_components.ex) 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](https://github.com/Adzz/petacular/commit/df785b8cd2a470e6d2a52950c39c521f5e5368ee) for everything we need to get a db working with docker. [This commit](https://github.com/Adzz/petacular/commit/abbdb457a780125cf66f041ff3eb5f3952be6866) 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](https://github.com/Adzz/petacular/commit/8a1b3f4bfa96d41afd674ac0ce102f9c33bcb62b) 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`):

```elixir
@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
```

I have included a built-in component from the generated [`PetacularWeb.CoreComponents`](https://github.com/Adzz/petacular/blob/main/lib/petacular_web/components/core_components.ex) module (found in `lib/petacular_web/components/core_components.ex`) — a button. The [button is defined here](https://github.com/Adzz/petacular/blob/main/lib/petacular_web/components/core_components.ex#L213) and it is simple to use:

```elixir
# in /lib/petacular_web/pages/home_live.ex
...
~H"""
    <PetacularWeb.CoreComponents.button>
      Add New Pet +
    </PetacularWeb.CoreComponents.button>
"""
...
```

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](https://github.com/Adzz/petacular/blob/main/lib/petacular_web/components/core_components.ex#L44) 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`](https://hexdocs.pm/phoenix_live_view/Phoenix.Component.html#module-attributes) — for example, `attr :show, :boolean, default: false`, and one [slot](https://hexdocs.pm/phoenix_live_view/Phoenix.Component.html#module-slots).

### What Are `attr`s and Slots?

An `attr` defines data that's expected to pass. Some `attr`s 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](https://github.com/Adzz/petacular/blob/main/lib/petacular_web/components/core_components.ex#L27), the modal component looks like this:

```elixir
<.modal id="confirm-modal">
  This is a modal.
</.modal>
```

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>`](https://github.com/Adzz/petacular/blob/main/lib/petacular_web/components/core_components.ex#L398) component with an optional `:subtitle` slot. To provide a subtitle, we do this:

```elixir
<CoreComponents.header>
  This is my title.
  <:subtitle>
    This is my subtitle. There are many like it but this one is mine.
  </:subtitle>
</CoreComponents.header>
```

## Making the Modal Appear in Phoenix

The [docs for the modal](https://github.com/Adzz/petacular/blob/main/lib/petacular_web/components/core_components.ex#L27) 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`](https://github.com/Adzz/petacular/blob/main/lib/petacular_web/components/core_components.ex#L603) and [`show_modal`](https://github.com/Adzz/petacular/blob/main/lib/petacular_web/components/core_components.ex#L591) functions. Let's look at how they work first. The show modal is defined like this:

```elixir
# 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
```

This uses the new(ish) [`JS` module](https://hexdocs.pm/phoenix_live_view/Phoenix.LiveView.JS.html) 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:

```elixir
# 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
```

### 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](https://github.com/Adzz/petacular/blob/main/lib/petacular_web/components/core_components.ex#L72), so we don't need to worry about it.

From [our function docs](https://github.com/Adzz/petacular/blob/main/lib/petacular_web/components/core_components.ex#L34), 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:

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

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

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:

```elixir
# 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>
```

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

```elixir
# 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>
"""
```

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

[See the full diff of changes](https://github.com/Adzz/petacular/commit/dde845c66ed42d7d90290ff19d4a1d5052f556ed).

## 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](/elixir-alchemy)!**