impulse_thaw/spin_button/
mod.rs

1mod rule;
2mod types;
3
4pub use rule::*;
5pub use types::*;
6
7use crate::{FieldInjection, Rule};
8use leptos::prelude::*;
9use num_traits::Bounded;
10use std::ops::{Add, Sub};
11use std::str::FromStr;
12use thaw_utils::{class_list, mount_style, with, BoxOneCallback, Model, OptionalProp};
13
14/// SpinButton are used to allow numerical input bounded between minimum and maximum values
15/// with buttons to increment and decrement the input value.
16///
17/// Note: SpinButton is a generic component, so the type must be specified. Example: `<SpinButton<i32> step_page=1/>`.
18/// [Related issue](https://github.com/leptos-rs/leptos/issues/3200)
19#[component]
20pub fn SpinButton<T>(
21    #[prop(optional, into)] class: MaybeProp<String>,
22    #[prop(optional, into)] id: MaybeProp<String>,
23    /// A string specifying a name for the input control.
24    /// This name is submitted along with the control's value when the form data is submitted.
25    #[prop(optional, into)]
26    name: MaybeProp<String>,
27    /// The rules to validate Field.
28    #[prop(optional, into)]
29    rules: Vec<SpinButtonRule<T>>,
30    /// Current value of the control.
31    #[prop(optional, into)]
32    value: Model<T>,
33    /// Large difference between two values. This should be greater
34    /// than step and is used when users hit the Page Up or Page Down keys.
35    #[prop(into)]
36    step_page: Signal<T>,
37    /// The minimum number that the input value can take.
38    #[prop(default = T::min_value().into(), into)]
39    min: Signal<T>,
40    /// The maximum number that the input value can take.
41    #[prop(default = T::max_value().into(), into)]
42    max: Signal<T>,
43    /// Placeholder of input number.
44    #[prop(optional, into)]
45    placeholder: MaybeProp<String>,
46    /// Whether the input is disabled.
47    #[prop(optional, into)]
48    disabled: Signal<bool>,
49    /// Size of the input.
50    #[prop(optional, into)]
51    size: Signal<SpinButtonSize>,
52    /// Modifies the user input before assigning it to the value.
53    #[prop(optional, into)]
54    parser: OptionalProp<BoxOneCallback<String, Option<T>>>,
55    /// Formats the value to be shown to the user.
56    #[prop(optional, into)]
57    format: OptionalProp<BoxOneCallback<T, String>>,
58) -> impl IntoView
59where
60    T: Send + Sync,
61    T: Add<Output = T> + Sub<Output = T> + PartialOrd + Bounded,
62    T: Default + Clone + FromStr + ToString + 'static,
63{
64    mount_style("spin-button", include_str!("./spin-button.css"));
65    let (id, name) = FieldInjection::use_id_and_name(id, name);
66    let validate = Rule::validate(rules, value, name);
67    let initialization_value = value.get_untracked().to_string();
68
69    let update_value = move |new_value| {
70        if with!(|value| value == &new_value) {
71            return;
72        }
73        let min = min.get_untracked();
74        let max = max.get_untracked();
75
76        if new_value < min {
77            value.set(min);
78        } else if new_value > max {
79            value.set(max);
80        } else {
81            value.set(new_value);
82        }
83        validate.run(Some(SpinButtonRuleTrigger::Change));
84    };
85
86    let increment_disabled = Memo::new(move |_| disabled.get() || value.get() >= max.get());
87    let decrement_disabled = Memo::new(move |_| disabled.get() || value.get() <= min.get());
88
89    let on_change = move |e| {
90        let target_value = event_target_value(&e);
91        let v = if let Some(parser) = parser.as_ref() {
92            parser(target_value)
93        } else {
94            target_value.parse::<T>().ok()
95        };
96
97        if let Some(value) = v {
98            update_value(value);
99        } else {
100            value.update(|_| {});
101        }
102    };
103
104    view! {
105        <span class=class_list![
106            "thaw-spin-button",
107            ("thaw-spin-button--disabled", move || disabled.get()),
108            move || format!("thaw-spin-button--{}", size.get().as_str()),
109            class
110        ]>
111            <input
112                autocomplete="off"
113                role="spinbutton"
114                aria-valuenow=move || value.get().to_string()
115                type="text"
116                disabled=move || disabled.get()
117                placeholder=move || placeholder.get()
118                value=initialization_value
119                prop:value=move || {
120                    let value = value.get();
121                    if let Some(format) = format.as_ref() {
122                        format(value)
123                    } else {
124                        value.to_string()
125                    }
126                }
127                class="thaw-spin-button__input"
128                id=id
129                name=name
130                on:change=on_change
131            />
132            <button
133                tabindex="-1"
134                aria-label="Increment value"
135                type="button"
136                class="thaw-spin-button__increment-button"
137                class=(
138                    "thaw-spin-button__increment-button--disabled",
139                    move || increment_disabled.get(),
140                )
141                disabled=move || disabled.get()
142                on:click=move |_| {
143                    if !increment_disabled.get_untracked() {
144                        update_value(value.get_untracked() + step_page.get_untracked());
145                    }
146                }
147            >
148                <svg
149                    fill="currentColor"
150                    aria-hidden="true"
151                    width="16"
152                    height="16"
153                    viewBox="0 0 16 16"
154                >
155                    <path
156                        d="M3.15 10.35c.2.2.5.2.7 0L8 6.21l4.15 4.14a.5.5 0 0 0 .7-.7l-4.5-4.5a.5.5 0 0 0-.7 0l-4.5 4.5a.5.5 0 0 0 0 .7Z"
157                        fill="currentColor"
158                    ></path>
159                </svg>
160            </button>
161            <button
162                tabindex="-1"
163                aria-label="Decrement value"
164                type="button"
165                class="thaw-spin-button__decrement-button"
166                disabled=move || disabled.get()
167                class=(
168                    "thaw-spin-button__decrement-button--disabled",
169                    move || decrement_disabled.get(),
170                )
171                on:click=move |_| {
172                    if !decrement_disabled.get_untracked() {
173                        update_value(value.get_untracked() - step_page.get_untracked());
174                    }
175                }
176            >
177                <svg
178                    fill="currentColor"
179                    aria-hidden="true"
180                    width="16"
181                    height="16"
182                    viewBox="0 0 16 16"
183                >
184                    <path
185                        d="M3.15 5.65c.2-.2.5-.2.7 0L8 9.79l4.15-4.14a.5.5 0 0 1 .7.7l-4.5 4.5a.5.5 0 0 1-.7 0l-4.5-4.5a.5.5 0 0 1 0-.7Z"
186                        fill="currentColor"
187                    ></path>
188                </svg>
189            </button>
190        </span>
191    }
192}