Phoenix 1.7 for Elixir: Edit a Form in a Modal
Tags: elixir, phoenix
Published: 2023-09-20 14:11:58

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](https://github.com/Adzz/petacular)._

## 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`](https://hexdocs.pm/phoenix_live_view/Phoenix.LiveView.JS.html#push/1). 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:

```elixir
<.icon name="hero-cpu-chip" />
```

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:

```elixir
# in lib/petacular_web/router.ex
live("/storybook", PetacularWeb.Pages.StoryBookLive, :show)
```

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):

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

defp icon_names() do
  (File.cwd!() <> "/assets/vendor/heroicons/optimized/20/solid")
  |> Path.expand()
  |> File.ls!()
  |> Enum.map(fn path ->
    "hero-" <> String.replace(path, ".svg", "")
  end)
end
```

Then the markup:

```elixir
# in lib/petacular_web/pages/story_book_live.ex
@impl true
def render(assigns) do
  ~H"""
  <div>
    <h1 class="text-xl mb-4 font-semibold">Storytime</h1>
    <div class="flex flex-col flex-wrap space-evenly">
      <%= for icon_name <- icon_names() do %>
        <section class="p-4 outline outline-1 mb-2 rounded">
          <h2 class="font-semibold"><%= icon_name %></h2>
          <div class="flex space-x-2 p-y-2 p-x-4 mr-2 my-2">
            <CoreComponents.icon name={icon_name} />
            <p>&ltCoreComponents.icon name="<%= icon_name %>"/&gt</p>
          </div>
          <div class="flex space-x-2 p-y-2 p-x-4 mr-2 my-2">
            <CoreComponents.icon name={icon_name<> "-mini"} />
            <p>&ltCoreComponents.icon name="<%= icon_name %>-mini"/&gt</p>
          </div>
          <div class="flex space-x-2 p-y-2 p-x-4 mr-2 my-2">
            <CoreComponents.icon name={icon_name<> "-solid"} />
            <p>&ltCoreComponents.icon name="<%= icon_name %>-solid"/&gt</p>
          </div>
        </section>
      <% end %>
    </div>
  </div>
  """
end
```

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](https://tailwindcss.com/docs/content-configuration#dynamic-class-names).

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:

```js
safelist: [{ pattern: /hero\-.*/ }],
```

Now, we can head to `http://localhost:4000/dev/storybook` and see all the icons. See [this commit](https://github.com/Adzz/petacular/commit/91a0ff0e3af688a47864dc030a83b0f705e8f021) for all of the changes.

### Back to Editing Our Form

Okay, now we can select our edit icon and put it on the page.

```elixir
<PetacularWeb.CoreComponents.icon name="hero-pencil-square-solid" class="mr-2" />
```

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](https://github.com/Adzz/petacular/blob/main/lib/petacular_web/pages/home_live.ex) we used in parts one and two.

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

  @impl true
  def render(assigns) do
    ~H"""
    ...

    <div class="w-50 mb-4">
      <%= for pet <- @pets do %>
        <div class="flex">
          <button phx-click={open_edit_modal(pet.id)}>
            <PetacularWeb.CoreComponents.icon name="hero-pencil-square-solid" class="mr-2" />
          </button>
          <p>Name: <span class="font-semibold"><%= pet.name %></span></p>
        </div>
      <% end %>
    </div>

    ...
    """
  end
```

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:

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

  @impl true
  def render(assigns) do
    ~H"""
  <PetacularWeb.CoreComponents.modal id="edit_modal">
    <h2>Edit a pet.</h2>

    <PetacularWeb.CoreComponents.simple_form for={@edit_changeset} phx-submit="edit_pet">
      <PetacularWeb.CoreComponents.input
        label="Name"
        id="edit_name"
        field={@edit_changeset[:name]}
        value={@edit_changeset[:name].value}
      />
      <:actions>
        <PetacularWeb.CoreComponents.button>
          Save
        </PetacularWeb.CoreComponents.button>
      </:actions>
    </PetacularWeb.CoreComponents.simple_form>
  </PetacularWeb.CoreComponents.modal>
    ...
    """
  end
```

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:

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

@impl true
def mount(_params, _session, socket) do
  default_assigns = %{
    pets: Repo.all(Petacular.Pet),
    edit_form: Phoenix.Component.to_form(Petacular.Pet.create_changeset(%{})),
    create_form: Phoenix.Component.to_form(Petacular.Pet.create_changeset(%{}))
  }

  {:ok, assign(socket, default_assigns)}
end
```

## Add the `open_edit_modal` Function

Now let's implement the `open_edit_modal` function. This has to do two things:

1. Open the modal.
2. Trigger a message to the backend so we can seed the changeset.

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

defp open_edit_modal(pet_id) do
  %JS{}
  |> JS.push("open_edit_modal", value: %{pet_id: pet_id})
  |> PetacularWeb.CoreComponents.show_modal("edit_modal")
end
```

The handler for this event needs to select the relevant pet from the list of pets and put that into the changeset:

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

@impl true
def handle_event("open_edit_modal", %{"pet_id" => id}, socket) do
  pet = Enum.find(socket.assigns.pets, &(&1.id == id))

  new_assigns = %{
    edit_form: Phoenix.Component.to_form(Petacular.Pet.edit_changeset(%{}, pet))
  }

  {:noreply, assign(socket, new_assigns)}
end
```

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:

```elixir
value={@edit_changeset[:name].value |> IO.inspect(limit: :infinity, label: "edit for name:")}
```

### 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](https://github.com/Adzz/petacular/blob/main/lib/petacular_web/components/core_components.ex#L591) has this line, which focuses the first "focussable" element in the modal:

```elixir
|> JS.focus_first(to: "##{id}-content")
```

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](https://hexdocs.pm/phoenix_live_view/form-bindings.html#javascript-client-specifics):

> 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?

1. Remove the auto-focus capabilities of the modal.
2. Have the edit modal focus on something that is not the input first.
3. 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](https://github.com/Adzz/petacular/blob/main/lib/petacular_web/components/core_components.ex#L600) 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](https://developer.mozilla.org/en-US/docs/Web/Accessibility/Understanding_WCAG/Keyboard)
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`](https://github.com/Adzz/petacular/blob/main/lib/petacular_web/pages/home_live.ex#L74) function to accept the name, then use `JS.set_attribute` to set the field value.

```elixir
...
<button phx-click={open_edit_modal(pet.id, pet.name)}>
  <PetacularWeb.CoreComponents.icon name="hero-pencil-square-solid" class="mr-2" />
</button>
...

defp open_edit_modal(pet_id, pet_name) do
  %JS{}
  |> JS.push("open_edit_modal", value: %{pet_id: pet_id})
  |> JS.set_attribute({"value", pet_name}, to: "#edit_name")
  |> PetacularWeb.CoreComponents.show_modal("edit_modal")
end
```

## 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:

```elixir
<%= Phoenix.HTML.Form.hidden_input(f, :id, id: "edit_pet_id_input") %>
```

And set the value when we open the modal:

```elixir
defp open_edit_modal(pet_id, pet_name) do
  %JS{}
  |> JS.push("open_edit_modal", value: %{pet_id: pet_id})
  |> JS.set_attribute({"value", pet_name}, to: "#edit_name")
  |> JS.set_attribute({"value", pet_id}, to: "#edit_pet_id_input")
  # ^^^ this line ^^^^
  |> PetacularWeb.CoreComponents.show_modal("edit_modal")
end
```

We will see the id of the pet appear in the params, allowing us to select the pet we are editing from the assigns:

```elixir
@impl true
def handle_event("edit_pet", %{"pet" => %{"id" => id} = params}, socket) do
  pet = Enum.find(socket.assigns.pets, &(&1.id == String.to_integer(id)))

  case Repo.insert(Petacular.Pet.edit_changeset(params, pet)) do
    {:error, message} ->
      {:noreply, socket |> put_flash(:error, inspect(message))}

    {:ok, _} ->
      new_assigns = %{
        pets: Repo.all(Petacular.Pet),
        edit_form: Phoenix.Component.to_form(Petacular.Pet.create_changeset(%{}))
      }

      socket =
        socket
        |> assign(new_assigns)
        |> push_event("close_modal", %{to: "#close_modal_btn_edit_modal"})

      {:noreply, socket}
  end
end
```

See [this commit](https://github.com/Adzz/petacular/commit/364bec4c35c796bcd19f63ccd42ad230c0ed4afc) 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](/elixir-alchemy)!**