diff --git a/demo_markdown/docs/combobox/mod.md b/demo_markdown/docs/combobox/mod.md index 8270981..a143efd 100644 --- a/demo_markdown/docs/combobox/mod.md +++ b/demo_markdown/docs/combobox/mod.md @@ -1,7 +1,7 @@ # Combobox ```rust demo -let selected_options = RwSignal::new(vec![]); +let selected_options = RwSignal::new(None::); view! { @@ -17,7 +17,7 @@ view! { let selected_options = RwSignal::new(vec![]); view! { - + @@ -30,7 +30,7 @@ view! { let selected_options = RwSignal::new(vec![]); view! { - + diff --git a/thaw/src/combobox/combobox.rs b/thaw/src/combobox/combobox.rs index b9cf1b9..a567c61 100644 --- a/thaw/src/combobox/combobox.rs +++ b/thaw/src/combobox/combobox.rs @@ -3,13 +3,12 @@ use crate::_aria::use_active_descendant; use leptos::{context::Provider, ev, html, prelude::*}; use std::collections::HashMap; use thaw_components::{Binder, Follower, FollowerPlacement, FollowerWidth}; -use thaw_utils::{add_event_listener, mount_style, Model}; +use thaw_utils::{add_event_listener, mount_style, Model, VecModel}; #[component] pub fn Combobox( #[prop(optional, into)] value: Model, - #[prop(optional, into)] selected_options: Model>, - #[prop(optional)] multiselect: bool, + #[prop(optional, into)] selected_options: VecModel, #[prop(optional)] clearable: bool, children: Children, ) -> impl IntoView { @@ -23,7 +22,12 @@ pub fn Combobox( let clear_icon_ref = NodeRef::::new(); let is_show_clear_icon = Memo::new(move |_| { if clearable { - selected_options.with(|options| !options.is_empty()) + selected_options.with(|options| match options { + (None, None, Some(v)) => !v.is_empty(), + (None, Some(v), None) => v.is_some(), + (Some(v), None, None) => !v.is_empty(), + _ => unreachable!(), + }) } else { false } @@ -75,21 +79,24 @@ pub fn Combobox( 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; - } + if selected_options.with_untracked(|options| match options { + (None, None, Some(_)) => false, + (None, Some(v), None) => { + if let Some(v) = v.as_ref() { + v != &input_value + } else { + false } - false - }) { - selected_options.set(vec![]); } + (Some(v), None, None) => v != &input_value, + _ => unreachable!(), + }) { + selected_options.set(vec![]); } value.set(input_value); }; + let multiselect = selected_options.is_vec(); let combobox_injection = ComboboxInjection { value, multiselect, @@ -103,13 +110,20 @@ pub fn Combobox( let on_blur = { let active_descendant_controller = active_descendant_controller.clone(); move |_| { - if multiselect { - value.set(String::new()); - } else { - if selected_options.with_untracked(|options| options.is_empty()) { - value.set(String::new()); + selected_options.with_untracked(|options| match options { + (None, None, Some(_)) => value.set(String::new()), + (None, Some(v), None) => { + if v.is_none() { + value.set(String::new()) + } } - } + (Some(v), None, None) => { + if v.is_empty() { + value.set(String::new()) + } + } + _ => unreachable!(), + }); active_descendant_controller.blur(); } }; @@ -208,7 +222,7 @@ pub fn Combobox( #[derive(Clone, Copy)] pub(crate) struct ComboboxInjection { value: Model, - selected_options: Model>, + selected_options: VecModel, options: StoredValue>, is_show_listbox: RwSignal, pub multiselect: bool, @@ -229,23 +243,40 @@ impl ComboboxInjection { } pub fn is_selected(&self, value: &String) -> bool { - self.selected_options - .with(|options| options.contains(value)) + self.selected_options.with(|options| match options { + (None, None, Some(v)) => v.contains(value), + (None, Some(v), None) => { + if let Some(v) = v.as_ref() { + v == value + } else { + false + } + } + (Some(v), None, None) => v == value, + _ => unreachable!(), + }) } pub fn select_option(&self, value: &String, text: &String) { - self.selected_options.update(|options| { - if self.multiselect { - if let Some(index) = options.iter().position(|v| v == value) { - options.remove(index); + self.selected_options.update(|options| match options { + (None, None, Some(v)) => { + if let Some(index) = v.iter().position(|v| v == value) { + v.remove(index); return; } - options.push(value.clone()); - } else { - *options = vec![value.clone()]; + v.push(value.clone()); + } + (None, Some(v), None) => { + *v = Some(value.clone()); self.value.set(text.clone()); self.is_show_listbox.set(false); } + (Some(v), None, None) => { + *v = value.clone(); + self.value.set(text.clone()); + self.is_show_listbox.set(false); + } + _ => unreachable!(), }); } } diff --git a/thaw_utils/src/signals/mod.rs b/thaw_utils/src/signals/mod.rs index 3290905..a8b3364 100644 --- a/thaw_utils/src/signals/mod.rs +++ b/thaw_utils/src/signals/mod.rs @@ -5,7 +5,7 @@ mod signal_watch; mod stored_maybe_signal; pub use component_ref::ComponentRef; -pub use model::{Model, OptionModel}; +pub use model::{Model, OptionModel, VecModel}; pub use optional_maybe_signal::OptionalMaybeSignal; pub use signal_watch::SignalWatch; pub use stored_maybe_signal::StoredMaybeSignal; diff --git a/thaw_utils/src/signals/model.rs b/thaw_utils/src/signals/model.rs index b286f7b..5c8f86c 100644 --- a/thaw_utils/src/signals/model.rs +++ b/thaw_utils/src/signals/model.rs @@ -1,6 +1,8 @@ mod option_model; +mod vec_model; pub use option_model::OptionModel; +pub use vec_model::VecModel; use leptos::reactive_graph::{ computed::Memo, diff --git a/thaw_utils/src/signals/model/vec_model.rs b/thaw_utils/src/signals/model/vec_model.rs new file mode 100644 index 0000000..d6ab1b8 --- /dev/null +++ b/thaw_utils/src/signals/model/vec_model.rs @@ -0,0 +1,277 @@ +use leptos::{ + prelude::Update, + reactive_graph::{ + computed::Memo, + signal::{ReadSignal, RwSignal, WriteSignal}, + traits::{GetUntracked, Set, With, WithUntracked}, + wrappers::read::Signal, + }, +}; + +pub enum VecModel +where + T: 'static, +{ + T(Signal, WriteSignal, Option>), + Option( + Signal>, + WriteSignal>, + Option>>, + ), + Vec( + Signal>, + WriteSignal>, + Option>>, + ), +} + +impl Default for VecModel { + fn default() -> Self { + Self::new(Default::default()) + } +} + +impl Clone for VecModel { + fn clone(&self) -> Self { + *self + } +} + +impl Copy for VecModel {} + +impl VecModel { + fn new(value: T) -> Self { + Self::new_option(Some(value)) + } + + fn new_option(value: Option) -> Self { + let rw_signal = RwSignal::new(value); + rw_signal.into() + } + + fn new_vec(value: Vec) -> Self { + let rw_signal = RwSignal::new(value); + rw_signal.into() + } + + pub fn is_vec(&self) -> bool { + if let VecModel::Vec(_, _, _) = self { + true + } else { + false + } + } + + pub fn with( + &self, + fun: impl FnOnce((Option<&T>, Option<&Option>, Option<&Vec>)) -> O, + ) -> O { + match self { + Self::T(read, _, _) => read.with(|value| fun((Some(value), None, None))), + Self::Option(read, _, _) => read.with(|value| fun((None, Some(value), None))), + Self::Vec(read, _, _) => read.with(|value| fun((None, None, Some(value)))), + } + } + + pub fn with_untracked( + &self, + fun: impl FnOnce((Option<&T>, Option<&Option>, Option<&Vec>)) -> O, + ) -> O { + match self { + Self::T(read, _, _) => read.with_untracked(|value| fun((Some(value), None, None))), + Self::Option(read, _, _) => read.with_untracked(|value| fun((None, Some(value), None))), + Self::Vec(read, _, _) => read.with_untracked(|value| fun((None, None, Some(value)))), + } + } +} + +impl VecModel { + pub fn set(&self, mut value: Vec) { + match self { + Self::T(read, write, on_write) => { + let value = if value.is_empty() { + Default::default() + } else { + value.remove(0) + }; + + write.set(value); + if let Some(on_write) = on_write.as_ref() { + on_write.set(read.get_untracked()); + } + } + Self::Option(read, write, on_write) => { + let value = if value.is_empty() { + None + } else { + Some(value.remove(0)) + }; + write.set(value); + if let Some(on_write) = on_write.as_ref() { + on_write.set(read.get_untracked()); + } + } + Self::Vec(read, write, on_write) => { + write.set(value); + if let Some(on_write) = on_write.as_ref() { + on_write.set(read.get_untracked()); + } + } + } + } + + pub fn update( + &self, + fun: impl FnOnce((Option<&mut T>, Option<&mut Option>, Option<&mut Vec>)), + ) { + match self { + Self::T(read, write, on_write) => { + write.update(move |write| { + fun((Some(write), None, None)); + }); + if let Some(on_write) = on_write.as_ref() { + on_write.set(read.get_untracked()); + } + } + Self::Option(read, write, on_write) => { + write.update(move |write| { + fun((None, Some(write), None)); + }); + if let Some(on_write) = on_write.as_ref() { + on_write.set(read.get_untracked()); + } + } + Self::Vec(read, write, on_write) => { + write.update(move |write| { + fun((None, None, Some(write))); + }); + if let Some(on_write) = on_write.as_ref() { + on_write.set(read.get_untracked()); + } + } + } + } +} + +impl From for VecModel { + fn from(value: T) -> Self { + Self::new(value) + } +} + +impl From> for VecModel { + fn from(value: Option) -> Self { + Self::new_option(value) + } +} + +impl From> for VecModel { + fn from(value: Vec) -> Self { + Self::new_vec(value) + } +} + +impl From> for VecModel { + fn from(rw_signal: RwSignal) -> Self { + let (read, write) = rw_signal.split(); + Self::T(read.into(), write, None) + } +} + +impl From>> for VecModel { + fn from(rw_signal: RwSignal>) -> Self { + let (read, write) = rw_signal.split(); + Self::Option(read.into(), write, None) + } +} + +impl From>> for VecModel { + fn from(rw_signal: RwSignal>) -> Self { + let (read, write) = rw_signal.split(); + Self::Vec(read.into(), write, None) + } +} + +impl From<(Signal, WriteSignal)> for VecModel { + fn from((read, write): (Signal, WriteSignal)) -> Self { + Self::T(read, write, None) + } +} + +impl From<(Signal>, WriteSignal>)> for VecModel { + fn from((read, write): (Signal>, WriteSignal>)) -> Self { + Self::Option(read, write, None) + } +} + +impl From<(Signal>, WriteSignal>)> for VecModel { + fn from((read, write): (Signal>, WriteSignal>)) -> Self { + Self::Vec(read, write, None) + } +} + +impl From<(ReadSignal, WriteSignal)> for VecModel { + fn from((read, write): (ReadSignal, WriteSignal)) -> Self { + Self::T(read.into(), write, None) + } +} + +impl From<(ReadSignal>, WriteSignal>)> for VecModel { + fn from((read, write): (ReadSignal>, WriteSignal>)) -> Self { + Self::Option(read.into(), write, None) + } +} + +impl From<(ReadSignal>, WriteSignal>)> for VecModel { + fn from((read, write): (ReadSignal>, WriteSignal>)) -> Self { + Self::Vec(read.into(), write, None) + } +} + +impl From<(Memo, WriteSignal)> for VecModel { + fn from((read, write): (Memo, WriteSignal)) -> Self { + Self::T(read.into(), write, None) + } +} + +impl From<(Memo>, WriteSignal>)> for VecModel { + fn from((read, write): (Memo>, WriteSignal>)) -> Self { + Self::Option(read.into(), write, None) + } +} + +impl From<(Memo>, WriteSignal>)> for VecModel { + fn from((read, write): (Memo>, WriteSignal>)) -> Self { + Self::Vec(read.into(), write, None) + } +} + +impl From<(Option, WriteSignal)> for VecModel { + fn from((read, write): (Option, WriteSignal)) -> Self { + let mut model = Self::new(read.unwrap_or_default()); + if let VecModel::T(_, _, on_write) = &mut model { + *on_write = Some(write); + } + model + } +} + +impl From<(Option>, WriteSignal>)> for VecModel { + fn from((read, write): (Option>, WriteSignal>)) -> Self { + let mut model = Self::new_option(read.unwrap_or_default()); + if let VecModel::Option(_, _, on_write) = &mut model { + *on_write = Some(write); + } + model + } +} + +impl From<(Option>, WriteSignal>)> for VecModel { + fn from((read, write): (Option>, WriteSignal>)) -> Self { + let mut model = Self::new_vec(read.unwrap_or_default()); + if let VecModel::Vec(_, _, on_write) = &mut model { + *on_write = Some(write); + } + model + } +}