impulse_thaw/input/
mod.rs1mod rule;
2mod types;
3
4pub use rule::*;
5pub use types::*;
6
7use crate::{FieldInjection, Rule};
8use leptos::{ev, html, prelude::*};
9
10use thaw_utils::{
11 class_list, mount_style, ArcOneCallback, BoxOneCallback, ComponentRef, Model, OptionalProp,
12};
13
14#[component]
15pub fn Input(
16 #[prop(optional, into)] class: MaybeProp<String>,
17 #[prop(optional, into)] id: MaybeProp<String>,
18 #[prop(optional, into)] autofocus: MaybeProp<bool>,
19 #[prop(optional, into)]
22 name: MaybeProp<String>,
23 #[prop(optional, into)]
25 rules: Vec<InputRule>,
26 #[prop(optional, into)]
28 value: Model<String>,
29 #[prop(optional, into)]
31 allow_value: Option<ArcOneCallback<String, bool>>,
32 #[prop(optional, into)]
34 input_type: Signal<InputType>,
35 #[prop(optional, into)]
37 placeholder: MaybeProp<String>,
38 #[prop(optional, into)]
40 on_focus: Option<BoxOneCallback<ev::FocusEvent>>,
41 #[prop(optional, into)]
43 on_blur: Option<BoxOneCallback<ev::FocusEvent>>,
44 #[prop(optional, into)]
46 disabled: Signal<bool>,
47 #[prop(optional, into)]
49 readonly: Signal<bool>,
50 #[prop(optional, into)]
52 input_size: Signal<Option<i32>>,
53 #[prop(optional)] input_prefix: Option<InputPrefix>,
54 #[prop(optional)] input_suffix: Option<InputSuffix>,
55 #[prop(optional, into)] input_style: MaybeProp<String>,
56 #[prop(optional)] comp_ref: ComponentRef<InputRef>,
57 #[prop(optional, into)]
59 parser: OptionalProp<BoxOneCallback<String, Option<String>>>,
60 #[prop(optional, into)]
62 format: OptionalProp<BoxOneCallback<String, String>>,
63 #[prop(optional, into)]
65 size: Signal<InputSize>,
66 #[prop(optional, into)] autocomplete: MaybeProp<String>,
67) -> impl IntoView {
68 mount_style("input", include_str!("./input.css"));
69 let (id, name) = FieldInjection::use_id_and_name(id, name);
70 let validate = Rule::validate(rules, value, name);
71 let autofocus = autofocus.get_untracked().unwrap_or(false);
72
73 let parser_none = parser.is_none();
74 let on_input = {
75 let allow_value = allow_value.clone();
76 move |e| {
77 if !parser_none {
78 validate.run(Some(InputRuleTrigger::Input));
79 return;
80 }
81 let input_value = event_target_value(&e);
82 if let Some(allow_value) = allow_value.as_ref() {
83 if !allow_value(input_value.clone()) {
84 value.update(|_| {});
85 return;
86 }
87 }
88 value.set(input_value);
89 validate.run(Some(InputRuleTrigger::Input));
90 }
91 };
92 let on_change = move |e| {
93 let Some(parser) = parser.as_ref() else {
94 validate.run(Some(InputRuleTrigger::Change));
95 return;
96 };
97 let Some(parsed_input_value) = parser(event_target_value(&e)) else {
98 value.update(|_| {});
99 return;
100 };
101 if let Some(allow_value) = allow_value.as_ref() {
102 if !allow_value(parsed_input_value.clone()) {
103 value.update(|_| {});
104 return;
105 }
106 }
107 value.set(parsed_input_value);
108 validate.run(Some(InputRuleTrigger::Change));
109 };
110 let is_focus = RwSignal::new(false);
111 let on_internal_focus = move |ev| {
112 is_focus.set(true);
113 if let Some(on_focus) = on_focus.as_ref() {
114 on_focus(ev);
115 }
116 validate.run(Some(InputRuleTrigger::Focus));
117 };
118 let on_internal_blur = move |ev| {
119 is_focus.set(false);
120 if let Some(on_blur) = on_blur.as_ref() {
121 on_blur(ev);
122 }
123 validate.run(Some(InputRuleTrigger::Blur));
124 };
125
126 let input_ref = NodeRef::<html::Input>::new();
127 comp_ref.load(InputRef { input_ref });
128
129 let on_mousedown = move |event: ev::MouseEvent| {
130 let el: web_sys::HtmlElement = event_target(&event);
131
132 if el.tag_name() != "INPUT" {
133 event.prevent_default();
134 if !is_focus.get_untracked() {
135 if let Some(comp_ref) = comp_ref.get_untracked() {
136 comp_ref.focus();
137 }
138 }
139 }
140 };
141
142 let input_value = value.get_untracked();
143
144 let prefix_if_ = input_prefix.as_ref().map_or(false, |prefix| prefix.if_);
145 let suffix_if_ = input_suffix.as_ref().map_or(false, |suffix| suffix.if_);
146
147 view! {
148 <span
149 class=class_list![
150 "thaw-input",
151 ("thaw-input--prefix", prefix_if_),
152 ("thaw-input--suffix", suffix_if_),
153 ("thaw-input--disabled", move || disabled.get()),
154 move || format!("thaw-input--{}", size.get().as_str()),
155 class
156 ]
157
158 on:mousedown=on_mousedown
159 >
160 {if let Some(prefix) = input_prefix.and_then(|prefix| prefix.if_.then_some(prefix)) {
161 view! { <div class="thaw-input__prefix">{(prefix.children)()}</div> }.into()
162 } else {
163 None
164 }}
165
166 <input
167 id=id
168 type=move || input_type.get().as_str()
169 name=name
170 value=input_value
171 autofocus=autofocus
172 prop:value=move || {
173 let value = value.get();
174 if let Some(format) = format.as_ref() {
175 format(value)
176 } else {
177 value.to_string()
178 }
179 }
180
181 on:input=on_input
182 on:change=on_change
183 on:focus=on_internal_focus
184 on:blur=on_internal_blur
185 class="thaw-input__input"
186 disabled=disabled
187 readonly=readonly
188 size=input_size
189 placeholder=move || placeholder.get()
190 node_ref=input_ref
191 style=move || input_style.get()
192 autocomplete=move || autocomplete.get()
193 />
194
195 {if let Some(suffix) = input_suffix.and_then(|suffix| suffix.if_.then_some(suffix)) {
196 view! { <div class="thaw-input__suffix">{(suffix.children)()}</div> }.into()
197 } else {
198 None
199 }}
200
201 </span>
202 }
203}