yew_limput/
lib.rs

1use std::rc::Rc;
2
3use tracing::error;
4use web_sys::HtmlInputElement;
5use web_sys::wasm_bindgen::JsCast;
6use yew::prelude::*;
7
8pub type LimitedInputFilter = dyn for<'a> Fn(&'a char) -> bool;
9
10#[macro_export]
11macro_rules! input_filter {
12    ($filter:expr) => {
13        Rc::new($filter) as Rc<$crate::LimitedInputFilter>
14    };
15}
16
17#[derive(Properties)]
18struct LimitedInputProps {
19    input_type: &'static str,
20    class: AttrValue,
21    filter: Rc<LimitedInputFilter>,
22    max_len: Option<usize>,
23    on_max_len: Callback<String>,
24}
25
26impl PartialEq for LimitedInputProps {
27    fn eq(&self, other: &Self) -> bool {
28        self.input_type == other.input_type
29            && self.class == other.class
30            && Rc::ptr_eq(&self.filter, &other.filter)
31            && self.max_len == other.max_len
32    }
33}
34
35#[function_component]
36fn LimitedInput(props: &LimitedInputProps) -> Html {
37    let oninput = {
38        let filter = props.filter.clone();
39        let max_len = props.max_len.unwrap_or(usize::MAX);
40        let on_max_len = props.on_max_len.clone();
41
42        move |event: InputEvent| {
43            let Some(target) = event.target() else {
44                error!("input event has no target");
45                return;
46            };
47
48            let Ok(input) = target.dyn_into::<HtmlInputElement>() else {
49                error!("input event target is not an input element");
50                return;
51            };
52
53            let value = input.value();
54
55            let filtered_value: String = value
56                .chars()
57                .filter(|c| (filter)(c))
58                .take(max_len)
59                .collect();
60
61            input.set_value(&filtered_value);
62
63            if filtered_value.len() >= max_len {
64                on_max_len.emit(filtered_value);
65            }
66        }
67    };
68
69    html! {
70        <input
71            type={props.input_type}
72            class={&props.class}
73            {oninput}
74        />
75    }
76}
77
78#[derive(Properties)]
79pub struct LimitedTextInputProps {
80    #[prop_or_default]
81    pub class: AttrValue,
82    pub filter: Rc<LimitedInputFilter>,
83    pub max_len: Option<usize>,
84    #[prop_or_else(|| Callback::noop())]
85    pub on_max_len: Callback<String>,
86}
87
88impl PartialEq for LimitedTextInputProps {
89    fn eq(&self, other: &Self) -> bool {
90        self.class == other.class
91            && Rc::ptr_eq(&self.filter, &other.filter)
92            && self.max_len == other.max_len
93    }
94}
95
96#[function_component]
97pub fn LimitedTextInput(props: &LimitedTextInputProps) -> Html {
98    let input_type = "text";
99    let class = &props.class;
100    let filter = props.filter.clone();
101    let max_len = props.max_len;
102    let on_max_len = props.on_max_len.clone();
103
104    html! {
105        <LimitedInput {input_type} {class} {filter} {max_len} {on_max_len} />
106    }
107}
108
109#[derive(PartialEq, Properties)]
110pub struct LimitedNumericInputProps {
111    #[prop_or_default]
112    pub class: AttrValue,
113    pub max_len: Option<usize>,
114    #[prop_or_else(|| Callback::noop())]
115    pub on_max_len: Callback<String>,
116}
117
118#[function_component]
119pub fn LimitedNumericInput(props: &LimitedNumericInputProps) -> Html {
120    let class = &props.class;
121    let filter = Rc::new(|c: &char| c.is_ascii_digit()) as Rc<LimitedInputFilter>;
122    let max_len = props.max_len;
123    let on_max_len = props.on_max_len.clone();
124
125    html! {
126        <LimitedTextInput {class} {filter} {max_len} {on_max_len} />
127    }
128}