From 5ca277ee80608fc782b7de803347eff51c36d627 Mon Sep 17 00:00:00 2001
From: ryan <ryannganga13325@gmail.com>
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! {
         <head>
+            <script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
+            <script src="https://twitter.github.io/typeahead.js/releases/latest/typeahead.bundle.min.js"></script>
             <script src="https://cdnjs.cloudflare.com/ajax/libs/corejs-typeahead/1.3.1/typeahead.bundle.min.js"></script>
         </head>
         <Router>
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<String, Vec<WikidataSuggestion>>) -> 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<String, Vec<WikidataSuggestion>>) -> Js
             Reflect::set(&obj, &"value".into(), &suggestion.id.clone().into()).unwrap();
             array.push(&obj);
         }
-        sync.call1(&JsValue::NULL, &array).unwrap();
-    }) as Box<dyn Fn(String, js_sys::Function)>);
 
-    // Configure Bloodhound 
+        sync.call1(&JsValue::NULL, &array).unwrap();
+    }) as Box<dyn Fn(String, Function)>);
+
     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::<js_sys::Function>().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<WikidataSuggestion>,
     node_ref: NodeRef<Input>,
 ) {
-    // 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<dyn FnMut(_)>);
+    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::<Bloodhound>();
-    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::<Bloodhound>();
+    
     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 '<div class="suggestion-item">' +
-                   '<strong>' + data.label + '</strong>' +
-                   (data.description ? '<br><small>' + data.description + '</small>' : '') +
-                   '</div>';
+    // 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<dyn FnMut(web_sys::Event, JsValue)>);
 
-    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::<JsValue>();
-            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<dyn FnMut(_)>);
-
-    input.add_event_listener_with_callback(
-        "typeahead:select",
-        closure.as_ref().unchecked_ref()
-    ).unwrap();
     closure.forget();
 }
\ No newline at end of file