feat(typeahead): Improve typeahead initialization and event handling

This commit is contained in:
ryan 2025-04-08 02:30:58 +03:00
parent d6661c2ac9
commit 5ca277ee80
2 changed files with 102 additions and 94 deletions

View file

@ -30,6 +30,8 @@ pub fn App() -> impl IntoView {
// }); // });
view! { view! {
<head> <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> <script src="https://cdnjs.cloudflare.com/ajax/libs/corejs-typeahead/1.3.1/typeahead.bundle.min.js"></script>
</head> </head>
<Router> <Router>

View file

@ -7,6 +7,8 @@ use gloo_utils::format::JsValueSerdeExt;
use wasm_bindgen::JsCast; use wasm_bindgen::JsCast;
use web_sys::HtmlInputElement; use web_sys::HtmlInputElement;
use leptos::logging::log; use leptos::logging::log;
use std::time::Duration;
#[component] #[component]
pub fn TypeaheadInput( pub fn TypeaheadInput(
value: String, value: String,
@ -16,11 +18,20 @@ pub fn TypeaheadInput(
) -> impl IntoView { ) -> impl IntoView {
let (is_initialized, set_initialized) = create_signal(false); let (is_initialized, set_initialized) = create_signal(false);
create_effect(move |_| { spawn_local(async move {
if let (Some(input), false) = (node_ref.get(), is_initialized.get()) { 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()); let bloodhound = initialize_bloodhound(fetch_suggestions.clone());
initialize_typeahead(&input, bloodhound, on_select.clone(), node_ref.clone()); initialize_typeahead(&input, bloodhound, on_select.clone(), node_ref.clone());
set_initialized.set(true); 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" class="typeahead"
prop:value=value prop:value=value
node_ref=node_ref 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 { fn initialize_bloodhound(fetch: Callback<String, Vec<WikidataSuggestion>>) -> JsValue {
let bloodhound_options = Object::new(); let bloodhound_options = Object::new();
// Store the Closure in a variable to prevent premature garbage collection // Configure Bloodhound remote with proper parameters
let remote_fn = Closure::wrap(Box::new(move |query: String, sync: js_sys::Function| { let remote_fn = Closure::wrap(Box::new(move |query: String, sync: Function| {
log!("Fetching suggestions for: {}", query); log!("[BLOODHOUND] Fetching suggestions for: {}", query);
let suggestions = fetch.call(query.clone()); let suggestions = fetch.call(query.clone());
log!("[BLOODHOUND] Received {} suggestions", suggestions.len());
let array = Array::new(); let array = Array::new();
for suggestion in &suggestions { for suggestion in &suggestions {
let obj = Object::new(); let obj = Object::new();
@ -63,24 +88,34 @@ fn initialize_bloodhound(fetch: Callback<String, Vec<WikidataSuggestion>>) -> Js
Reflect::set(&obj, &"value".into(), &suggestion.id.clone().into()).unwrap(); Reflect::set(&obj, &"value".into(), &suggestion.id.clone().into()).unwrap();
array.push(&obj); 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(); let remote_config = Object::new();
Reflect::set(&remote_config, &"url".into(), &"".into()).unwrap(); Reflect::set(
Reflect::set(&remote_config, &"wildcard".into(), &"%QUERY".into()).unwrap(); &remote_config,
Reflect::set(&remote_config, &"prepare".into(), &remote_fn.as_ref()).unwrap(); &"prepare".into(),
Reflect::set(&remote_config, &"rateLimitWait".into(), &JsValue::from(300)).unwrap(); &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, &"remote".into(), &remote_config).unwrap();
Reflect::set(&bloodhound_options, &"queryTokenizer".into(), &JsValue::from("whitespace")).unwrap(); Reflect::set(&bloodhound_options, &"datumTokenizer".into(), &JsValue::from_str("whitespace")).unwrap();
Reflect::set(&bloodhound_options, &"datumTokenizer".into(), &JsValue::from("whitespace")).unwrap(); Reflect::set(&bloodhound_options, &"queryTokenizer".into(), &JsValue::from_str("whitespace")).unwrap();
let bloodhound = Bloodhound::new(&bloodhound_options.into()); let bloodhound = Bloodhound::new(&bloodhound_options.into());
bloodhound.initialize(true); bloodhound.initialize(true);
// Prevent Closure from being dropped
remote_fn.forget(); remote_fn.forget();
bloodhound.into() bloodhound.into()
@ -92,92 +127,63 @@ fn initialize_typeahead(
on_select: Callback<WikidataSuggestion>, on_select: Callback<WikidataSuggestion>,
node_ref: NodeRef<Input>, node_ref: NodeRef<Input>,
) { ) {
// input event handler for direct typing log!("[TYPEAHEAD] Initializing for input: {}", input.id());
let node_ref_clone = node_ref.clone(); let input_id = format!("typeahead-{}", uuid::Uuid::new_v4());
let input_handler = Closure::wrap(Box::new(move |_event: web_sys::Event| { input.set_id(&input_id);
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(_)>);
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 dataset = Object::new();
let bloodhound_ref = bloodhound.unchecked_ref::<Bloodhound>();
Reflect::set(&dataset, &"source".into(), &bloodhound_ref.tt_adapter()).unwrap(); Reflect::set(&dataset, &"source".into(), &bloodhound_ref.tt_adapter()).unwrap();
Reflect::set(&dataset, &"display".into(), &"label".into()).unwrap(); Reflect::set(&dataset, &"display".into(), &"label".into()).unwrap();
Reflect::set(&dataset, &"limit".into(), &JsValue::from(10)).unwrap(); Reflect::set(&dataset, &"limit".into(), &JsValue::from(10)).unwrap();
// Create proper templates // Create and register the closure
let templates = Object::new(); let closure = Closure::wrap(Box::new(move |_event: web_sys::Event, suggestion: JsValue| {
Reflect::set(&templates, &"suggestion".into(), &js_sys::eval(r#" log!("[TYPEAHEAD] Selection made");
function(data) { let data: WikidataSuggestion = suggestion.into_serde().unwrap();
return '<div class="suggestion-item">' + on_select.call(data.clone());
'<strong>' + data.label + '</strong>' +
(data.description ? '<br><small>' + data.description + '</small>' : '') +
'</div>';
}
"#).unwrap()).unwrap();
Reflect::set(&dataset, &"templates".into(), &templates).unwrap(); if let Some(input) = node_ref.get() {
input.set_value(&data.label);
}
}) as Box<dyn FnMut(web_sys::Event, JsValue)>);
// 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 // Typeahead initialization using jQuery
let init_script = format!( let init_script = format!(
r#"(function() {{ r#"
$('#{}').typeahead({}, {}); (function() {{
}})"#, console.log('[TYPEAHEAD] Initializing for #{id}');
input.id(), $('#{id}').typeahead(
JSON::stringify(&typeahead_options).unwrap(), {{
JSON::stringify(&dataset).unwrap() 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(); 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(); closure.forget();
} }