Improve numeric input (#130)

This commit is contained in:
Cecile Tonglet 2021-11-23 08:41:26 +01:00 committed by GitHub
parent 89e6c93b86
commit 9c0a670eed
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
3 changed files with 229 additions and 110 deletions

View file

@ -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>
</>
}
}

View file

@ -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>
}
}

View file

@ -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<
T: Add<Output = T>
+ Clone
+ Display
+ FromStr
+ PartialEq
+ PartialOrd
+ Sub<Output = T>
+ 'static,
> Component for NumericInput<T>
impl<T> Component for NumericInput<T>
where
<T as Sub>::Output: ToString,
<T as Add>::Output: ToString,
T: Add<Output = T>
+ Sub<Output = T>
+ Copy
+ Display
+ FromStr
+ PartialEq
+ PartialOrd
+ 'static,
{
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,42 +120,155 @@ where
}
fn view(&self) -> Html {
html! {
<ControlGroup
class=classes!("bp3-numeric-input")
fill=self.props.fill
large=self.props.large
>
<InputGroup
placeholder=self.props.placeholder.clone()
large=self.props.large
disabled=self.props.disabled
left_icon=self.props.left_icon
value=self.input.clone()
oninput=self.link.callback(|e: InputData| Msg::UpdateValue(e.value))
onkeydown=self.link.callback(|e: KeyboardEvent| {
if e.key() == "ArrowUp" {
Msg::Up
} else if e.key() == "ArrowDown" {
Msg::Down
} else {
Msg::Noop
}
})
/>
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! {
<ButtonGroup vertical=true class=classes!("bp3-fixed")>
<Button
icon=IconName::ChevronUp
disabled=self.props.disabled
disabled=button_up_disabled
onclick=self.link.callback(|_| Msg::Up)
/>
<Button
icon=IconName::ChevronDown
disabled=self.props.disabled
disabled=button_down_disabled
onclick=self.link.callback(|_| Msg::Down)
/>
</ButtonGroup>
</ControlGroup>
}
};
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::InputUpdate(e.value))
onkeydown=self.link.callback(|e: KeyboardEvent| {
if e.key() == "ArrowUp" {
Msg::Up
} else if e.key() == "ArrowDown" {
Msg::Down
} else {
Msg::Noop
}
})
/>
};
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>);