Beter data-confirm modals in Phoenix LiveView
Tags: webdev, elixir, phoenix, liveview
Published: 2024-04-22 08:08:51

## TLDR; here's the result

{% embed https://youtu.be/cXj-qCByHLM %}

### The default experience

I really loved that Phoenix provided a nice plugin for having quick confirm behaviours without needing to do a lot of juggling things. While the behaviour works for testing it's really lacking UX for production use.

{% embed https://youtu.be/NBb0uArop_g %}

And as it turns out it's not really clear how to customise the behaviour, as out of the box it uses window.confirm which awaits the action before continuing on.

So if you want this behaviour you need to implement some sort of preventDefault, but execute after method.

Well you're in luck as today i've finally got around to writing this post.

### The setup

First I've generated a phoenix application

```bash
mix phx.new better_data_confirm --databse sqlite3
```

Next I've generated a Posts resource so that i could show this behaviour without too much hassle:

```bash
 mix phx.gen.live Blog Post posts title:string content:string
```

That will get you to the default behaviour video above

### Adding the danger_dialog

Next i've added a dialog element to the `lib/better_data_confirm_web/components/layouts/root.html.heex` root layout

Before:

```elixir
  <body class="bg-white antialiased">
    <%= @inner_content %>
  </body>
```

After:

```elixir
  <body class="bg-white antialiased">
    <%= @inner_content %>
    <dialog
      id="danger_dialog"
      class="backdrop:bg-slate-800/75 shadow-xl rounded-md bg-white p-6 border"
    >
      <form method="dialog" class="grid gap-6 place-items-center">
        <h1 class="text-2xl" data-ref="title">
          Are you sure?
        </h1>
        <div class="flex gap-4 items-center justify-end">
          <.button data-ref="cancel" type="submit" value="cancel">
            Cancel
          </.button>
          <.button data-ref="confirm" type="submit" value="confirm" class="bg-red-500">
            Confirm
          </.button>
        </div>
      </form>
    </dialog>
  </body>
```

Now this is up to your design choices and so on, you can have translated string, aria attributes, animations all that jazz, i'll link to a few good posts about that in the end.

But for the basic example it's a simple dialog with some styling and that's all.

### Overriding the default behaviour

next in the `assets/js/app.js` we need to add the overriding behaviour, pop this code at the end of the file.

```js
// Attribute which we use to re-trigger the click event
const CONFIRM_ATTRIBUTE = "data-confirm-fired"

// our dialog from the `root.html.heex`
const DANGER_DIALOG = document.getElementById("danger_dialog");

// Here we override the behaviour
document.body.addEventListener('phoenix.link.click', function (event) {
  // we prevent the default handling of this by phoenix html
  event.stopPropagation();

  // grab the target
  const { target: targetButton } = event;
  const title = targetButton.getAttribute("data-confirm");

  // if the target does not have `data-confirm` we simply ignore and continue
  if (!title) { return true; }

  // For re-triggering the click event
  if (targetButton.hasAttribute(CONFIRM_ATTRIBUTE)) {
    targetButton.removeAttribute(CONFIRM_ATTRIBUTE)
    return true;
  }

  // We do this since `window.confirm` prevents all execution by default.
  // To recreate this behaviour we `preventDefault` 
  // Then add an attribute which will allow us to re-trigger the click event while skipping the dialog
  event.preventDefault();
  targetButton.setAttribute(CONFIRM_ATTRIBUTE, "")

  // Reset the `returnValue` as otherwise on keyboard `Esc` it will simply take the most recent `returnValue`, causing all sorts of issues :D
  DANGER_DIALOG.returnValue = "cancel";

  // We use the title, which is nice we can have translated titles
  DANGER_DIALOG.querySelector("[data-ref='title']").innerText = title;


  // <dialog> is a very cool element and provides a lot of cool things out of the box, like showing the modal in the #top-layer
  DANGER_DIALOG.showModal();

  // Re-triggering logic
  DANGER_DIALOG.addEventListener('close', ({ target }) => {
    if (target.returnValue === "confirm") {
      // we re-trigger the click event
      // since we have the attribute set. This will just execute the click event
      targetButton.click();
    } else {
      // Remove the attribute on cancel as otherwise the next click would execute the click event without the dialog
      targetButton.removeAttribute(CONFIRM_ATTRIBUTE);
    }
  // once: true, automatically remove the listener after first execution
  }, { once: true })

}, false)
```

### And Voila

You now have a customised modal for data-confirm!
You can add any other things you need!

- allow customising button texts
- add a message
- translate
- change icons
- animate the appearance/hiding of the modal
- have different modal types danger/success/prompt

The sky is the limit...

### Read more to create even better ux:

https://www.scelto.no/blog/promise-based-dialog
https://developer.chrome.com/blog/entry-exit-animations

### Don't forget to leave a like!

Good luck with your adventures! If you enjoyed the post, likes are very appreciated, even better if you share this :D