slaveOftime
blazorfsharpblazor

How easy it is to make dialog and toast in blazor

2023-02-12

First, I will define some types and create some helper method to open dialog or toast by program:

Define dialogs and toasts

type DialogContext = { Id: int; View: NodeRenderFragment }

type DialogController = { Close: unit -> unit }

type IComponentHook with

    member hook.SharedStore = hook.ServiceProvider.GetService<IShareStore>()

    member hook.Dialogs = hook.SharedStore.CreateCVal(nameof hook.Dialogs, List.empty<DialogContext>)

    member hook.Toasts = hook.SharedStore.CreateCVal(nameof hook.Toasts, List.empty<DialogContext>)

    member hook.OpenDialog(makeView: DialogController -> NodeRenderFragment) =
        let id = Random.Shared.Next()
        let cts = new CancellationTokenSource()
        let controller = {
            Close =
                fun () ->
                    hook.Dialogs.Publish(List.filter (fun x -> x.Id <> id))
                    cts.Cancel()
                    cts.Dispose()
        }
        let dialogContext = { Id = id; View = makeView controller }
        hook.Dialogs.Publish(List.append [ dialogContext ])
        task {
            try
                do! Task.Delay(-1, cts.Token)
            with _ ->
                ()
        }

    member hook.OpenToast(durationMs: int, makeView: DialogController -> NodeRenderFragment) =
        let id = Random.Shared.Next()
        let controller = {
            Close = fun () -> hook.Toasts.Publish(List.filter (fun x -> x.Id <> id))
        }
        let dialogContext = { Id = id; View = makeView controller }
        hook.Toasts.Publish(List.append [ dialogContext ])
        ignore (
            task {
                do! Task.Delay durationMs
                controller.Close()
            }
        )

Then, I will display dialogs and toasts in list adaptively.

Create a container to display the dialogs and toasts

[<FunBlazorCustomElement>]
type DialogContainer() =
    inherit FunComponent()

    override _.Render() =
        html.inject (fun (hook: IComponentHook) ->
            html.fragment [
                adaptiview () {
                    let! dialogs = hook.Dialogs
                    for dialog in dialogs do
                        div {
                            key dialog.Id
                            class' "modal modal-open"
                            dialog.View
                        }
                }
                div {
                    class' "z-[999] toast toast-top toast-end"
                    adaptiview () {
                        let! toasts = hook.Toasts
                        for toast in toasts do
                            div {
                                key toast.Id
                                toast.View
                            }
                    }
                }
            ]
        )

Fianlly, I can use it in my business logic:

Create a button to trigger a dialog, after clicked Ok we can display toasts:

Use a button to start all

button {
    class' "btn btn-primary"
    onclick (fun _ -> task {
        do! openDialog ()
        openToast 1
        do! Task.Delay 1000
        openToast 2
        openToast 3
    })
    "Start some task"
}

Use dialog

let openDialog () =
    hook.OpenDialog(fun ctx -> div {
        class' "modal-box"
        p {
            class' "py-4"
            "Task will finish soon."
        }
        div {
            class' "modal-action"
            button {
                class' "btn btn-primary"
                onclick (ignore >> ctx.Close)
                "Ok"
            }
        }
    })

Use toast

let openToast (i: int) =
    hook.OpenToast(
        3_000,
        fun _ -> div {
            class' "alert alert-success"
            $"Task{i} finished successful!"
        }
    )

Do you like this post?