Dialog
Rust UI component that displays a modal dialog that the user can interact with.
dialog
use leptos::prelude::*; use crate::components::ui::dialog::{ DialogBody, DialogButtonFormCancel, DialogButtonFormSubmit, DialogComponent, DialogContent, DialogFooter, DialogForm, DialogProvider, DialogTitle, DialogTrigger, }; use crate::components::ui::form::FormField; use crate::components::ui::input::Input; #[component] pub fn DemoDialog() -> impl IntoView { view! { <DialogProvider> <DialogTrigger>"Change payment method"</DialogTrigger> <DialogComponent> <DialogContent> <DialogTitle>"Change payment method"</DialogTitle> <DialogForm> <DialogBody class="flex flex-col gap-2"> <FormField> <label class="w-1/3 mr-auto text-gray-400">"Card number"</label> <Input name="card-number" /> </FormField> <FormField> <label class="w-1/3 mr-auto text-gray-400">"Expiration"</label> <Input name="expiration" /> </FormField> <FormField> <label class="w-1/3 mr-auto text-gray-400">"CVC"</label> <Input r#type="password" name="cvc" placeholder="•••" /> </FormField> </DialogBody> <DialogFooter> <DialogButtonFormCancel>"Cancel"</DialogButtonFormCancel> <DialogButtonFormSubmit>"Save changes"</DialogButtonFormSubmit> </DialogFooter> </DialogForm> </DialogContent> </DialogComponent> </DialogProvider> } }
Installation
You can run either of the following commands:
# cargo install ui-cli --forceui add demo_dialogui add dialog
Update the imports to match your project setup.
Copy and paste the following code into your project:
components/ui/dialog.rs
use icons::X; use leptos::html::Dialog; use leptos::prelude::*; use leptos_ui::clx; use tw_merge::*; use wasm_bindgen::JsCast; use web_sys::HtmlElement; use crate::components::ui::_styles::STYLES; use crate::components::ui::button::{Button, ButtonVariant}; mod components { use super::*; clx! {Card, div, "rounded-lg border bg-card shadow p-4 w-full"} clx! {DialogFooter, footer, "flex gap-6 justify-end py-4 px-8"} clx! {DialogTitle, h2, "text-2xl font-bold"} clx! {DialogBody, div, ""} clx! {DialogContent, div, "p-8 space-y-3 bg-background shadow-lg border"} clx! {DialogCloseBtn, button, "absolute right-4 top-4 rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-accent data-[state=open]:text-muted-foreground"} } pub use components::*; /*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/ /* ✨ FUNCTIONS ✨ */ /*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/ #[derive(Clone)] pub struct DialogContext { pub dialog_ref: NodeRef<Dialog>, } pub fn use_dialog_context() -> DialogContext { use_context::<DialogContext>().expect("DialogContext not found") } #[allow(unused_braces)] #[component] pub fn DialogProvider(children: Children) -> impl IntoView { let dialog_ref = NodeRef::<Dialog>::new(); provide_context(DialogContext { dialog_ref }); view! { {children()} } } /*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/ /* ✨ FUNCTIONS ✨ */ /*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/ #[component] pub fn DialogComponent( #[prop(into, optional)] class: Signal<String>, children: Children, ) -> impl IntoView { let dialog_context = use_dialog_context(); let dialog_ref = dialog_context.dialog_ref; let class = tw_merge!( STYLES::DIALOG_BACKDROP, STYLES::DIALOG_OPEN_STATE, STYLES::DIALOG_OPACITY_TRANSITION, STYLES::BLOCK_INSET_ZERO, "duration-300 translate-y-20", "p-0 w-2/3 rounded-2xl text-md", class() ); let close_dialog = move |_| { if let Some(dialog) = dialog_ref.get() { dialog.close(); } }; view! { <dialog node_ref=dialog_ref on:click=move |ev| { let target = ev.target().expect("Target not found"); if let Some(element) = target.dyn_ref::<HtmlElement>() && element.tag_name() == "DIALOG" && let Some(dialog) = dialog_ref.get() { dialog.close(); } } on:close=move |ev| { #[allow(unused_variables)] let _target = ev.target().expect("Target not found"); } class=class > <div class="relative"> <DialogCloseBtn on:click=close_dialog> <span class="sr-only">"Close Dialog"</span> <X class="size-4" /> </DialogCloseBtn> {children()} </div> </dialog> } } #[component] pub fn DialogTrigger( #[prop(into, optional)] class: Signal<String>, children: Children, ) -> impl IntoView { let dialog_context = use_dialog_context(); let dialog_ref = dialog_context.dialog_ref; let show_dialog = move |_| { if let Some(dialog) = dialog_ref.get() { dialog.show_modal().expect("Failed to show modal"); } }; view! { <Button on:click=show_dialog variant=ButtonVariant::Outline class=class> {children()} </Button> } } #[component] pub fn DialogForm( #[prop(into, optional)] class: Signal<String>, children: Children, ) -> impl IntoView { let class = Memo::new(move |_| tw_merge!("flex flex-col gap-2", class())); view! { <form method="dialog" class=class> {children()} </form> } } #[component] pub fn DialogButtonFormCancel( #[prop(into, optional)] class: Signal<String>, children: Children, ) -> impl IntoView { let class = Memo::new(move |_| tw_merge!("", class())); view! { <Button class=class variant=ButtonVariant::Outline formmethod="dialog" value="cancel"> {children()} </Button> } } #[component] pub fn DialogButtonFormSubmit( #[prop(into, optional)] class: Signal<String>, children: Children, ) -> impl IntoView { let class = Memo::new(move |_| tw_merge!("", class())); view! { <Button class=class formmethod="dialog" value="submit"> {children()} </Button> } }
Update the imports to match your project setup.
Usage
// Coming soon 🦀