mirror of
https://github.com/yewprint/yewprint
synced 2024-11-22 11:33:04 +00:00
Improve numeric input (#130)
This commit is contained in:
parent
89e6c93b86
commit
9c0a670eed
3 changed files with 229 additions and 110 deletions
|
@ -1,5 +1,6 @@
|
|||
use std::borrow::Cow;
|
||||
use yew::prelude::*;
|
||||
use yewprint::{Button, IconName, NumericInput, Text};
|
||||
use yewprint::{Button, Callout, IconName, Intent, NumericInput};
|
||||
|
||||
pub struct Example {
|
||||
props: ExampleProps,
|
||||
|
@ -13,6 +14,9 @@ pub struct ExampleProps {
|
|||
pub fill: bool,
|
||||
pub disabled: bool,
|
||||
pub large: bool,
|
||||
pub disable_buttons: bool,
|
||||
pub buttons_on_the_left: bool,
|
||||
pub left_icon: bool,
|
||||
}
|
||||
|
||||
pub enum Msg {
|
||||
|
@ -65,21 +69,27 @@ impl Component for Example {
|
|||
<NumericInput<i32>
|
||||
disabled=self.props.disabled
|
||||
fill=self.props.large
|
||||
value=self.value_two
|
||||
bounds=i32::MIN..=i32::MAX
|
||||
value=self.value
|
||||
bounds={-105..}
|
||||
increment=10
|
||||
placeholder=String::from("Enter an integer...")
|
||||
onchange=self.link.callback(|x| Msg::UpdateValueTwo(x))
|
||||
placeholder=String::from("Greater or equal to -105...")
|
||||
onchange=self.link.callback(|x| Msg::UpdateValue(x))
|
||||
disable_buttons=self.props.disable_buttons
|
||||
buttons_on_the_left=self.props.buttons_on_the_left
|
||||
left_icon=self.props.left_icon.then(|| IconName::Dollar)
|
||||
/>
|
||||
<NumericInput<i32>
|
||||
disabled=self.props.disabled
|
||||
fill=self.props.fill
|
||||
large=self.props.large
|
||||
value=self.value
|
||||
bounds=-10..=10
|
||||
value=self.value_two
|
||||
bounds={-10..=10}
|
||||
increment=1
|
||||
placeholder=String::from("Integer between -10 and 10")
|
||||
onchange=self.link.callback(|x| Msg::UpdateValue(x))
|
||||
onchange=self.link.callback(|x| Msg::UpdateValueTwo(x))
|
||||
disable_buttons=self.props.disable_buttons
|
||||
buttons_on_the_left=self.props.buttons_on_the_left
|
||||
left_icon=self.props.left_icon.then(|| IconName::Dollar)
|
||||
/>
|
||||
<Button
|
||||
icon=IconName::Refresh
|
||||
|
@ -87,7 +97,15 @@ impl Component for Example {
|
|||
>
|
||||
{"Reset at 4"}
|
||||
</Button>
|
||||
<Text>{format!("actual values are {} and {}", self.value, self.value_two)}</Text>
|
||||
<Callout
|
||||
title=Cow::Borrowed("Selected values")
|
||||
intent=Intent::Primary
|
||||
>
|
||||
<ul>
|
||||
<li>{self.value}</li>
|
||||
<li>{self.value_two}</li>
|
||||
</ul>
|
||||
</Callout>
|
||||
</>
|
||||
}
|
||||
}
|
||||
|
|
|
@ -21,6 +21,9 @@ impl Component for NumericInputDoc {
|
|||
fill: false,
|
||||
disabled: false,
|
||||
large: false,
|
||||
disable_buttons: false,
|
||||
buttons_on_the_left: false,
|
||||
left_icon: false,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
@ -93,6 +96,30 @@ crate::build_example_prop_component! {
|
|||
checked=self.props.large
|
||||
label=html!("Large")
|
||||
/>
|
||||
<Switch
|
||||
onclick=self.update_props(|props, _| ExampleProps {
|
||||
disable_buttons: !props.disable_buttons,
|
||||
..props
|
||||
})
|
||||
checked=self.props.disable_buttons
|
||||
label=html!("Disable buttons")
|
||||
/>
|
||||
<Switch
|
||||
onclick=self.update_props(|props, _| ExampleProps {
|
||||
buttons_on_the_left: !props.buttons_on_the_left,
|
||||
..props
|
||||
})
|
||||
checked=self.props.buttons_on_the_left
|
||||
label=html!("Buttons on the left")
|
||||
/>
|
||||
<Switch
|
||||
onclick=self.update_props(|props, _| ExampleProps {
|
||||
left_icon: !props.left_icon,
|
||||
..props
|
||||
})
|
||||
checked=self.props.left_icon
|
||||
label=html!("Left icon")
|
||||
/>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,21 +1,20 @@
|
|||
use crate::{Button, ButtonGroup, ControlGroup, IconName, InputGroup, Intent};
|
||||
use std::fmt::Display;
|
||||
use std::ops::{Add, RangeInclusive, Sub};
|
||||
use std::ops::{Add, Bound, RangeBounds, Sub};
|
||||
use std::str::FromStr;
|
||||
use yew::html::IntoPropValue;
|
||||
use yew::prelude::*;
|
||||
|
||||
pub struct NumericInput<
|
||||
pub struct NumericInput<T>
|
||||
where
|
||||
T: Add<Output = T>
|
||||
+ Clone
|
||||
+ Sub<Output = T>
|
||||
+ Copy
|
||||
+ Display
|
||||
+ FromStr
|
||||
+ PartialEq
|
||||
+ PartialOrd
|
||||
+ Sub<Output = T>
|
||||
+ 'static,
|
||||
> where
|
||||
<T as Sub>::Output: ToString,
|
||||
<T as Add>::Output: ToString,
|
||||
{
|
||||
props: NumericInputProps<T>,
|
||||
link: ComponentLink<Self>,
|
||||
|
@ -23,18 +22,16 @@ pub struct NumericInput<
|
|||
}
|
||||
|
||||
#[derive(Clone, PartialEq, Properties)]
|
||||
pub struct NumericInputProps<
|
||||
pub struct NumericInputProps<T>
|
||||
where
|
||||
T: Add<Output = T>
|
||||
+ Clone
|
||||
+ Sub<Output = T>
|
||||
+ Copy
|
||||
+ Display
|
||||
+ FromStr
|
||||
+ PartialEq
|
||||
+ PartialOrd
|
||||
+ Sub<Output = T>
|
||||
+ 'static,
|
||||
> where
|
||||
<T as Sub>::Output: ToString,
|
||||
<T as Add>::Output: ToString,
|
||||
{
|
||||
#[prop_or_default]
|
||||
pub disabled: bool,
|
||||
|
@ -49,35 +46,40 @@ pub struct NumericInputProps<
|
|||
#[prop_or_default]
|
||||
pub left_icon: Option<IconName>,
|
||||
#[prop_or_default]
|
||||
pub left_element: Option<Html>,
|
||||
#[prop_or_default]
|
||||
pub right_element: Option<Html>,
|
||||
#[prop_or_default]
|
||||
pub intent: Option<Intent>,
|
||||
#[prop_or_default]
|
||||
pub onchange: Callback<T>,
|
||||
pub value: T,
|
||||
#[prop_or_default]
|
||||
pub value: Option<T>,
|
||||
pub bounds: RangeInclusive<T>,
|
||||
pub bounds: NumericInputRangeBounds<T>,
|
||||
pub increment: T,
|
||||
#[prop_or_default]
|
||||
pub disable_buttons: bool,
|
||||
#[prop_or_default]
|
||||
pub buttons_on_the_left: bool,
|
||||
}
|
||||
|
||||
pub enum Msg {
|
||||
UpdateValue(String),
|
||||
InputUpdate(String),
|
||||
Up,
|
||||
Down,
|
||||
Noop,
|
||||
}
|
||||
|
||||
impl<
|
||||
impl<T> Component for NumericInput<T>
|
||||
where
|
||||
T: Add<Output = T>
|
||||
+ Clone
|
||||
+ Sub<Output = T>
|
||||
+ Copy
|
||||
+ Display
|
||||
+ FromStr
|
||||
+ PartialEq
|
||||
+ PartialOrd
|
||||
+ Sub<Output = T>
|
||||
+ 'static,
|
||||
> Component for NumericInput<T>
|
||||
where
|
||||
<T as Sub>::Output: ToString,
|
||||
<T as Add>::Output: ToString,
|
||||
{
|
||||
type Message = Msg;
|
||||
type Properties = NumericInputProps<T>;
|
||||
|
@ -92,54 +94,15 @@ where
|
|||
|
||||
fn update(&mut self, msg: Self::Message) -> ShouldRender {
|
||||
match msg {
|
||||
Msg::UpdateValue(value) => {
|
||||
if let Ok(num) = value.trim().parse::<T>() {
|
||||
if num >= *self.props.bounds.end() {
|
||||
self.props.value = Some(self.props.bounds.end().clone());
|
||||
self.input = self.props.bounds.end().to_string();
|
||||
} else if num <= *self.props.bounds.start() {
|
||||
self.props.value = Some(self.props.bounds.start().clone());
|
||||
self.input = self.props.bounds.start().to_string();
|
||||
} else {
|
||||
self.props.value = Some(num);
|
||||
self.input = value;
|
||||
}
|
||||
self.props.onchange.emit(self.props.value.clone().unwrap());
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
Msg::Up => {
|
||||
if let Some(num) = self.props.value.clone() {
|
||||
if num >= *self.props.bounds.end() {
|
||||
self.props.value = Some(self.props.bounds.end().clone());
|
||||
self.input = self.props.bounds.end().to_string();
|
||||
} else {
|
||||
self.props.value = Some(num.clone() + self.props.increment.clone());
|
||||
self.input = (num + self.props.increment.clone()).to_string();
|
||||
}
|
||||
self.props.onchange.emit(self.props.value.clone().unwrap());
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
Msg::Down => {
|
||||
if let Some(num) = self.props.value.clone() {
|
||||
if num <= *self.props.bounds.start() {
|
||||
self.props.value = Some(self.props.bounds.start().clone());
|
||||
self.input = self.props.bounds.start().to_string();
|
||||
} else {
|
||||
self.props.value = Some(num.clone() - self.props.increment.clone());
|
||||
self.input = (num - self.props.increment.clone()).to_string();
|
||||
}
|
||||
self.props.onchange.emit(self.props.value.clone().unwrap());
|
||||
true
|
||||
Msg::InputUpdate(new_value) => {
|
||||
if let Ok(new_value) = new_value.trim().parse::<T>() {
|
||||
self.update_value(new_value)
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
Msg::Up => self.update_value(self.props.value + self.props.increment),
|
||||
Msg::Down => self.update_value(self.props.value - self.props.increment),
|
||||
Msg::Noop => false,
|
||||
}
|
||||
}
|
||||
|
@ -147,9 +110,7 @@ where
|
|||
fn change(&mut self, props: Self::Properties) -> ShouldRender {
|
||||
if self.props != props {
|
||||
if self.props.value != props.value {
|
||||
if let Some(value) = props.value.as_ref() {
|
||||
self.input = value.to_string();
|
||||
}
|
||||
self.input = props.value.to_string();
|
||||
}
|
||||
self.props = props;
|
||||
true
|
||||
|
@ -159,19 +120,47 @@ where
|
|||
}
|
||||
|
||||
fn view(&self) -> Html {
|
||||
let NumericInputProps {
|
||||
value,
|
||||
increment,
|
||||
disabled,
|
||||
disable_buttons,
|
||||
buttons_on_the_left,
|
||||
..
|
||||
} = self.props;
|
||||
let bounds = &self.props.bounds;
|
||||
let button_up_disabled = disabled || bounds.clamp(value + increment, increment) == value;
|
||||
let button_down_disabled = disabled || bounds.clamp(value - increment, increment) == value;
|
||||
|
||||
let buttons = if disable_buttons {
|
||||
html!()
|
||||
} else {
|
||||
html! {
|
||||
<ControlGroup
|
||||
class=classes!("bp3-numeric-input")
|
||||
fill=self.props.fill
|
||||
large=self.props.large
|
||||
>
|
||||
<ButtonGroup vertical=true class=classes!("bp3-fixed")>
|
||||
<Button
|
||||
icon=IconName::ChevronUp
|
||||
disabled=button_up_disabled
|
||||
onclick=self.link.callback(|_| Msg::Up)
|
||||
/>
|
||||
<Button
|
||||
icon=IconName::ChevronDown
|
||||
disabled=button_down_disabled
|
||||
onclick=self.link.callback(|_| Msg::Down)
|
||||
/>
|
||||
</ButtonGroup>
|
||||
}
|
||||
};
|
||||
|
||||
let input_group = html! {
|
||||
<InputGroup
|
||||
placeholder=self.props.placeholder.clone()
|
||||
large=self.props.large
|
||||
disabled=self.props.disabled
|
||||
left_icon=self.props.left_icon
|
||||
left_element=self.props.left_element.clone()
|
||||
right_element=self.props.right_element.clone()
|
||||
value=self.input.clone()
|
||||
oninput=self.link.callback(|e: InputData| Msg::UpdateValue(e.value))
|
||||
oninput=self.link.callback(|e: InputData| Msg::InputUpdate(e.value))
|
||||
onkeydown=self.link.callback(|e: KeyboardEvent| {
|
||||
if e.key() == "ArrowUp" {
|
||||
Msg::Up
|
||||
|
@ -182,19 +171,104 @@ where
|
|||
}
|
||||
})
|
||||
/>
|
||||
<ButtonGroup vertical=true class=classes!("bp3-fixed")>
|
||||
<Button
|
||||
icon=IconName::ChevronUp
|
||||
disabled=self.props.disabled
|
||||
onclick=self.link.callback(|_| Msg::Up)
|
||||
/>
|
||||
<Button
|
||||
icon=IconName::ChevronDown
|
||||
disabled=self.props.disabled
|
||||
onclick=self.link.callback(|_| Msg::Down)
|
||||
/>
|
||||
</ButtonGroup>
|
||||
};
|
||||
|
||||
if buttons_on_the_left {
|
||||
html! {
|
||||
<ControlGroup
|
||||
class=classes!("bp3-numeric-input")
|
||||
fill=self.props.fill
|
||||
large=self.props.large
|
||||
>
|
||||
{buttons}
|
||||
{input_group}
|
||||
</ControlGroup>
|
||||
}
|
||||
} else {
|
||||
html! {
|
||||
<ControlGroup
|
||||
class=classes!("bp3-numeric-input")
|
||||
fill=self.props.fill
|
||||
large=self.props.large
|
||||
>
|
||||
{input_group}
|
||||
{buttons}
|
||||
</ControlGroup>
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> NumericInput<T>
|
||||
where
|
||||
T: Add<Output = T>
|
||||
+ Sub<Output = T>
|
||||
+ Copy
|
||||
+ Display
|
||||
+ FromStr
|
||||
+ PartialEq
|
||||
+ PartialOrd
|
||||
+ 'static,
|
||||
{
|
||||
fn update_value(&mut self, new_value: T) -> ShouldRender {
|
||||
let new_value = self.props.bounds.clamp(new_value, self.props.increment);
|
||||
|
||||
if new_value != self.props.value {
|
||||
self.input = new_value.to_string();
|
||||
self.props.onchange.emit(new_value);
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub struct NumericInputRangeBounds<T> {
|
||||
pub start: Bound<T>,
|
||||
pub end: Bound<T>,
|
||||
}
|
||||
|
||||
impl<T> Default for NumericInputRangeBounds<T> {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
start: Bound::Unbounded,
|
||||
end: Bound::Unbounded,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> NumericInputRangeBounds<T>
|
||||
where
|
||||
T: PartialOrd + Copy + Add<Output = T> + Sub<Output = T>,
|
||||
{
|
||||
pub fn clamp(&self, value: T, increment: T) -> T {
|
||||
match (self.start, self.end) {
|
||||
(Bound::Included(min), _) if value < min => min,
|
||||
(Bound::Excluded(min), _) if value <= min => min + increment,
|
||||
(_, Bound::Included(max)) if value > max => max,
|
||||
(_, Bound::Excluded(max)) if value >= max => max - increment,
|
||||
_ => value,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
macro_rules! impl_into_prop_value {
|
||||
($path:path) => {
|
||||
impl<T: Copy> IntoPropValue<NumericInputRangeBounds<T>> for $path {
|
||||
fn into_prop_value(self) -> NumericInputRangeBounds<T> {
|
||||
NumericInputRangeBounds {
|
||||
start: self.start_bound().cloned(),
|
||||
end: self.end_bound().cloned(),
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
impl_into_prop_value!(std::ops::Range<T>);
|
||||
impl_into_prop_value!(std::ops::RangeFrom<T>);
|
||||
impl_into_prop_value!(std::ops::RangeFull);
|
||||
impl_into_prop_value!(std::ops::RangeInclusive<T>);
|
||||
impl_into_prop_value!(std::ops::RangeTo<T>);
|
||||
impl_into_prop_value!(std::ops::RangeToInclusive<T>);
|
||||
|
|
Loading…
Reference in a new issue