Given that we have a list of items, and a "delete" button for each item. We want that when user clicks the button, a confirm dialog will appear, asking user one more time, before proceeding with API call to delete the item from the database.
Here is how to do that in Lustre. The dialog in this example is the HTML native dialog created by window.confirm()
method, in case we want some thing quick and simple. If you want to use <dialog>
element, I will write later.
First, let's write a view function to prepare the HTML:
import lustre/attribute as a
import lustre/element/html as h
import lustre/event as ev
import lucide_lustre as lucide_icon
fn render_post_row(post: Post) {
let Post(id:, ..) = post
h.button(
[
a.type_("button"),
a.class("hover:text-red-600 cursor-pointer"),
],
[
lucide_icon.eraser([a.class("w-5 h-auto")]),
],
),
}
We define message variant for the click event and the "confirmed" event:
pub type Msg {
...
UserClickedDeletion(String)
UserConfirmedDeletion(String)
}
Attach the click handler:
h.button(
[
...,
ev.on_click(UserClickedDeletion(id)),
],
[
...
],
),
When handling the UserClickedDeletion
message, we will call the JS method window.confirm
to create and show the dialog. It's JS code, so we need to create a simple FFI function:
In element.ffi.mjs:
/**
* Show a confirmation dialog to the user
* @param {string} message - The message to display in the confirmation dialog
* @returns {boolean} True if user confirmed, false otherwise
*/
export function confirm(message) {
return window.confirm(message)
}
In ffi.gleam:
@external(javascript, "./element.ffi.mjs", "confirm")
pub fn confirm(message: String) -> Bool
The operation of showing dialog, waiting for user action must be asynchronous, to not block our app update cycle, so we will implement it as Effect
. The effect.before_paint()
utility can be used for this:
fn update(model: Model, msg: Msg) -> #(Model, Effect(Msg)) {
case msg {
UserClickedDeletion(id) -> {
// To show dialog for deletion confirmation
let whatnext = {
use dispatch, _root <- effect.before_paint
let agreed = ffi.confirm("Are you sure want to delete?")
io.println("User agree? " <> bool.to_string(agreed))
use <- bool.guard(!agreed, Nil)
dispatch(UserConfirmedDeletion(id))
}
#(model, whatnext)
}
}
}
If user clicks "Yes", the Lustre runtime will send back the UserConfirmedDeletion
message to us.
If you are not familiar with Gleam use
expression yet, here is the callback version:
let whatsnext =
effect.before_paint(fn(dispatch, _root) {
let agreed = ffi.confirm("Are you sure want to delete?")
io.println("User agree? " <> bool.to_string(agreed))
case agreed {
True -> dispatch(UserConfirmedDeletion(id))
False -> Nil
}
})
In the handling of UserConfirmedDeletion
, we start to call API to delete the item:
fn update(model: Model, msg: Msg) -> #(Model, Effect(Msg)) {
case msg {
UserConfirmedDeletion(id) -> {
let model = Model(..model, loading_status: IsSubmitting)
let whatnext = actions.delete_content_item_via_api(id)
#(model, whatnext)
}
}
}
I don't go to the detail of actions.delete_content_item_via_api()
, because it is out of scope. Basically, it is just an Effect
that uses rsvp
to make API call.