From 5ca277ee80608fc782b7de803347eff51c36d627 Mon Sep 17 00:00:00 2001 From: ryan Date: Tue, 8 Apr 2025 02:30:58 +0300 Subject: [PATCH] feat(typeahead): Improve typeahead initialization and event handling --- src/app.rs | 2 + src/components/typeahead_input.rs | 194 +++++++++++++++--------------- 2 files changed, 102 insertions(+), 94 deletions(-) diff --git a/src/app.rs b/src/app.rs index 927f816..8b69dc6 100644 --- a/src/app.rs +++ b/src/app.rs @@ -30,6 +30,8 @@ pub fn App() -> impl IntoView { // }); view! { + + diff --git a/src/components/typeahead_input.rs b/src/components/typeahead_input.rs index 02484f1..501245e 100644 --- a/src/components/typeahead_input.rs +++ b/src/components/typeahead_input.rs @@ -7,6 +7,8 @@ use gloo_utils::format::JsValueSerdeExt; use wasm_bindgen::JsCast; use web_sys::HtmlInputElement; use leptos::logging::log; +use std::time::Duration; + #[component] pub fn TypeaheadInput( value: String, @@ -16,11 +18,20 @@ pub fn TypeaheadInput( ) -> impl IntoView { let (is_initialized, set_initialized) = create_signal(false); - create_effect(move |_| { - if let (Some(input), false) = (node_ref.get(), is_initialized.get()) { - let bloodhound = initialize_bloodhound(fetch_suggestions.clone()); - initialize_typeahead(&input, bloodhound, on_select.clone(), node_ref.clone()); - set_initialized.set(true); + spawn_local(async move { + log!("[INIT] Component mounted"); + + let mut retries = 0; + while retries < 10 { + if let Some(input) = node_ref.get() { + log!("[INIT] Input element found"); + let bloodhound = initialize_bloodhound(fetch_suggestions.clone()); + initialize_typeahead(&input, bloodhound, on_select.clone(), node_ref.clone()); + set_initialized.set(true); + break; + } + gloo_timers::future::sleep(Duration::from_millis(100)).await; + retries += 1; } }); @@ -30,6 +41,18 @@ pub fn TypeaheadInput( class="typeahead" prop:value=value node_ref=node_ref + on:focus=move |_| log!("[FOCUS] Name input focused") + on:blur=move |_| log!("[FOCUS] Name input blurred") + on:input=move |ev| { + let value = event_target_value(&ev); + log!("[INPUT] Value changed: {}", value); + if let Some(input) = node_ref.get() { + // Correct DOM element access using JsCast + let dom_input: &web_sys::HtmlInputElement = &*input; + let id = dom_input.id(); + let _ = js_sys::eval(&format!("console.log('JS Value:', $('#{}').val())", id)); + } + } /> } } @@ -52,10 +75,12 @@ extern "C" { fn initialize_bloodhound(fetch: Callback>) -> JsValue { let bloodhound_options = Object::new(); - // Store the Closure in a variable to prevent premature garbage collection - let remote_fn = Closure::wrap(Box::new(move |query: String, sync: js_sys::Function| { - log!("Fetching suggestions for: {}", query); + // Configure Bloodhound remote with proper parameters + let remote_fn = Closure::wrap(Box::new(move |query: String, sync: Function| { + log!("[BLOODHOUND] Fetching suggestions for: {}", query); let suggestions = fetch.call(query.clone()); + log!("[BLOODHOUND] Received {} suggestions", suggestions.len()); + let array = Array::new(); for suggestion in &suggestions { let obj = Object::new(); @@ -63,26 +88,36 @@ fn initialize_bloodhound(fetch: Callback>) -> Js Reflect::set(&obj, &"value".into(), &suggestion.id.clone().into()).unwrap(); array.push(&obj); } - sync.call1(&JsValue::NULL, &array).unwrap(); - }) as Box); - // Configure Bloodhound + sync.call1(&JsValue::NULL, &array).unwrap(); + }) as Box); + let remote_config = Object::new(); - Reflect::set(&remote_config, &"url".into(), &"".into()).unwrap(); - Reflect::set(&remote_config, &"wildcard".into(), &"%QUERY".into()).unwrap(); - Reflect::set(&remote_config, &"prepare".into(), &remote_fn.as_ref()).unwrap(); - Reflect::set(&remote_config, &"rateLimitWait".into(), &JsValue::from(300)).unwrap(); + Reflect::set( + &remote_config, + &"prepare".into(), + &js_sys::eval(&format!( + "function(query, callback) {{ + return {}(query, callback); + }}", + remote_fn.as_ref().unchecked_ref::().to_string() + )).unwrap() + ).unwrap(); + + Reflect::set( + &remote_config, + &"wildcard".into(), + &JsValue::from_str("%QUERY") + ).unwrap(); Reflect::set(&bloodhound_options, &"remote".into(), &remote_config).unwrap(); - Reflect::set(&bloodhound_options, &"queryTokenizer".into(), &JsValue::from("whitespace")).unwrap(); - Reflect::set(&bloodhound_options, &"datumTokenizer".into(), &JsValue::from("whitespace")).unwrap(); + Reflect::set(&bloodhound_options, &"datumTokenizer".into(), &JsValue::from_str("whitespace")).unwrap(); + Reflect::set(&bloodhound_options, &"queryTokenizer".into(), &JsValue::from_str("whitespace")).unwrap(); let bloodhound = Bloodhound::new(&bloodhound_options.into()); bloodhound.initialize(true); - - // Prevent Closure from being dropped remote_fn.forget(); - + bloodhound.into() } @@ -92,92 +127,63 @@ fn initialize_typeahead( on_select: Callback, node_ref: NodeRef, ) { - // input event handler for direct typing - let node_ref_clone = node_ref.clone(); - let input_handler = Closure::wrap(Box::new(move |_event: web_sys::Event| { - if let Some(input) = node_ref_clone.get() { - let value = input.value(); - log!("Input updated: {}", value); - // Create synthetic change event for Leptos - let event = web_sys::CustomEvent::new("input").unwrap(); - input.dispatch_event(&event).unwrap(); - } - }) as Box); + log!("[TYPEAHEAD] Initializing for input: {}", input.id()); + let input_id = format!("typeahead-{}", uuid::Uuid::new_v4()); + input.set_id(&input_id); - input.add_event_listener_with_callback( - "input", - input_handler.as_ref().unchecked_ref() - ).unwrap(); - input_handler.forget(); - - let typeahead_options = Object::new(); - Reflect::set(&typeahead_options, &"hint".into(), &JsValue::TRUE).unwrap(); - Reflect::set(&typeahead_options, &"highlight".into(), &JsValue::TRUE).unwrap(); - Reflect::set(&typeahead_options, &"minLength".into(), &JsValue::from(1)).unwrap(); - - // Bloodhound remote configuration - let bloodhound_ref = bloodhound.unchecked_ref::(); - let remote_config = Object::new(); - Reflect::set(&remote_config, &"prepare".into(), &JsValue::from_str("function(q) { return { q: q }; }")).unwrap(); - Reflect::set(&remote_config, &"transform".into(), &js_sys::Function::new_with_args("response", "return response;")).unwrap(); - - // Update dataset configuration let dataset = Object::new(); + let bloodhound_ref = bloodhound.unchecked_ref::(); + Reflect::set(&dataset, &"source".into(), &bloodhound_ref.tt_adapter()).unwrap(); Reflect::set(&dataset, &"display".into(), &"label".into()).unwrap(); Reflect::set(&dataset, &"limit".into(), &JsValue::from(10)).unwrap(); - // Create proper templates - let templates = Object::new(); - Reflect::set(&templates, &"suggestion".into(), &js_sys::eval(r#" - function(data) { - return '
' + - '' + data.label + '' + - (data.description ? '
' + data.description + '' : '') + - '
'; + // Create and register the closure + let closure = Closure::wrap(Box::new(move |_event: web_sys::Event, suggestion: JsValue| { + log!("[TYPEAHEAD] Selection made"); + let data: WikidataSuggestion = suggestion.into_serde().unwrap(); + on_select.call(data.clone()); + + if let Some(input) = node_ref.get() { + input.set_value(&data.label); } - "#).unwrap()).unwrap(); + }) as Box); - Reflect::set(&dataset, &"templates".into(), &templates).unwrap(); + // Register the closure in the JS global scope + let handler_name = format!("handler_{}", input_id); + let handler_name_global = handler_name.clone(); + let global = js_sys::global(); + Reflect::set( + &global, + &handler_name_global.into(), + &closure.as_ref() + ).unwrap(); // Typeahead initialization using jQuery let init_script = format!( - r#"(function() {{ - $('#{}').typeahead({}, {}); - }})"#, - input.id(), - JSON::stringify(&typeahead_options).unwrap(), - JSON::stringify(&dataset).unwrap() + r#" + (function() {{ + console.log('[TYPEAHEAD] Initializing for #{id}'); + $('#{id}').typeahead( + {{ + hint: true, + highlight: true, + minLength: 1 + }}, + {dataset} + ).on('typeahead:select', function(ev, suggestion) {{ + console.log('[TYPEAHEAD] Select event triggered'); + {handler}(ev, suggestion); + }}); + console.log('[TYPEAHEAD] Initialization complete for #{id}'); + }})(); + "#, + id = input_id, + dataset = JSON::stringify(&dataset).unwrap(), + handler = handler_name ); - + + log!("[TYPEAHEAD] Init script: {}", init_script); let _ = js_sys::eval(&init_script).unwrap(); - - // Handle selection events - let closure = Closure::wrap(Box::new(move |event: web_sys::Event| { - if let Some(selected) = event.target() { - let js_value = selected.unchecked_into::(); - let data: WikidataSuggestion = js_sys::JSON::parse( - &js_sys::JSON::stringify(&js_value) - .unwrap() - .as_string() - .unwrap() - .as_str() - ).unwrap() - .into_serde() - .unwrap(); - let data_clone = data.clone(); - - on_select.call(data); - // Explicitly update the input value - if let Some(input) = node_ref.get() { - input.set_value(&data_clone.label); - } - } - }) as Box); - - input.add_event_listener_with_callback( - "typeahead:select", - closure.as_ref().unchecked_ref() - ).unwrap(); closure.forget(); } \ No newline at end of file