mirror of
https://github.com/adoyle0/thaw.git
synced 2025-01-22 22:09:22 -05:00
feat: SpinButton adds parser and format props
This commit is contained in:
parent
6cb1335299
commit
9e0bf0eaa7
5 changed files with 127 additions and 58 deletions
|
@ -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 |
|
||||
|
|
|
@ -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>
|
||||
}
|
||||
```
|
|
@ -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
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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)
|
||||
|
|
Loading…
Add table
Reference in a new issue