leptos_twelements/components/forms/
input.rs1use leptos::{html::Div, *};
2use wasm_bindgen::prelude::wasm_bindgen;
3use web_sys::HtmlDivElement;
4
5use crate::utils::{MaybeSignalExt, SignalBoolExt};
6
7#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
9pub enum InputType {
10    Text,
13
14    Email,
17
18    Password,
21
22    Number,
25
26    Tel,
29
30    Url,
33}
34
35impl InputType {
36    const fn html_attrib(&self) -> &'static str {
37        match self {
38            Self::Text => "text",
39            Self::Email => "email",
40            Self::Password => "password",
41            Self::Number => "number",
42            Self::Tel => "tel",
43            Self::Url => "url",
44        }
45    }
46}
47
48#[component]
52pub fn Input<OnChangeFn: Fn(String) + 'static>(
53    #[prop(into)]
58    value: MaybeSignal<String>,
59    on_change: OnChangeFn,
61    #[prop(into, default = false.into())]
63    disabled: MaybeSignal<bool>,
64    #[prop(into, default = false.into())]
66    readonly: MaybeSignal<bool>,
67    #[prop(into)]
70    id: String,
71    #[prop(into, default=InputType::Text.into())] input_type: MaybeSignal<InputType>,
72    #[prop(into, default = "".into())]
74    label: MaybeSignal<String>,
75    #[prop(into, default = 0.0.into())] min: MaybeSignal<f32>,
80    #[prop(into, default = 0.0.into())] step: MaybeSignal<f32>,
81    #[prop(into, default = "".to_string().into())]
83    unit: MaybeSignal<String>,
84) -> impl IntoView {
85    let class = move || {
86        let mut class = "peer block min-h-[auto] w-full rounded border-0 px-3 py-[0.32rem] leading-[1.6] outline-none transition-all duration-200 ease-linear focus:placeholder:opacity-100 peer-focus:text-primary data-[te-input-state-active]:placeholder:opacity-100 motion-reduce:transition-none dark:text-neutral-200 dark:placeholder:text-neutral-200 dark:peer-focus:text-primary [&:not([data-te-input-placeholder-active])]:placeholder:opacity-0".to_string();
87        if disabled() || readonly() {
88            class.push_str(" bg-neutral-100 dark:bg-neutral-700");
89        } else {
90            class.push_str(" bg-transparent")
91        }
92        class
93    };
94
95    let element_ref: NodeRef<Div> = create_node_ref();
97    create_effect(move |_| {
98        if let Some(element) = element_ref() {
99            let jsinput = JsInput::new(&element);
100            on_cleanup(move || jsinput.dispose());
101        }
102    });
103
104    let id = if id.is_empty() { None } else { Some(id) };
105
106    let unit = store_value(unit);
107
108    view! {
109        <div ref=element_ref class="relative mb-3">
110            <input
111                type=input_type.map(InputType::html_attrib)
112                class=class
113                placeholder=label.clone()
114                aria-label=label.clone()
115                id=id.clone()
116                disabled=disabled
117                readonly=readonly
118                min=move || if input_type() == InputType::Number { Some(min().to_string()) } else { None }
119                step=move || if input_type() == InputType::Number { Some(step().to_string()) } else { None }
120                prop:value=value
121                on:input=move |ev| {
122                    on_change(event_target_value(&ev));
123                }
124            />
125            <label
126                for=id
127                class="pointer-events-none absolute left-3 top-0 mb-0 max-w-[90%] origin-[0_0] truncate pt-[0.37rem] leading-[1.6] text-neutral-500 transition-all duration-200 ease-out peer-focus:-translate-y-[0.9rem] peer-focus:scale-[0.8] peer-focus:text-primary peer-data-[te-input-state-active]:-translate-y-[0.9rem] peer-data-[te-input-state-active]:scale-[0.8] motion-reduce:transition-none dark:text-neutral-200 dark:peer-focus:text-primary"
128            >
129                {label}
130            </label>
131            <Show when=unit.with_value(|unit| unit.map(String::is_empty).not()) fallback=move || view!{}>
132                <span class="pointer-events-none absolute right-8 top-0 mb-0 max-w-[90%] pt-[0.37rem] leading-[1.6] text-neutral-500 dark:text-neutral-200">{unit}</span>
134            </Show>
135        </div>
136    }
137}
138
139#[wasm_bindgen]
140extern "C" {
141    #[wasm_bindgen(js_namespace = te, js_name = Input)]
142    type JsInput;
143
144    #[wasm_bindgen(constructor, js_namespace = te, js_class = Input, final)]
146    fn new(e: &HtmlDivElement) -> JsInput;
147
148    #[wasm_bindgen(method, js_namespace = te, js_class = Input, final)]
149    fn dispose(this: &JsInput);
150}