fix(typeahead): add lifecycle management

This commit is contained in:
ryan 2025-04-22 15:21:52 +03:00
parent 37d157725e
commit 2d286e5834

View file

@ -8,6 +8,8 @@ use wasm_bindgen::JsCast;
use web_sys::HtmlInputElement; use web_sys::HtmlInputElement;
use leptos::logging::log; use leptos::logging::log;
use std::time::Duration; use std::time::Duration;
use std::rc::Rc;
use std::cell::RefCell;
#[component] #[component]
pub fn TypeaheadInput( pub fn TypeaheadInput(
@ -20,14 +22,42 @@ pub fn TypeaheadInput(
) -> impl IntoView { ) -> impl IntoView {
let (is_initialized, set_initialized) = create_signal(false); let (is_initialized, set_initialized) = create_signal(false);
// Flag to track if component is mounted
let is_mounted = Rc::new(RefCell::new(true));
let is_mounted_clone = is_mounted.clone();
// Cleanup function to run when component is unmounted
on_cleanup(move || {
log!("[CLEANUP] TypeaheadInput component unmounting");
*is_mounted_clone.borrow_mut() = false;
});
// Clone necessary values for the async task
let fetch_suggestions_clone = fetch_suggestions.clone();
let on_select_clone = on_select.clone();
let node_ref_clone = node_ref.clone();
spawn_local(async move { spawn_local(async move {
log!("[INIT] Component mounted"); log!("[INIT] Component mounted");
let mut retries = 0; let mut retries = 0;
while retries < 10 { while retries < 10 {
if let Some(input) = node_ref.get() { // Check if component is still mounted before proceeding
if !*is_mounted.borrow() {
log!("[INIT] Component unmounted, aborting initialization");
return;
}
if let Some(input) = node_ref_clone.get() {
log!("[INIT] Input element found"); log!("[INIT] Input element found");
let bloodhound = initialize_bloodhound(fetch_suggestions.clone());
// Only proceed if component is still mounted
if !*is_mounted.borrow() {
log!("[INIT] Component unmounted after input found, aborting");
return;
}
let bloodhound = initialize_bloodhound(fetch_suggestions_clone.clone());
// Store bloodhound globally // Store bloodhound globally
js_sys::Reflect::set( js_sys::Reflect::set(
@ -36,23 +66,33 @@ pub fn TypeaheadInput(
&bloodhound &bloodhound
).unwrap(); ).unwrap();
initialize_typeahead(&input, bloodhound, on_select.clone(), node_ref.clone()); // Only proceed if component is still mounted
if !*is_mounted.borrow() {
log!("[INIT] Component unmounted before typeahead init, aborting");
return;
}
initialize_typeahead(&input, bloodhound, on_select_clone.clone(), node_ref_clone.clone());
// Only set initialized if component is still mounted
if *is_mounted.borrow() {
set_initialized.set(true); set_initialized.set(true);
}
break; break;
} }
gloo_timers::future::sleep(Duration::from_millis(100)).await; gloo_timers::future::sleep(Duration::from_millis(100)).await;
retries += 1; retries += 1;
} }
}); });
view! { // CSS
<style> let css = r#"
{r#" .typeahead.tt-input {
.typeahead.tt-input {{
background: transparent !important; background: transparent !important;
}} }
.tt-menu {{ .tt-menu {
width: 100% !important; width: 100% !important;
background: white; background: white;
border: 1px solid #ddd; border: 1px solid #ddd;
@ -61,45 +101,49 @@ pub fn TypeaheadInput(
max-height: 300px; max-height: 300px;
overflow-y: auto; overflow-y: auto;
z-index: 1000 !important; z-index: 1000 !important;
}} }
.tt-dataset-suggestions {{ .tt-dataset-suggestions {
padding: 8px 0; padding: 8px 0;
}} }
.suggestion-item * {{ .suggestion-item * {
pointer-events: none; /* Prevent element interception */ pointer-events: none; /* Prevent element interception */
white-space: nowrap; /* Prevent text wrapping */ white-space: nowrap; /* Prevent text wrapping */
overflow: hidden; /* Hide overflow */ overflow: hidden; /* Hide overflow */
text-overflow: ellipsis; /* Add ellipsis for long text */ text-overflow: ellipsis; /* Add ellipsis for long text */
}} }
.suggestion-item {{ .suggestion-item {
padding: 8px 15px; padding: 8px 15px;
border-bottom: 1px solid #eee; border-bottom: 1px solid #eee;
}} }
.suggestion-item:hover {{ .suggestion-item:hover {
background-color: #f8f9fa; background-color: #f8f9fa;
cursor: pointer; cursor: pointer;
}} }
.label {{ .label {
font-weight: 500; font-weight: 500;
color: #333; color: #333;
}} }
.description {{ .description {
font-size: 0.9em; font-size: 0.9em;
color: #666; color: #666;
margin-top: 2px; margin-top: 2px;
}} }
.empty-suggestion {{ .empty-suggestion {
padding: 8px 15px; padding: 8px 15px;
color: #999; color: #999;
}} }
"#} "#;
view! {
<style>
{css}
</style> </style>
<input <input
@ -317,6 +361,32 @@ fn initialize_typeahead(
).unwrap(); ).unwrap();
closure.forget(); closure.forget();
// Cleanup code to remove the typeahead when component is unmounted
let cleanup_script = format!(
r#"
// Store a reference to the cleanup function for this input
if (!window.typeaheadCleanupFunctions) {{
window.typeaheadCleanupFunctions = {{}};
}}
window.typeaheadCleanupFunctions['{id}'] = function() {{
try {{
$('#{id}').typeahead('destroy');
delete window['{handler}'];
console.log('[JS] Typeahead cleanup for #{id} completed');
}} catch (e) {{
console.error('[JS] Typeahead cleanup error:', e);
}}
}};
"#,
id = input_id,
handler = handler_name
);
if let Err(e) = js_sys::eval(&cleanup_script) {
log!("[RUST] Cleanup script eval error: {:?}", e);
}
// Initialization script with enhanced logging // Initialization script with enhanced logging
let init_script = format!( let init_script = format!(
r#" r#"
@ -380,4 +450,22 @@ fn initialize_typeahead(
if let Err(e) = js_sys::eval(&init_script) { if let Err(e) = js_sys::eval(&init_script) {
log!("[RUST] Eval error: {:?}", e); log!("[RUST] Eval error: {:?}", e);
} }
// Register cleanup function to run when component is unmounted
on_cleanup(move || {
log!("[CLEANUP] Running typeahead cleanup for input: {}", input_id);
let cleanup_call = format!(
r#"
if (window.typeaheadCleanupFunctions && window.typeaheadCleanupFunctions['{id}']) {{
window.typeaheadCleanupFunctions['{id}']();
delete window.typeaheadCleanupFunctions['{id}'];
}}
"#,
id = input_id
);
if let Err(e) = js_sys::eval(&cleanup_call) {
log!("[RUST] Cleanup call eval error: {:?}", e);
}
});
} }