feat: SpinButton adds parser and format props

This commit is contained in:
luoxiao 2024-08-02 00:13:19 +08:00
parent 6cb1335299
commit 9e0bf0eaa7
5 changed files with 127 additions and 58 deletions

View file

@ -6,7 +6,7 @@ let value = RwSignal::new(String::from("o"));
view! { view! {
<Space vertical=true> <Space vertical=true>
<Input value/> <Input value/>
<Input value variant=InputVariant::Password placeholder="Password"/> <Input value input_type=InputType::Password placeholder="Password"/>
</Space> </Space>
} }
``` ```
@ -83,19 +83,24 @@ view! {
} }
``` ```
### Input attrs ### Custom parsing
```rust demo ```rust demo
let value = RwSignal::new(String::from("loren_ipsun"));
let format = move |v: String| {
v.replace("_", " ")
};
let parser = move |v: String| {
Some(v.replace(" ", "_"))
};
view! { view! {
<Space> <Input value parser format />
<label for="demo-input-attrs">"Do you like cheese?"</label> <p>"Underlying value: "{ value }</p>
<Input attr:id="demo-input-attrs"/>
</Space>
} }
``` ```
### Input Props ### Input Props
| Name | Type | Default | Description | | Name | Type | Default | Description |

View file

@ -31,3 +31,47 @@ view! {
<SpinButton value step_page=1 disabled=true/> <SpinButton value step_page=1 disabled=true/>
} }
``` ```
### Custom parsing
```rust demo
let value = RwSignal::new(0.0);
let format = move |v: f64| {
let v = v.to_string();
let dot_pos = v.chars().position(|c| c == '.').unwrap_or_else(|| v.chars().count());
let mut int: String = v.chars().take(dot_pos).collect();
let sign: String = if v.chars().take(1).collect::<String>() == String::from("-") {
int = int.chars().skip(1).collect();
String::from("-")
} else {
String::from("")
};
let dec: String = v.chars().skip(dot_pos + 1).take(2).collect();
let int = int
.as_bytes()
.rchunks(3)
.rev()
.map(std::str::from_utf8)
.collect::<Result<Vec<&str>, _>>()
.unwrap()
.join(".");
format!("{}{},{:0<2}", sign, int, dec)
};
let parser = move |v: String| {
let comma_pos = v.chars().position(|c| c == ',').unwrap_or_else(|| v.chars().count());
let int_part = v.chars().take(comma_pos).filter(|a| a.is_digit(10)).collect::<String>();
let dec_part = v.chars().skip(comma_pos + 1).take(2).filter(|a| a.is_digit(10)).collect::<String>();
format!("{:0<1}.{:0<2}", int_part, dec_part).parse::<f64>().ok()
};
view! {
<SpinButton value parser format step_page=1.0 />
<p>"Underlying value: "{ value }</p>
}
```

View file

@ -34,45 +34,44 @@ pub fn Input(
#[prop(optional)] input_suffix: Option<InputSuffix>, #[prop(optional)] input_suffix: Option<InputSuffix>,
#[prop(optional)] comp_ref: ComponentRef<InputRef>, #[prop(optional)] comp_ref: ComponentRef<InputRef>,
#[prop(optional, into)] class: MaybeProp<String>, #[prop(optional, into)] class: MaybeProp<String>,
#[prop(optional, into)] parser: OptionalProp<BoxOneCallback<String, String>>, #[prop(optional, into)] parser: OptionalProp<BoxOneCallback<String, Option<String>>>,
#[prop(optional, into)] format: OptionalProp<BoxOneCallback<String, String>>, #[prop(optional, into)] format: OptionalProp<BoxOneCallback<String, String>>,
// #[prop(attrs)] attrs: Vec<(&'static str, Attribute)>, // #[prop(attrs)] attrs: Vec<(&'static str, Attribute)>,
) -> impl IntoView { ) -> impl IntoView {
mount_style("input", include_str!("./input.css")); mount_style("input", include_str!("./input.css"));
let value_trigger = ArcTrigger::new();
let parser_none = parser.is_none(); let parser_none = parser.is_none();
let on_input = { let on_input = {
let value_trigger = value_trigger.clone();
let allow_value = allow_value.clone(); let allow_value = allow_value.clone();
move |e| { move |e| {
if parser_none { if !parser_none {
let input_value = event_target_value(&e); return;
if let Some(allow_value) = allow_value.as_ref() {
if !allow_value(input_value.clone()) {
value_trigger.trigger();
return;
}
}
value.set(input_value);
} }
let input_value = event_target_value(&e);
if let Some(allow_value) = allow_value.as_ref() {
if !allow_value(input_value.clone()) {
value.update(|_| {});
return;
}
}
value.set(input_value);
} }
}; };
let on_change = { let on_change = move |e| {
let value_trigger = value_trigger.clone(); let Some(parser) = parser.as_ref() else {
move |e| { return;
if let Some(parser) = parser.as_ref() { };
let parsed_input_value = parser(event_target_value(&e)); let Some(parsed_input_value) = parser(event_target_value(&e)) else {
if let Some(allow_value) = allow_value.as_ref() { value.update(|_| {});
if !allow_value(parsed_input_value.clone()) { return;
value_trigger.trigger(); };
return; if let Some(allow_value) = allow_value.as_ref() {
} if !allow_value(parsed_input_value.clone()) {
} value.update(|_| {});
value.set(parsed_input_value); return;
} }
} }
value.set(parsed_input_value);
}; };
let is_focus = RwSignal::new(false); let is_focus = RwSignal::new(false);
let on_internal_focus = move |ev| { let on_internal_focus = move |ev| {
@ -152,8 +151,12 @@ pub fn Input(
type=move || input_type.get().as_str() type=move || input_type.get().as_str()
value=input_value value=input_value
prop:value=move || { prop:value=move || {
value_trigger.track(); let value = value.get();
format.as_ref().map_or_else(|| value.get(), |f| f(value.get())) if let Some(format) = format.as_ref() {
format(value)
} else {
value.to_string()
}
} }
on:input=on_input on:input=on_input

View file

@ -2,7 +2,9 @@ use leptos::prelude::*;
use num_traits::Bounded; use num_traits::Bounded;
use std::ops::{Add, Sub}; use std::ops::{Add, Sub};
use std::str::FromStr; use std::str::FromStr;
use thaw_utils::{class_list, mount_style, with, Model, StoredMaybeSignal}; use thaw_utils::{
class_list, mount_style, with, BoxOneCallback, Model, OptionalProp, StoredMaybeSignal,
};
#[component] #[component]
pub fn SpinButton<T>( pub fn SpinButton<T>(
@ -12,6 +14,8 @@ pub fn SpinButton<T>(
#[prop(default = T::min_value().into(), into)] min: MaybeSignal<T>, #[prop(default = T::min_value().into(), into)] min: MaybeSignal<T>,
#[prop(default = T::max_value().into(), into)] max: MaybeSignal<T>, #[prop(default = T::max_value().into(), into)] max: MaybeSignal<T>,
#[prop(optional, into)] disabled: MaybeSignal<bool>, #[prop(optional, into)] disabled: MaybeSignal<bool>,
#[prop(optional, into)] parser: OptionalProp<BoxOneCallback<String, Option<T>>>,
#[prop(optional, into)] format: OptionalProp<BoxOneCallback<T, String>>,
) -> impl IntoView ) -> impl IntoView
where where
T: Send + Sync, T: Send + Sync,
@ -24,19 +28,6 @@ where
let step_page: StoredMaybeSignal<_> = step_page.into(); let step_page: StoredMaybeSignal<_> = step_page.into();
let min: StoredMaybeSignal<_> = min.into(); let min: StoredMaybeSignal<_> = min.into();
let max: StoredMaybeSignal<_> = max.into(); let max: StoredMaybeSignal<_> = max.into();
let input_value = RwSignal::new(String::new());
Effect::new_isomorphic(move |prev| {
value.with(|value| {
if let Some(prev) = prev {
if value == &prev {
return prev;
}
}
input_value.set(value.to_string());
value.clone()
})
});
let update_value = move |new_value| { let update_value = move |new_value| {
let min = min.get_untracked(); let min = min.get_untracked();
@ -53,6 +44,21 @@ where
let increment_disabled = Memo::new(move |_| disabled.get() || value.get() >= max.get()); let increment_disabled = Memo::new(move |_| disabled.get() || value.get() >= max.get());
let decrement_disabled = Memo::new(move |_| disabled.get() || value.get() <= min.get()); let decrement_disabled = Memo::new(move |_| disabled.get() || value.get() <= min.get());
let on_change = move |e| {
let target_value = event_target_value(&e);
let v = if let Some(parser) = parser.as_ref() {
parser(target_value)
} else {
target_value.parse::<T>().ok()
};
if let Some(value) = v {
update_value(value);
} else {
value.update(|_| {});
}
};
view! { view! {
<span <span
class=class_list!["thaw-spin-button", ("thaw-spin-button--disabled", move || disabled.get()), class] class=class_list!["thaw-spin-button", ("thaw-spin-button--disabled", move || disabled.get()), class]
@ -64,16 +70,16 @@ where
type="text" type="text"
disabled=move || disabled.get() disabled=move || disabled.get()
value=initialization_value value=initialization_value
prop:value=move || input_value.get() prop:value=move || {
class="thaw-spin-button__input" let value = value.get();
on:change=move |e| { if let Some(format) = format.as_ref() {
let target_value = event_target_value(&e); format(value)
let Ok(v) = target_value.parse::<T>() else { } else {
input_value.update(|_| {}); value.to_string()
return; }
};
update_value(v);
} }
class="thaw-spin-button__input"
on:change=on_change
/> />
<button <button
tabindex="-1" tabindex="-1"

View file

@ -1,6 +1,8 @@
use leptos::prelude::{MaybeSignal, Memo, ReadSignal, RwSignal, Signal}; use leptos::prelude::{MaybeSignal, Memo, ReadSignal, RwSignal, Signal};
use std::ops::{Deref, DerefMut}; use std::ops::{Deref, DerefMut};
use crate::BoxOneCallback;
pub struct OptionalProp<T>(Option<T>); pub struct OptionalProp<T>(Option<T>);
impl<T> Default for OptionalProp<T> { impl<T> Default for OptionalProp<T> {
@ -92,6 +94,15 @@ impl<T> From<Signal<T>> for OptionalProp<MaybeSignal<T>> {
} }
} }
impl<F, A, Return> From<F> for OptionalProp<BoxOneCallback<A, Return>>
where
F: Fn(A) -> Return + Send + Sync + 'static,
{
fn from(value: F) -> Self {
Self(Some(BoxOneCallback::new(value)))
}
}
impl<T> From<Option<T>> for OptionalProp<T> { impl<T> From<Option<T>> for OptionalProp<T> {
fn from(value: Option<T>) -> Self { fn from(value: Option<T>) -> Self {
Self(value) Self(value)