impulse_thaw/spin_button/
mod.rs1mod 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#[component]
20pub fn SpinButton<T>(
21 #[prop(optional, into)] class: MaybeProp<String>,
22 #[prop(optional, into)] id: MaybeProp<String>,
23 #[prop(optional, into)]
26 name: MaybeProp<String>,
27 #[prop(optional, into)]
29 rules: Vec<SpinButtonRule<T>>,
30 #[prop(optional, into)]
32 value: Model<T>,
33 #[prop(into)]
36 step_page: Signal<T>,
37 #[prop(default = T::min_value().into(), into)]
39 min: Signal<T>,
40 #[prop(default = T::max_value().into(), into)]
42 max: Signal<T>,
43 #[prop(optional, into)]
45 placeholder: MaybeProp<String>,
46 #[prop(optional, into)]
48 disabled: Signal<bool>,
49 #[prop(optional, into)]
51 size: Signal<SpinButtonSize>,
52 #[prop(optional, into)]
54 parser: OptionalProp<BoxOneCallback<String, Option<T>>>,
55 #[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}