refactor: OptionModel and VecModel with

This commit is contained in:
luoxiao 2024-08-21 14:07:26 +08:00 committed by luoxiaozero
parent 3bba024540
commit d33b4e27e8
12 changed files with 104 additions and 67 deletions

View file

@ -2,7 +2,7 @@ use crate::{Button, ButtonGroup};
use chrono::{Datelike, Days, Local, Month, Months, NaiveDate}; use chrono::{Datelike, Days, Local, Month, Months, NaiveDate};
use leptos::prelude::*; use leptos::prelude::*;
use std::ops::Deref; use std::ops::Deref;
use thaw_utils::{class_list, mount_style, OptionModel}; use thaw_utils::{class_list, mount_style, OptionModel, OptionModelWithValue};
#[component] #[component]
pub fn Calendar( pub fn Calendar(
@ -136,7 +136,12 @@ fn CalendarItem(
) -> impl IntoView { ) -> impl IntoView {
let is_selected = Memo::new({ let is_selected = Memo::new({
let date = date.clone(); let date = date.clone();
move |_| value.with(|value_date| value_date == Some(date.deref())) move |_| value.with(|value_date| {
match value_date {
OptionModelWithValue::T(v) => v == date.deref(),
OptionModelWithValue::Option(v) => v.as_ref() == Some(date.deref()),
}
})
}); });
let weekday_str = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"]; let weekday_str = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"];
let on_click = { let on_click = {

View file

@ -3,7 +3,7 @@ use crate::_aria::use_active_descendant;
use leptos::{context::Provider, ev, html, prelude::*}; use leptos::{context::Provider, ev, html, prelude::*};
use std::collections::HashMap; use std::collections::HashMap;
use thaw_components::{Binder, Follower, FollowerPlacement, FollowerWidth}; use thaw_components::{Binder, Follower, FollowerPlacement, FollowerWidth};
use thaw_utils::{add_event_listener, class_list, mount_style, Model, VecModel}; use thaw_utils::{add_event_listener, class_list, mount_style, Model, VecModel, VecModelWithValue};
#[component] #[component]
pub fn Combobox( pub fn Combobox(
@ -34,10 +34,9 @@ pub fn Combobox(
let is_show_clear_icon = Memo::new(move |_| { let is_show_clear_icon = Memo::new(move |_| {
if clearable { if clearable {
selected_options.with(|options| match options { selected_options.with(|options| match options {
(None, None, Some(v)) => !v.is_empty(), VecModelWithValue::T(v) => !v.is_empty(),
(None, Some(v), None) => v.is_some(), VecModelWithValue::Option(v) => v.is_some(),
(Some(v), None, None) => !v.is_empty(), VecModelWithValue::Vec(v) => !v.is_empty(),
_ => unreachable!(),
}) })
} else { } else {
false false
@ -94,16 +93,15 @@ pub fn Combobox(
let on_input = move |ev| { let on_input = move |ev| {
let input_value = event_target_value(&ev); let input_value = event_target_value(&ev);
if selected_options.with_untracked(|options| match options { if selected_options.with_untracked(|options| match options {
(None, None, Some(_)) => false, VecModelWithValue::T(v) => v != &input_value,
(None, Some(v), None) => { VecModelWithValue::Option(v) => {
if let Some(v) = v.as_ref() { if let Some(v) = v.as_ref() {
v != &input_value v != &input_value
} else { } else {
false false
} }
} }
(Some(v), None, None) => v != &input_value, VecModelWithValue::Vec(_) => false,
_ => unreachable!(),
}) { }) {
selected_options.set(vec![]); selected_options.set(vec![]);
} }
@ -125,18 +123,17 @@ pub fn Combobox(
let active_descendant_controller = active_descendant_controller.clone(); let active_descendant_controller = active_descendant_controller.clone();
move |_| { move |_| {
selected_options.with_untracked(|options| match options { selected_options.with_untracked(|options| match options {
(None, None, Some(_)) => value.set(String::new()), VecModelWithValue::T(v) => {
(None, Some(v), None) => {
if v.is_none() {
value.set(String::new())
}
}
(Some(v), None, None) => {
if v.is_empty() { if v.is_empty() {
value.set(String::new()) value.set(String::new())
} }
} }
_ => unreachable!(), VecModelWithValue::Option(v) => {
if v.is_none() {
value.set(String::new())
}
}
VecModelWithValue::Vec(_) => value.set(String::new()),
}); });
active_descendant_controller.blur(); active_descendant_controller.blur();
} }
@ -297,16 +294,15 @@ impl ComboboxInjection {
pub fn is_selected(&self, value: &String) -> bool { pub fn is_selected(&self, value: &String) -> bool {
self.selected_options.with(|options| match options { self.selected_options.with(|options| match options {
(None, None, Some(v)) => v.contains(value), VecModelWithValue::T(v) => v == value,
(None, Some(v), None) => { VecModelWithValue::Option(v) => {
if let Some(v) = v.as_ref() { if let Some(v) = v.as_ref() {
v == value v == value
} else { } else {
false false
} }
} }
(Some(v), None, None) => v == value, VecModelWithValue::Vec(v) => v.contains(value),
_ => unreachable!(),
}) })
} }

View file

@ -4,7 +4,7 @@ use chrono::NaiveDate;
use leptos::{html, prelude::*}; use leptos::{html, prelude::*};
use panel::{Panel, PanelRef}; use panel::{Panel, PanelRef};
use thaw_components::{Binder, Follower, FollowerPlacement}; use thaw_components::{Binder, Follower, FollowerPlacement};
use thaw_utils::{class_list, mount_style, now_date, ComponentRef, OptionModel}; use thaw_utils::{class_list, mount_style, now_date, ComponentRef, OptionModel, OptionModelWithValue};
#[component] #[component]
pub fn DatePicker( pub fn DatePicker(
@ -20,9 +20,13 @@ pub fn DatePicker(
let show_date_format = "%Y-%m-%d"; let show_date_format = "%Y-%m-%d";
let update_show_date_text = move || { let update_show_date_text = move || {
value.with_untracked(move |date| { value.with_untracked(move |date| {
let text = date.map_or(String::new(), |date| { let text = match date {
OptionModelWithValue::T(v) => v.format(show_date_format).to_string(),
OptionModelWithValue::Option(v) => v.map_or(String::new(), |date| {
date.format(show_date_format).to_string() date.format(show_date_format).to_string()
}); }),
};
show_date_text.set(text); show_date_text.set(text);
}); });
}; };

View file

@ -2,7 +2,7 @@ use super::{FieldContextInjection, FieldInjection, FieldValidationState};
use leptos::prelude::*; use leptos::prelude::*;
use send_wrapper::SendWrapper; use send_wrapper::SendWrapper;
use std::ops::Deref; use std::ops::Deref;
use thaw_utils::{Model, OptionModel}; use thaw_utils::{Model, OptionModel, OptionModelWithValue};
type RuleValidator<T> = Box<dyn Fn(&T, Signal<Option<String>>) -> Result<(), FieldValidationState>>; type RuleValidator<T> = Box<dyn Fn(&T, Signal<Option<String>>) -> Result<(), FieldValidationState>>;
@ -112,14 +112,20 @@ impl<T: Send + Sync> RuleValueWithUntracked<T> for Model<T> {
} }
} }
impl<T: Send + Sync + Clone> RuleValueWithUntracked<Option<T>> for OptionModel<T> { impl RuleValueWithUntracked<Option<String>> for OptionModel<String> {
fn value_with_untracked( fn value_with_untracked(
&self, &self,
f: impl FnOnce(&Option<T>) -> Result<(), FieldValidationState>, f: impl FnOnce(&Option<String>) -> Result<(), FieldValidationState>,
) -> Result<(), FieldValidationState> { ) -> Result<(), FieldValidationState> {
self.with_untracked(move |v| { self.with_untracked(move |v| match v {
let v = v.map(|v| v.clone()); OptionModelWithValue::T(v) => {
f(&v) if v.is_empty() {
f(&None)
} else {
f(&Some(v.clone()))
}
}
OptionModelWithValue::Option(v) => f(v),
}) })
} }
} }

View file

@ -2,7 +2,7 @@ use super::NavDrawerInjection;
use crate::Icon; use crate::Icon;
use leptos::{either::Either, html, prelude::*}; use leptos::{either::Either, html, prelude::*};
use thaw_components::CSSTransition; use thaw_components::CSSTransition;
use thaw_utils::{class_list, StoredMaybeSignal}; use thaw_utils::{class_list, StoredMaybeSignal, VecModelWithValue};
#[component] #[component]
pub fn NavCategory( pub fn NavCategory(
@ -18,10 +18,9 @@ pub fn NavCategory(
.selected_category_value .selected_category_value
.with(|selected_category_value| { .with(|selected_category_value| {
value.with(|value| match selected_category_value { value.with(|value| match selected_category_value {
(None, None, Some(v)) => v.contains(value), VecModelWithValue::T(v) => v == value,
(None, Some(v), None) => v.as_ref() == Some(value), VecModelWithValue::Option(v) => v.as_ref() == Some(value),
(Some(v), None, None) => v == value, VecModelWithValue::Vec(v) => v.contains(value),
_ => unreachable!(),
}) })
}) })
}); });

View file

@ -1,7 +1,7 @@
use super::NavDrawerInjection; use super::NavDrawerInjection;
use crate::Icon; use crate::Icon;
use leptos::{either::Either, prelude::*}; use leptos::{either::Either, prelude::*};
use thaw_utils::{class_list, StoredMaybeSignal}; use thaw_utils::{class_list, OptionModelWithValue, StoredMaybeSignal};
#[component] #[component]
pub fn NavItem( pub fn NavItem(
@ -17,7 +17,12 @@ pub fn NavItem(
let value = value.get_untracked(); let value = value.get_untracked();
if nav_drawer if nav_drawer
.selected_value .selected_value
.with_untracked(|selected_value| selected_value != Some(&value)) .with_untracked(|selected_value|
match selected_value {
OptionModelWithValue::T(v) => v != &value,
OptionModelWithValue::Option(v) => v.as_ref() != Some(&value),
}
)
{ {
nav_drawer.selected_value.set(Some(value)); nav_drawer.selected_value.set(Some(value));
} }
@ -39,7 +44,10 @@ pub fn NavItem(
let selected = Memo::new(move |_| { let selected = Memo::new(move |_| {
nav_drawer nav_drawer
.selected_value .selected_value
.with(|selected_value| value.with(|value| selected_value == Some(value))) .with(|selected_value| value.with(|value| match selected_value {
OptionModelWithValue::T(v) => v == value,
OptionModelWithValue::Option(v) => v.as_ref() == Some(&value),
}))
}); });
if let Some(href) = href { if let Some(href) = href {

View file

@ -4,7 +4,7 @@ pub use radio_group::{RadioGroup, RadioGroupRule, RadioGroupRuleTrigger};
use leptos::prelude::*; use leptos::prelude::*;
use radio_group::RadioGroupInjection; use radio_group::RadioGroupInjection;
use thaw_utils::{class_list, mount_style}; use thaw_utils::{class_list, mount_style, OptionModelWithValue};
#[component] #[component]
pub fn Radio( pub fn Radio(
@ -26,7 +26,10 @@ pub fn Radio(
let group = group.clone(); let group = group.clone();
move |_| { move |_| {
item_value item_value
.with_value(|value| group.value.with(|group_value| group_value == Some(value))) .with_value(|value| group.value.with(|group_value| match group_value {
OptionModelWithValue::T(v) => v == value,
OptionModelWithValue::Option(v) => v.as_ref() == Some(value),
}))
} }
}); });

View file

@ -5,7 +5,7 @@ use crate::{
use chrono::{Local, NaiveTime, Timelike}; use chrono::{Local, NaiveTime, Timelike};
use leptos::{html, prelude::*}; use leptos::{html, prelude::*};
use thaw_components::{Binder, CSSTransition, Follower, FollowerPlacement}; use thaw_components::{Binder, CSSTransition, Follower, FollowerPlacement};
use thaw_utils::{class_list, mount_style, ArcOneCallback, ComponentRef, OptionModel}; use thaw_utils::{class_list, mount_style, ArcOneCallback, ComponentRef, OptionModel, OptionModelWithValue};
#[component] #[component]
pub fn TimePicker( pub fn TimePicker(
@ -23,9 +23,13 @@ pub fn TimePicker(
let show_time_text = RwSignal::new(String::new()); let show_time_text = RwSignal::new(String::new());
let update_show_time_text = move || { let update_show_time_text = move || {
value.with_untracked(move |time| { value.with_untracked(move |time| {
let text = time.as_ref().map_or(String::new(), |time| { let text = match time {
OptionModelWithValue::T(v) => v.format(show_time_format).to_string(),
OptionModelWithValue::Option(v) => v.map_or(String::new(), |time| {
time.format(show_time_format).to_string() time.format(show_time_format).to_string()
}); }),
};
show_time_text.set(text); show_time_text.set(text);
}); });
}; };

View file

@ -4,6 +4,6 @@ mod signal_watch;
mod stored_maybe_signal; mod stored_maybe_signal;
pub use component_ref::ComponentRef; pub use component_ref::ComponentRef;
pub use model::{Model, OptionModel, VecModel}; pub use model::{Model, OptionModel, VecModel, VecModelWithValue, OptionModelWithValue};
pub use signal_watch::SignalWatch; pub use signal_watch::SignalWatch;
pub use stored_maybe_signal::StoredMaybeSignal; pub use stored_maybe_signal::StoredMaybeSignal;

View file

@ -1,8 +1,8 @@
mod option_model; mod option_model;
mod vec_model; mod vec_model;
pub use option_model::OptionModel; pub use option_model::{OptionModel, OptionModelWithValue};
pub use vec_model::VecModel; pub use vec_model::{VecModel, VecModelWithValue};
use leptos::reactive_graph::{ use leptos::reactive_graph::{
computed::Memo, computed::Memo,

View file

@ -48,21 +48,26 @@ impl<T: Send + Sync> OptionModel<T> {
rw_signal.into() rw_signal.into()
} }
pub fn with<O>(&self, fun: impl FnOnce(Option<&T>) -> O) -> O { pub fn with<O>(&self, fun: impl FnOnce(OptionModelWithValue<T>) -> O) -> O {
match self { match self {
Self::T(read, _, _) => read.with(|value| fun(Some(value))), Self::T(read, _, _) => read.with(|value| fun(OptionModelWithValue::T(value))),
Self::Option(read, _, _) => read.with(|value| fun(value.as_ref())), Self::Option(read, _, _) => read.with(|value| fun(OptionModelWithValue::Option(value))),
} }
} }
pub fn with_untracked<O>(&self, fun: impl FnOnce(Option<&T>) -> O) -> O { pub fn with_untracked<O>(&self, fun: impl FnOnce(OptionModelWithValue<T>) -> O) -> O {
match self { match self {
Self::T(read, _, _) => read.with_untracked(|value| fun(Some(value))), Self::T(read, _, _) => read.with_untracked(|value| fun(OptionModelWithValue::T(value))),
Self::Option(read, _, _) => read.with_untracked(|value| fun(value.as_ref())), Self::Option(read, _, _) => read.with_untracked(|value| fun(OptionModelWithValue::Option(value))),
} }
} }
} }
pub enum OptionModelWithValue<'a, T> {
T(&'a T),
Option(&'a Option<T>),
}
impl<T: Send + Sync + Clone> OptionModel<T> { impl<T: Send + Sync + Clone> OptionModel<T> {
pub fn get(&self) -> Option<T> { pub fn get(&self) -> Option<T> {
match self { match self {

View file

@ -69,25 +69,32 @@ impl<T: Send + Sync> VecModel<T> {
pub fn with<O>( pub fn with<O>(
&self, &self,
fun: impl FnOnce((Option<&T>, Option<&Option<T>>, Option<&Vec<T>>)) -> O, fun: impl FnOnce(VecModelWithValue<T>) -> O,
) -> O { ) -> O {
match self { match self {
Self::T(read, _, _) => read.with(|value| fun((Some(value), None, None))), Self::T(read, _, _) => read.with(|value| fun(VecModelWithValue::T(value))),
Self::Option(read, _, _) => read.with(|value| fun((None, Some(value), None))), Self::Option(read, _, _) => read.with(|value| fun(VecModelWithValue::Option(value))),
Self::Vec(read, _, _) => read.with(|value| fun((None, None, Some(value)))), Self::Vec(read, _, _) => read.with(|value| fun(VecModelWithValue::Vec(value))),
} }
} }
pub fn with_untracked<O>( pub fn with_untracked<O>(&self, fun: impl FnOnce(VecModelWithValue<T>) -> O) -> O {
&self,
fun: impl FnOnce((Option<&T>, Option<&Option<T>>, Option<&Vec<T>>)) -> O,
) -> O {
match self { match self {
Self::T(read, _, _) => read.with_untracked(|value| fun((Some(value), None, None))), Self::T(read, _, _) => read.with_untracked(|value| fun(VecModelWithValue::T(value))),
Self::Option(read, _, _) => read.with_untracked(|value| fun((None, Some(value), None))), Self::Option(read, _, _) => {
Self::Vec(read, _, _) => read.with_untracked(|value| fun((None, None, Some(value)))), read.with_untracked(|value| fun(VecModelWithValue::Option(value)))
}
Self::Vec(read, _, _) => {
read.with_untracked(|value| fun(VecModelWithValue::Vec(value)))
} }
} }
}
}
pub enum VecModelWithValue<'a, T> {
T(&'a T),
Option(&'a Option<T>),
Vec(&'a Vec<T>),
} }
impl<T: Send + Sync + Clone + Default> VecModel<T> { impl<T: Send + Sync + Clone + Default> VecModel<T> {