/ src / frontend / src / utils / input_number_within_range.rs
input_number_within_range.rs
 1  use crate::utils::stored_maybe_signal::StoredMaybeSignal;
 2  use icondata as i;
 3  use leptos::*;
 4  use std::cmp::PartialOrd;
 5  use std::ops::{Add, Sub};
 6  use std::str::FromStr;
 7  use thaw::{Button, ButtonVariant, Icon, Input, InputSuffix};
 8  
 9  #[component]
10  pub fn InputNumberWithinRange<T>(
11      #[prop(optional, into)] value: RwSignal<T>,
12      #[prop(optional, into)] placeholder: MaybeSignal<String>,
13      #[prop(into)] step: MaybeSignal<T>,
14      #[prop(into)] min: MaybeSignal<T>,
15      #[prop(into)] max: MaybeSignal<T>,
16  ) -> impl IntoView
17  where
18      T: Add<Output = T> + Sub<Output = T> + PartialOrd,
19      T: Default + Clone + FromStr + ToString + 'static,
20  {
21      let input_value = create_rw_signal(String::default());
22  
23      create_effect(move |prev| {
24          value.with(|value| {
25              let value = value.to_string();
26              if let Some(prev) = prev {
27                  if value == prev {
28                      return prev;
29                  }
30              }
31              input_value.set(value.clone());
32              value
33          })
34      });
35  
36      let allow_value = Callback::<String, bool>::new(move |v: String| {
37          let Ok(v) = v.parse::<T>() else { return false };
38          value.set(v);
39          true
40      });
41      let step: StoredMaybeSignal<_> = step.into();
42      let min: StoredMaybeSignal<_> = min.into();
43      let max: StoredMaybeSignal<_> = max.into();
44  
45      let add = Callback::<ev::MouseEvent>::new(move |_| {
46          let prev = value.get_untracked();
47          if prev >= max.get_untracked() {
48              return;
49          }
50          value.set(prev + step.get_untracked());
51      });
52      let sub = Callback::<ev::MouseEvent>::new(move |_| {
53          let prev = value.get_untracked();
54          if prev <= min.get_untracked() {
55              return;
56          }
57          value.set(prev - step.get_untracked());
58      });
59      let set_within_range = Callback::<ev::FocusEvent>::new(move |_| {
60          let value_gotten = value.get_untracked();
61          let min = min.get_untracked();
62          let max = max.get_untracked();
63          if value_gotten < min {
64              value.set(min.clone());
65          } else if value_gotten > max {
66              value.set(max.clone());
67          }
68      });
69      view! {
70          <Input value=input_value allow_value placeholder on_blur=set_within_range>
71              <InputSuffix slot>
72                  <Button variant=ButtonVariant::Link on_click=sub>
73                      <Icon
74                          icon=i::AiMinusOutlined
75                          style="font-size: 18px"
76                      />
77                  </Button>
78                  <Button variant=ButtonVariant::Link on_click=add>
79                      <Icon
80                          icon=i::AiPlusOutlined
81                          style="font-size: 18px"
82                      />
83                  </Button>
84              </InputSuffix>
85          </Input>
86      }
87  }