use dioxus::dioxus_core::AttributeValue;
use dioxus::prelude::*;
#[cfg(clerk_client)]
const HOST_RETRY_MS: u32 = 16;
#[cfg(clerk_client)]
const HOST_DEADLINE_MS: u32 = 10_000;
#[derive(Clone, Copy, PartialEq)]
pub(crate) enum Widget {
SignIn,
SignUp,
UserButton,
UserProfile,
CreateOrganization,
OrganizationProfile,
OrganizationSwitcher,
OrganizationList,
Waitlist,
TaskSetupMfa,
}
impl Widget {
fn host_id_prefix(self) -> &'static str {
match self {
Self::SignIn => "clerk-signin",
Self::SignUp => "clerk-signup",
Self::UserButton => "clerk-userbutton",
Self::UserProfile => "clerk-userprofile",
Self::CreateOrganization => "clerk-createorganization",
Self::OrganizationProfile => "clerk-organizationprofile",
Self::OrganizationSwitcher => "clerk-organizationswitcher",
Self::OrganizationList => "clerk-organizationlist",
Self::Waitlist => "clerk-waitlist",
Self::TaskSetupMfa => "clerk-tasksetupmfa",
}
}
#[cfg(clerk_client)]
pub(crate) fn mount_method(self) -> &'static str {
match self {
Self::SignIn => "mountSignIn",
Self::SignUp => "mountSignUp",
Self::UserButton => "mountUserButton",
Self::UserProfile => "mountUserProfile",
Self::CreateOrganization => "mountCreateOrganization",
Self::OrganizationProfile => "mountOrganizationProfile",
Self::OrganizationSwitcher => "mountOrganizationSwitcher",
Self::OrganizationList => "mountOrganizationList",
Self::Waitlist => "mountWaitlist",
Self::TaskSetupMfa => "mountTaskSetupMFA",
}
}
#[cfg(clerk_client)]
pub(crate) fn unmount_method(self) -> &'static str {
match self {
Self::SignIn => "unmountSignIn",
Self::SignUp => "unmountSignUp",
Self::UserButton => "unmountUserButton",
Self::UserProfile => "unmountUserProfile",
Self::CreateOrganization => "unmountCreateOrganization",
Self::OrganizationProfile => "unmountOrganizationProfile",
Self::OrganizationSwitcher => "unmountOrganizationSwitcher",
Self::OrganizationList => "unmountOrganizationList",
Self::Waitlist => "unmountWaitlist",
Self::TaskSetupMfa => "unmountTaskSetupMFA",
}
}
#[cfg(clerk_client)]
fn mount(
self,
bridge: &crate::bridge::ClerkBridge,
element: &web_sys::Element,
options: &serde_json::Value,
) -> Result<(), crate::core::ClerkError> {
bridge.mount_widget(self, element, options)
}
#[cfg(clerk_client)]
fn unmount(self, bridge: &crate::bridge::ClerkBridge, element: &web_sys::Element) {
bridge.unmount_widget(self, element);
}
}
#[derive(Clone, Default, PartialEq)]
struct HostProps {
id: Option<String>,
attributes: Vec<Attribute>,
}
#[derive(Clone)]
struct MountedWidget {
id: String,
options: serde_json::Value,
#[cfg(clerk_client)]
element: web_sys::Element,
}
impl PartialEq for MountedWidget {
fn eq(&self, other: &Self) -> bool {
self.id == other.id && self.options == other.options
}
}
pub(super) struct WidgetProps {
options: serde_json::Value,
fallback: Element,
host: HostProps,
}
impl WidgetProps {
pub(super) fn new(
options: serde_json::Value,
fallback: Element,
id: Option<String>,
attributes: Vec<Attribute>,
) -> Self {
Self {
options,
fallback,
host: HostProps { id, attributes },
}
}
}
pub(super) fn render(widget: Widget, props: WidgetProps) -> Element {
let WidgetProps {
options,
fallback,
host,
} = props;
rsx! { WidgetHost { widget, options, fallback, host } }
}
#[component]
fn WidgetHost(
widget: Widget,
options: serde_json::Value,
fallback: Element,
host: HostProps,
) -> Element {
let id = host
.id
.or_else(|| spread_id(&host.attributes))
.unwrap_or_else(|| generated_host_id(widget));
let mut attributes = host.attributes;
attributes.retain(|attribute| attribute.name != "id");
let mounted_widget = use_signal(|| None::<MountedWidget>);
let is_mounted_with_current_options = mounted_widget
.read()
.as_ref()
.is_some_and(|mounted| mounted.id == id && mounted.options == options);
rsx! {
if !is_mounted_with_current_options {
{fallback}
}
div { id: "{id}", ..attributes,
WidgetLifecycle { id: id.clone(), widget, options, mounted_widget }
}
}
}
#[component]
fn WidgetLifecycle(
id: String,
widget: Widget,
options: serde_json::Value,
mounted_widget: Signal<Option<MountedWidget>>,
) -> Element {
#[cfg(clerk_client)]
{
use crate::bridge::ClerkBridge;
use crate::lifecycle::BridgeAction;
let id_for_mount = id.clone();
let id_for_guard = id.clone();
let id_for_done = id.clone();
let widget_local = widget;
let options_for_guard = options.clone();
let options_for_mount = options.clone();
let mounted_widget_guard = mounted_widget;
let mounted_widget_for_action = mounted_widget;
let mounted_widget_done = mounted_widget;
crate::lifecycle::use_loaded_bridge_action(
(&id, &options),
move || {
mounted_widget_guard.read().as_ref().is_none_or(|mounted| {
mounted.id != id_for_guard || mounted.options != options_for_guard
})
},
HOST_RETRY_MS,
HOST_DEADLINE_MS,
crate::core::ClerkError::Js(format!(
"clerk widget host element #{id} did not appear within {HOST_DEADLINE_MS} ms; the widget cannot mount"
)),
move |bridge| {
let Some(el) = host_element(&id_for_mount) else {
return Ok(BridgeAction::Deferred);
};
if let Some(mounted) = mounted_widget_for_action.read().as_ref() {
widget_local.unmount(bridge, &mounted.element);
}
widget_local.mount(bridge, &el, &options_for_mount)?;
Ok(BridgeAction::Done(MountedWidget {
id: id_for_done.clone(),
options: options_for_mount.clone(),
element: el,
}))
},
move |mounted| {
let mut mounted_widget = mounted_widget_done;
mounted_widget.set(Some(mounted));
},
);
use_drop(move || {
let Some(element) = mounted_widget
.peek()
.as_ref()
.map(|mounted| mounted.element.clone())
else {
return;
};
let bridge = ClerkBridge::current();
widget.unmount(&bridge, &element);
});
}
#[cfg(not(clerk_client))]
let _ = (&options, mounted_widget);
rsx! {}
}
fn spread_id(attributes: &[Attribute]) -> Option<String> {
attributes.iter().rev().find_map(|attribute| {
if attribute.name != "id" {
return None;
}
match &attribute.value {
AttributeValue::Text(value) => Some(value.clone()),
_ => None,
}
})
}
#[cfg(clerk_client)]
fn host_element(id: &str) -> Option<web_sys::Element> {
let window = web_sys::window()?;
let doc = window.document()?;
doc.get_element_by_id(id)
}
fn generated_host_id(widget: Widget) -> String {
generated_host_id_from_scope(widget, dioxus::dioxus_core::current_scope_id())
}
fn generated_host_id_from_scope(widget: Widget, scope_id: dioxus::prelude::ScopeId) -> String {
format!("{}-{}", widget.host_id_prefix(), scope_id.0)
}
#[cfg(test)]
mod tests {
use super::*;
use dioxus::prelude::ScopeId;
use std::sync::Mutex;
static TEST_LOCK: Mutex<()> = Mutex::new(());
static RECORDED_IDS: Mutex<Vec<String>> = Mutex::new(Vec::new());
#[test]
fn generated_host_id_is_stable_for_the_same_widget_scope() {
let first = generated_host_id_from_scope(Widget::SignIn, ScopeId(42));
let second = generated_host_id_from_scope(Widget::SignIn, ScopeId(42));
assert_eq!(first, second);
assert_eq!(first, "clerk-signin-42");
}
#[test]
fn task_setup_mfa_has_stable_generated_host_id() {
let id = generated_host_id_from_scope(Widget::TaskSetupMfa, ScopeId(42));
assert_eq!(id, "clerk-tasksetupmfa-42");
}
#[test]
fn spread_id_reads_text_id_attribute() {
let attributes = vec![Attribute::new("id", "custom-host", None, false)];
assert_eq!(spread_id(&attributes).as_deref(), Some("custom-host"));
}
#[test]
fn spread_id_is_none_without_an_id_attribute() {
let attributes = vec![Attribute::new("class", "mx-auto", None, false)];
assert_eq!(spread_id(&attributes), None);
}
#[test]
fn spread_id_prefers_the_last_id_attribute() {
let attributes = vec![
Attribute::new("id", "first", None, false),
Attribute::new("id", "second", None, false),
];
assert_eq!(spread_id(&attributes).as_deref(), Some("second"));
}
#[test]
fn generated_host_id_distinguishes_widget_instances_by_scope() {
assert_ne!(
generated_host_id_from_scope(Widget::SignIn, ScopeId(42)),
generated_host_id_from_scope(Widget::SignIn, ScopeId(43))
);
}
#[test]
fn generated_host_id_resets_with_each_virtual_dom_tree() {
let _guard = TEST_LOCK.lock().unwrap();
let first = generated_host_id_from_fresh_dom();
let second = generated_host_id_from_fresh_dom();
assert_eq!(first, second);
}
fn generated_host_id_from_fresh_dom() -> String {
RECORDED_IDS.lock().unwrap().clear();
let mut dom = VirtualDom::new(IdProbe);
dom.rebuild_in_place();
RECORDED_IDS
.lock()
.unwrap()
.first()
.cloned()
.expect("IdProbe records a generated id")
}
#[component]
fn IdProbe() -> Element {
RECORDED_IDS
.lock()
.unwrap()
.push(generated_host_id(Widget::SignIn));
rsx! {}
}
}
#[cfg(all(test, clerk_client))]
mod client_tests {
use super::*;
use wasm_bindgen_test::*;
wasm_bindgen_test_configure!(run_in_browser);
#[wasm_bindgen_test]
fn task_setup_mfa_uses_v6_method_names() {
assert_eq!(Widget::TaskSetupMfa.mount_method(), "mountTaskSetupMFA");
assert_eq!(Widget::TaskSetupMfa.unmount_method(), "unmountTaskSetupMFA");
}
}