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! {
<Space vertical=true>
<Input value/>
<Input value variant=InputVariant::Password placeholder="Password"/>
<Input value input_type=InputType::Password placeholder="Password"/>
</Space>
}
```
@ -83,19 +83,24 @@ view! {
}
```
### Input attrs
### Custom parsing
```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! {
<Space>
<label for="demo-input-attrs">"Do you like cheese?"</label>
<Input attr:id="demo-input-attrs"/>
</Space>
<Input value parser format />
<p>"Underlying value: "{ value }</p>
}
```
### Input Props
| Name | Type | Default | Description |

View file

@ -30,4 +30,48 @@ let value = RwSignal::new(0);
view! {
<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)] comp_ref: ComponentRef<InputRef>,
#[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(attrs)] attrs: Vec<(&'static str, Attribute)>,
) -> impl IntoView {
mount_style("input", include_str!("./input.css"));
let value_trigger = ArcTrigger::new();
let parser_none = parser.is_none();
let on_input = {
let value_trigger = value_trigger.clone();
let allow_value = allow_value.clone();
move |e| {
if parser_none {
let input_value = event_target_value(&e);
if let Some(allow_value) = allow_value.as_ref() {
if !allow_value(input_value.clone()) {
value_trigger.trigger();
return;
}
}
value.set(input_value);
if !parser_none {
return;
}
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 value_trigger = value_trigger.clone();
move |e| {
if let Some(parser) = parser.as_ref() {
let parsed_input_value = parser(event_target_value(&e));
if let Some(allow_value) = allow_value.as_ref() {
if !allow_value(parsed_input_value.clone()) {
value_trigger.trigger();
return;
}
}
value.set(parsed_input_value);
let on_change = move |e| {
let Some(parser) = parser.as_ref() else {
return;
};
let Some(parsed_input_value) = parser(event_target_value(&e)) else {
value.update(|_| {});
return;
};
if let Some(allow_value) = allow_value.as_ref() {
if !allow_value(parsed_input_value.clone()) {
value.update(|_| {});
return;
}
}
value.set(parsed_input_value);
};
let is_focus = RwSignal::new(false);
let on_internal_focus = move |ev| {
@ -152,8 +151,12 @@ pub fn Input(
type=move || input_type.get().as_str()
value=input_value
prop:value=move || {
value_trigger.track();
format.as_ref().map_or_else(|| value.get(), |f| f(value.get()))
let value = value.get();
if let Some(format) = format.as_ref() {
format(value)
} else {
value.to_string()
}
}
on:input=on_input

View file

@ -2,7 +2,9 @@ use leptos::prelude::*;
use num_traits::Bounded;
use std::ops::{Add, Sub};
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]
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::max_value().into(), into)] max: MaybeSignal<T>,
#[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
where
T: Send + Sync,
@ -24,19 +28,6 @@ where
let step_page: StoredMaybeSignal<_> = step_page.into();
let min: StoredMaybeSignal<_> = min.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 min = min.get_untracked();
@ -53,6 +44,21 @@ where
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 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! {
<span
class=class_list!["thaw-spin-button", ("thaw-spin-button--disabled", move || disabled.get()), class]
@ -64,16 +70,16 @@ where
type="text"
disabled=move || disabled.get()
value=initialization_value
prop:value=move || input_value.get()
class="thaw-spin-button__input"
on:change=move |e| {
let target_value = event_target_value(&e);
let Ok(v) = target_value.parse::<T>() else {
input_value.update(|_| {});
return;
};
update_value(v);
prop:value=move || {
let value = value.get();
if let Some(format) = format.as_ref() {
format(value)
} else {
value.to_string()
}
}
class="thaw-spin-button__input"
on:change=on_change
/>
<button
tabindex="-1"

View file

@ -1,6 +1,8 @@
use leptos::prelude::{MaybeSignal, Memo, ReadSignal, RwSignal, Signal};
use std::ops::{Deref, DerefMut};
use crate::BoxOneCallback;
pub struct OptionalProp<T>(Option<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> {
fn from(value: Option<T>) -> Self {
Self(value)