Adz
Posted on June 27, 2023
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
Then run:
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 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
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>
"""
...
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 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, the modal component looks like this:
<.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>
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>
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
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
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>
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>
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>
"""
Now when you click the button, the modal will open! 🎉
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!
Posted on June 27, 2023
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.