feat: Combobox adds selected_options prop

This commit is contained in:
luoxiao 2024-07-01 16:32:28 +08:00
parent f08956586e
commit 58c3905ed3
4 changed files with 83 additions and 58 deletions

View file

@ -31,9 +31,7 @@ pub fn SwitchVersion() -> impl IntoView {
<Combobox> <Combobox>
{ {
options.into_iter().map(|option| view! { options.into_iter().map(|option| view! {
<ComboboxOption key=option.1> <ComboboxOption value=option.1 text=option.0 />
{option.0}
</ComboboxOption>
}).collect_view() }).collect_view()
} }
</Combobox> </Combobox>

View file

@ -1,16 +1,12 @@
# Combobox # Combobox
```rust demo ```rust demo
let value = RwSignal::new(vec![]); let selected_options = RwSignal::new(vec![]);
view! { view! {
<Combobox value> <Combobox selected_options>
<ComboboxOption key="cat"> <ComboboxOption value="cat" text="Car" />
"Cat" <ComboboxOption value="dog" text="Dog" />
</ComboboxOption>
<ComboboxOption key="dog">
"Dog"
</ComboboxOption>
</Combobox> </Combobox>
} }
``` ```
@ -18,16 +14,12 @@ view! {
### Clearable ### Clearable
```rust demo ```rust demo
let value = RwSignal::new(vec![]); let selected_options = RwSignal::new(vec![]);
view! { view! {
<Combobox value multiselect=true clearable=true> <Combobox selected_options multiselect=true clearable=true>
<ComboboxOption key="cat"> <ComboboxOption value="cat" text="Car" />
"Cat" <ComboboxOption value="dog" text="Dog" />
</ComboboxOption>
<ComboboxOption key="dog">
"Dog"
</ComboboxOption>
</Combobox> </Combobox>
} }
``` ```
@ -35,16 +27,12 @@ view! {
### Multiselect ### Multiselect
```rust demo ```rust demo
let value = RwSignal::new(vec![]); let selected_options = RwSignal::new(vec![]);
view! { view! {
<Combobox value multiselect=true> <Combobox selected_options multiselect=true>
<ComboboxOption key="cat"> <ComboboxOption value="cat" text="Car" />
"Cat" <ComboboxOption value="dog" text="Dog" />
</ComboboxOption>
<ComboboxOption key="dog">
"Dog"
</ComboboxOption>
</Combobox> </Combobox>
} }
``` ```

View file

@ -5,7 +5,8 @@ use thaw_utils::{add_event_listener, mount_style, Model};
#[component] #[component]
pub fn Combobox( pub fn Combobox(
#[prop(optional, into)] value: Model<Vec<String>>, #[prop(optional, into)] value: Model<String>,
#[prop(optional, into)] selected_options: Model<Vec<String>>,
#[prop(optional)] multiselect: bool, #[prop(optional)] multiselect: bool,
#[prop(optional)] clearable: bool, #[prop(optional)] clearable: bool,
children: Children, children: Children,
@ -19,7 +20,7 @@ pub fn Combobox(
let clear_icon_ref = NodeRef::<html::Span>::new(); let clear_icon_ref = NodeRef::<html::Span>::new();
let is_show_clear_icon = Memo::new(move |_| { let is_show_clear_icon = Memo::new(move |_| {
if clearable { if clearable {
value.with(|value| !value.is_empty()) selected_options.with(|options| !options.is_empty())
} else { } else {
false false
} }
@ -28,7 +29,7 @@ pub fn Combobox(
clear_icon_ref.on_load(move |clear_icon_el| { clear_icon_ref.on_load(move |clear_icon_el| {
let handler = add_event_listener(clear_icon_el.into_any(), ev::click, move |e| { let handler = add_event_listener(clear_icon_el.into_any(), ev::click, move |e| {
e.stop_propagation(); e.stop_propagation();
value.set(vec![]); selected_options.set(vec![]);
}); });
on_cleanup(move || handler.remove()); on_cleanup(move || handler.remove());
}); });
@ -66,6 +67,32 @@ pub fn Combobox(
on_cleanup(move || handle.remove()); on_cleanup(move || handle.remove());
} }
let on_input = move |ev| {
let input_value = event_target_value(&ev);
if !multiselect {
if selected_options.with_untracked(|options| {
if let Some(option) = options.first() {
if option != &input_value {
return true;
}
}
false
}) {
selected_options.set(vec![]);
}
}
value.set(input_value);
};
let on_blur = move |_| {
if multiselect {
value.set(String::new());
} else {
if selected_options.with_untracked(|options| options.is_empty()) {
value.set(String::new());
}
}
};
view! { view! {
<Binder target_ref=trigger_ref> <Binder target_ref=trigger_ref>
<div <div
@ -80,17 +107,11 @@ pub fn Combobox(
aria-expanded="true" aria-expanded="true"
role="combobox" role="combobox"
class="thaw-combobox__input" class="thaw-combobox__input"
// #TODO
readonly=true
prop:value=move || { prop:value=move || {
value.with(|value| { value.get()
if multiselect {
String::new()
} else {
value.first().cloned().unwrap_or_default()
}
})
} }
on:input=on_input
on:blur=on_blur
/> />
{ {
if clearable { if clearable {
@ -129,7 +150,7 @@ pub fn Combobox(
placement=FollowerPlacement::BottomStart placement=FollowerPlacement::BottomStart
width=FollowerWidth::MinTarget width=FollowerWidth::MinTarget
> >
<Provider value=ComboboxInjection{value, multiselect, is_show_listbox}> <Provider value=ComboboxInjection{value, multiselect, selected_options, is_show_listbox}>
<CSSTransition <CSSTransition
node_ref=listbox_ref node_ref=listbox_ref
name="fade-in-scale-up-transition" name="fade-in-scale-up-transition"
@ -155,7 +176,8 @@ pub fn Combobox(
#[derive(Clone, Copy)] #[derive(Clone, Copy)]
pub(crate) struct ComboboxInjection { pub(crate) struct ComboboxInjection {
value: Model<Vec<String>>, value: Model<String>,
selected_options: Model<Vec<String>>,
is_show_listbox: RwSignal<bool>, is_show_listbox: RwSignal<bool>,
pub multiselect: bool, pub multiselect: bool,
} }
@ -165,20 +187,22 @@ impl ComboboxInjection {
expect_context() expect_context()
} }
pub fn is_selected(&self, key: &String) -> bool { pub fn is_selected(&self, value: &String) -> bool {
self.value.with(|value| value.contains(key)) self.selected_options
.with(|options| options.contains(value))
} }
pub fn on_option_select(&self, key: &String) { pub fn on_option_select(&self, value: &String, text: &String) {
self.value.update(|value| { self.selected_options.update(|options| {
if self.multiselect { if self.multiselect {
if let Some(index) = value.iter().position(|k| k == key) { if let Some(index) = options.iter().position(|v| v == value) {
value.remove(index); options.remove(index);
return; return;
} }
value.push(key.clone()); options.push(value.clone());
} else { } else {
*value = vec![key.clone()]; *options = vec![value.clone()];
self.value.set(text.clone());
self.is_show_listbox.set(false); self.is_show_listbox.set(false);
} }
}); });

View file

@ -1,12 +1,24 @@
use crate::ComboboxInjection; use crate::ComboboxInjection;
use leptos::*; use leptos::*;
use thaw_components::{If, Then}; use thaw_components::{Fallback, If, OptionComp, Then};
use thaw_utils::class_list; use thaw_utils::class_list;
#[component] #[component]
pub fn ComboboxOption(#[prop(into)] key: String, children: Children) -> impl IntoView { pub fn ComboboxOption(
#[prop(optional, into)] value: Option<String>,
#[prop(into)] text: String,
#[prop(optional)] children: Option<Children>,
) -> impl IntoView {
let combobox = ComboboxInjection::use_(); let combobox = ComboboxInjection::use_();
let key = StoredValue::new(key); let value = StoredValue::new(value.unwrap_or_else(|| text.clone()));
let text = StoredValue::new(text);
let on_click = move |_| {
text.with_value(|text| {
value.with_value(|value| {
combobox.on_option_select(value, text);
});
});
};
view! { view! {
<div <div
@ -14,17 +26,15 @@ pub fn ComboboxOption(#[prop(into)] key: String, children: Children) -> impl Int
aria-selected="true" aria-selected="true"
class=class_list![ class=class_list![
"thaw-combobox-option", "thaw-combobox-option",
("thaw-combobox-option--selected", move || key.with_value(|key| combobox.is_selected(&key))) ("thaw-combobox-option--selected", move || value.with_value(|value| combobox.is_selected(&value)))
] ]
on:click=move |_| { on:click=on_click
key.with_value(|key| combobox.on_option_select(key));
}
> >
{ {
if combobox.multiselect { if combobox.multiselect {
view! { view! {
<span aria-hidden="true" class="thaw-combobox-option__check-icon--multiselect"> <span aria-hidden="true" class="thaw-combobox-option__check-icon--multiselect">
<If cond=Signal::derive(move || key.with_value(|key| combobox.is_selected(&key)))> <If cond=Signal::derive(move || value.with_value(|value| combobox.is_selected(&value)))>
<Then slot> <Then slot>
<svg fill="currentColor" aria-hidden="true" width="12" height="12" viewBox="0 0 12 12"> <svg fill="currentColor" aria-hidden="true" width="12" height="12" viewBox="0 0 12 12">
<path d="M9.76 3.2c.3.29.32.76.04 1.06l-4.25 4.5a.75.75 0 0 1-1.08.02L2.22 6.53a.75.75 0 0 1 1.06-1.06l1.7 1.7L8.7 3.24a.75.75 0 0 1 1.06-.04Z" fill="currentColor"></path> <path d="M9.76 3.2c.3.29.32.76.04 1.06l-4.25 4.5a.75.75 0 0 1-1.08.02L2.22 6.53a.75.75 0 0 1 1.06-1.06l1.7 1.7L8.7 3.24a.75.75 0 0 1 1.06-.04Z" fill="currentColor"></path>
@ -43,7 +53,12 @@ pub fn ComboboxOption(#[prop(into)] key: String, children: Children) -> impl Int
} }
} }
} }
{children()} <OptionComp value=children let:children>
<Fallback slot>
{text.get_value()}
</Fallback>
{children()}
</OptionComp>
</div> </div>
} }
} }