In part one of this series, we introduced the CoreComponents
that get generated when bootstrapping a new
Phoenix project. In part two, we implemented a create modal.
Now, we will implement an edit modal.
You can continue following along with our companion repo.
Editing a Form in a Modal
You will first notice that each item will need a different changeset. We want to edit each item, so we need to be able to build a changeset from a different struct each time.
You could do this by iterating over all of the items in mount
and rendering a different modal for every row, but this won't work at all. You would have to have
one changeset per assign, which doesn't work when you have a list to add to. It would
also mean a lot more HTML because you'd render the whole modal once per row. It's an all-round bad idea.
Instead, we need a way to build the correct changeset based on the item we click on.
We can do that by using another JS
function — push
.
This will push an event to the backend, along with any attributes that we want to send. If we add an edit button per row, the click action can push an event to the backend
with the pet_id
as a param. Then, we can select the pet from the list of assigns and build
a changeset out of it.
First, add the button to the markup. It might be nice to use an icon for this, so let's take a quick detour to icons.
Icons in Phoenix
Phoenix 1.7 ships with a vendored heroicons library and an <.icon>
component in CoreComponents
.
It works by supplying the name of an icon as a name
attr, like so:
The names for the available icons are the filenames contained in the following
path: assets/vendor/heroicons/optimized/20/solid/
.
Looking at the files doesn't tell us much about what they look like because we just see svg
markup.
What would be cool is if we could render a dev-only route that displays all icons on one page. Then when we consider using an icon, we can go to that page and peruse them all at our leisure.
First, let's add the route:
Then, we can make the necessary PetacularWeb.Pages.StoryBookLive
module. We'll now write a function that
generates all the icon names from the files in the assets folder, then iterate over them
and create an icon from each one. This will give us a dynamic list of icons to render.
Here are the icon_names
(this assumes you will start your server from the project's route):
Then the markup:
There is one more thing to do, though. Tailwind will purge all classes it doesn't see being used when the app is built. Usually, this is great because it means the bundle size is smaller, with more lightweight pages. However, here, it is going to bite us. When you refer to classes dynamically, Tailwind doesn't see those classes being used, so it purges them. Tailwind's docs warn about this.
We need to tell Tailwind not to purge all the icon modules so we can render them. We do that by adding a line of config into tailwind.config.js
, like so:
Now, we can head to http://localhost:4000/dev/storybook
and see all the icons. See
this commit
for all of the changes.
Back to Editing Our Form
Okay, now we can select our edit icon and put it on the page.
We will put this in a button and add a phx-click
that opens our edit modal for us. All
this can live in the homepage we used in parts one and two.
We also need to create our Edit
modal. This will be similar to our Create
modal, but a bit different, so we'll just create a new modal.
We can put this in our ~H
component just before the other modal:
Now we need to add a changeset to the assigns. Initially, we can put any changeset in mount because when we open the modal, we are going to seed it:
Add the open_edit_modal
Function
Now let's implement the open_edit_modal
function. This has to do two things:
- Open the modal.
- Trigger a message to the backend so we can seed the changeset.
The handler for this event needs to select the relevant pet from the list of pets and put that into the changeset:
When we open the modal, the page will re-render the form because the changeset has changed,
and the form will be seeded with the correct data. We can verify this by using
|> IO.inspect(limit: :infinity, label: "")
on the form value:
Debugging an Issue with the open_edit_modal
If you open the modal, you will see the correct value printed. But there is a problem — it's not showing on the page! What on earth could be the issue? This one is a doozy, so I will save you some hours of debugging.
The function that we use to open our modal has this line, which focuses the first "focussable" element in the modal:
This is done for accessibility, and so is generally a good idea.
The input happens to be the first focussable thing in our edit form. Phoenix also ensures that the client is the source of truth for an input's value:
For any given input with focus, LiveView will never overwrite the input's current value, even if it deviates from the server's rendered updates.
So what happens in our case? We push an asynchronous message to the backend, which changes an assign (the edit
form), causing a re-render — |> JS.push("open_edit_modal", value: %{pet_id: pet_id})
. Then we
open the modal with JavaScript, but because the message to the server is async, the modal
opens before we get a reply. The first focussable element is the input field, so that gets
focus, then the server responds. This would normally re-render the input field, but now won't, because the input has focus!
Fixing the Issue
We've done everything right, yet are left adrift. What are our options?
- Remove the auto-focus capabilities of the modal.
- Have the edit modal focus on something that is not the input first.
- Somehow make the form opening synchronous to the server message.
I honestly don't know which is better, but let's reason them out. One is easy to do — just remove this line
from the show_modal
function — but may have accessibility implications, which makes it a non-starter.
The second option seems reasonable at first. We could maybe set the tabindex
on the heading,
but mdn
recommends the following:
If an element can be focused using the keyboard, then it should be interactive; that is, the user should be able to do something to it and produce a change of some kind (for example, activating a link or changing an option).
So that's out.
The third option is possible, but requires some ceremony. Instead of opening the modal with JS, you could have the backend trigger a JS event that opens the modal when it's finished seeding the changeset. That requires adding a handler in JS to listen for the event and also means that the modal open is slower, because it requires at least one round trip to the server. For me, that is out as well.
The solution? A secret fourth option — set the value of the field with JS. This means the field will open quickly, be set to the correct value, and auto-focus.
To support that, we alter our open_edit_modal
function to accept the name, then use JS.set_attribute
to set the field value.
Implementing the Update in Our Phoenix Application
Now the only thing left to do is implement the edit_pet
handler. This is similar to the
create version, where we flash an error and close the modal on success. We first want to
select the pet we are editing, which means we need the pet's id. How can we get that?
The easiest way is to use a hidden input on the form. That way, when the form is submitted, the pet id will also be sent. To do that, we need to add the hidden input:
And set the value when we open the modal:
We will see the id of the pet appear in the params, allowing us to select the pet we are editing from the assigns:
See this commit for all the relevant changes.
And with that, we are done!
Wrapping Up
This concludes our three-part series in which we took a fresh Phoenix 1.7 application and built a create and edit modal for it.
Hopefully, this gives you some new ideas you can extend and implement for your own apps.
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!