mirror of
https://github.com/adoyle0/thaw.git
synced 2025-02-08 19:03:09 -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! {
|
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 |
|
||||||
|
|
|
@ -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>
|
||||||
|
}
|
||||||
|
```
|
|
@ -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 {
|
||||||
|
return;
|
||||||
|
}
|
||||||
let input_value = event_target_value(&e);
|
let input_value = event_target_value(&e);
|
||||||
if let Some(allow_value) = allow_value.as_ref() {
|
if let Some(allow_value) = allow_value.as_ref() {
|
||||||
if !allow_value(input_value.clone()) {
|
if !allow_value(input_value.clone()) {
|
||||||
value_trigger.trigger();
|
value.update(|_| {});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
value.set(input_value);
|
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 {
|
||||||
|
value.update(|_| {});
|
||||||
|
return;
|
||||||
|
};
|
||||||
if let Some(allow_value) = allow_value.as_ref() {
|
if let Some(allow_value) = allow_value.as_ref() {
|
||||||
if !allow_value(parsed_input_value.clone()) {
|
if !allow_value(parsed_input_value.clone()) {
|
||||||
value_trigger.trigger();
|
value.update(|_| {});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
value.set(parsed_input_value);
|
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
|
||||||
|
|
|
@ -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"
|
||||||
|
|
|
@ -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)
|
||||||
|
|
Loading…
Add table
Reference in a new issue