impulse_thaw/input/
mod.rs

1mod 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    /// A string specifying a name for the input control.
20    /// This name is submitted along with the control's value when the form data is submitted.
21    #[prop(optional, into)]
22    name: MaybeProp<String>,
23    /// The rules to validate Field.
24    #[prop(optional, into)]
25    rules: Vec<InputRule>,
26    /// Set the input value.
27    #[prop(optional, into)]
28    value: Model<String>,
29    /// Check the incoming value, if it returns false, input will not be accepted.
30    #[prop(optional, into)]
31    allow_value: Option<ArcOneCallback<String, bool>>,
32    /// An input can have different text-based types based on the type of value the user will enter.
33    #[prop(optional, into)]
34    input_type: Signal<InputType>,
35    /// Placeholder text for the input.
36    #[prop(optional, into)]
37    placeholder: MaybeProp<String>,
38    /// Callback triggered when the input is focussed on.
39    #[prop(optional, into)]
40    on_focus: Option<BoxOneCallback<ev::FocusEvent>>,
41    /// Callback triggered when the input is blurred.
42    #[prop(optional, into)]
43    on_blur: Option<BoxOneCallback<ev::FocusEvent>>,
44    /// Whether the input is disabled.
45    #[prop(optional, into)]
46    disabled: Signal<bool>,
47    /// Whether the input is readonly.
48    #[prop(optional, into)]
49    readonly: Signal<bool>,
50    /// Input size width.
51    #[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    /// Modifies the user input before assigning it to the value.
58    #[prop(optional, into)]
59    parser: OptionalProp<BoxOneCallback<String, Option<String>>>,
60    /// Formats the value to be shown to the user.
61    #[prop(optional, into)]
62    format: OptionalProp<BoxOneCallback<String, String>>,
63    /// Size of the input (changes the font size and spacing).
64    #[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}