Controlled hallucination

Learning Elm Part 9 - Confirmation Dialogs

Published: - Reading time: 6 minutes

This will probably be the shortest post yet in this series. Do I even need a table of contents? Why not.

In the previous post I added the bulk of what I find most useful in this small webapp: auto-complete similar transactions and an advanced edit mode.

There are several small UX tweaks remaining to be done, and in this post I will address a small one, preventing accidental deletion with a simple confirmation request to the user. In the process, I’ll learn a bit more on how to integrate Semantic UI with Elm.

As always, you can find a deployed version here, with the Elm Debugger here, and the source code here.

Table of contents

Adding a Semantic UI modal

Now that I am closer to using the webapp myself, I will add a basic but useful feature, which is to confirm I want to delete something. Sometimes I am walking and typing and I wouldn’t want to accidentally delete what I just stored in the app!

If I was writing a normal HTML/JS webpage, this would be simple. I had thought the same thing, a few weeks ago, when I was browsing Semantic UI menus. But then I realized that I had chosen to use a UI framework that relies, for certain things, heavily on JavaScript, and talking to JavaScript from Elm was… well, boring, to be honest. The combination of both things made me rethink my choices.

Anyway, back to confirmation dialogs! I chose to use a simple Semantic UI modal to ask the user if she was sure she wanted to delete the transaction. I modified the examples in the documentation, and added a small Elm function in my src/Misc.elm file:

viewConfirmModal : Html msg
viewConfirmModal =
    div [ class "ui mini modal" ]
        [ div [ class "content" ]
            [ p [] [ text "Are you sure?" ]
            ]
        , div [ class "actions" ]
            [ div [ class "ui red cancel inverted button", cyAttr "cancel-modal" ]
                [ i [ class "remove icon" ] []
                , text "No"
                ]
            , div [ class "ui green ok inverted button", cyAttr "confirm-modal" ]
                [ i [ class "checkmark icon" ] []
                , text "Yes"
                ]
            ]
        ]

(As an aside, I should really look into some HTML-to-Elm tool, I remember hearing about one in some podcast, maybe html-to-elm?)

You might be wondering:

  1. Why is there no input?
  2. Where are the messages/onClick?

The answer one, because this is a static HTML, and two, I just gave up and used Semantic UI as close as it was meant to be: through JavaScript.

This means I needed to add the ports in my src/EditTransaction.elm file:

port showDeleteModal : () -> Cmd msg
port cancelDelete : (Json.Decode.Value -> msg) -> Sub msg
port confirmDelete : (Json.Decode.Value -> msg) -> Sub msg

And a corresponding counterparts in JavaScript land:

app.ports.showDeleteModal.subscribe(() => {
  $(".ui.modal")
    .modal({
      detachable: false,
      closable: false,
      onDeny: () => {
        app.ports.cancelDelete.send();
        return true;
      },
      onApprove: () => {
        app.ports.confirmDelete.send();
        return true;
      },
    })
    .modal("show");
});
  1. Build the modal, this hooks up the HTML, does the JavaScript magic, adds the dimmer background, etc.
  2. Pass detachable = false because Semantic UI tries to optimize things and detaches the modal from the DOM, but I’d like to do that myself in the re-renders in Elm (if not, I’ll get multiple modals).
  3. Pass the closable = false. Clicking on the dimmer dismisses the modal, but Semantic UI does not consider this a denial - though I do… and I can’t attach to the onHide because it gets called when dismissed, denied and approved. So I kept it simple, users need to click No.
  4. Tell Elm the user clicked cancel.
  5. Tell Elm the user clicked confirm.

Then I replaced my DeleteTransaction String String Msg variant with three new ones:

type Msg
    = EditDate String
-- (...)
    | DeleteRequested
    | DeleteCancelled
    | DeleteConfirmed
-- (...)

update : Msg -> State -> ( State, Cmd Msg, Bool )
update msg model =
    case msg of
        DeleteRequested ->
            ( model, showDeleteModal (), False )

        DeleteCancelled ->
            ( model, Cmd.none, False )

        DeleteConfirmed ->
            ( model, deleteTransaction ( model.input.id, model.input.version ), True )
-- (...)
  1. DeleteRequested tells JavaScript-land to show the modal.
  2. DeleteCancelled is a no-op… when it comes to Elm, I am not tracking the modal. I could even delete the callback and the Msg variant!
  3. DeleteConfirmed replaces the old DeleteTransaction String String message, and calls JavaScript-land to delete the transaction.

I then rinses-and-repeated to do the same for the “Delete All Data” functionality, ending up with some duplication, but well, it is good enough for now.

Not an impressive result, but this is what it looks like:

Confirmation Dialog

End-to-end tests

Of course my Deleting a transaction scenario now failed, so I added the missing

And I answer “yes” in the confirmation message

(and it’s implementation, of course), alongside a new scenario:

Scenario: Deleting a transaction can be cancelled
    Given I have saved the following transactions:
        | date       | description | destination                  | source      | amount | currency |
        | 2024-03-01 | Pizza       | Expenses:Eat Out & Take Away | Assets:Cash | 3999   | USD      |
    And I open the app
    When I click on "Pizza"
    And I click the "Delete" button
    And I answer "no" in the confirmation message
    Then the description is "Pizza"

I only added to new step implementations:

When('I answer "yes" in the confirmation message', () =>
  cy.get('[data-cy="confirm-modal"]').click()
);
When('I answer "no" in the confirmation message', () =>
  cy.get('[data-cy="cancel-modal"]').click()
);

The rest already existed. Ten years ago, when I started writing end-to-end tests like this, and suddenly I touched no JavaScript and only wrote English, I thought it was kind of neat. I remember telling my boss “You can automate with sentences!”. Now we have ChatGPT, but still, I find it rewarding.

Final thoughts

There are some things I really don’t want to track in the Elm world. Things like “is the menu open” for example, are already taken care (and hopefully tested thoroughly) in the UI framework. So I think it makes sense not to force the tools too much and move that control to the JavaScript layer, even though it adds ports and some more message types.

I’ll eventually add a simple top menu bar, and when I experimented a bit a few weeks ago, it didn’t go well. As with the modal, you need to initialize the JavaScript, and Semantic UI didn’t like me changing the DOM after that. I hope that I find it easier next time.

This was a very small post! I’ll finish cleaning up the initial encryption work, and I’ll see you then. Bye!