material_dioxus/text_inputs/
textfield.rs

1use std::marker::PhantomData;
2
3use super::set_on_input_handler;
4use crate::text_inputs::{validity_state::ValidityStateJS, TextFieldType, ValidityTransform};
5use crate::StaticCallback;
6use dioxus::core::AttributeValue;
7use dioxus::prelude::*;
8use gloo::events::EventListener;
9use wasm_bindgen::prelude::*;
10use wasm_bindgen::JsCast;
11use web_sys::Node;
12use web_sys::ValidityState as NativeValidityState;
13
14#[wasm_bindgen(module = "/build/mwc-textfield.js")]
15extern "C" {
16    #[derive(Debug, Clone)]
17    #[wasm_bindgen(extends = Node)]
18    type TextField;
19
20    #[wasm_bindgen(getter, static_method_of = TextField)]
21    fn _dummy_loader() -> JsValue;
22
23    #[wasm_bindgen(method, setter = validityTransform)]
24    fn set_validity_transform(
25        this: &TextField,
26        val: &Closure<dyn Fn(String, NativeValidityState) -> ValidityStateJS>,
27    );
28
29    #[wasm_bindgen(method, setter)]
30    fn set_type(this: &TextField, val: &JsValue);
31
32    #[wasm_bindgen(method, getter)]
33    fn value(this: &TextField) -> String;
34
35    #[wasm_bindgen(method, setter)]
36    fn set_value(this: &TextField, val: &JsValue);
37}
38
39loader_hack!(TextField);
40
41/// Props for [`MatTextField`]
42///
43/// MWC Documentation:
44///
45/// - [Properties](https://github.com/material-components/material-components-web-components/tree/v0.27.0/packages/textfield#propertiesattributes)
46#[derive(Props)]
47pub struct TextFieldProps<'a> {
48    #[props(default)]
49    pub open: bool,
50    #[props(into)]
51    pub value: Option<String>,
52    #[props(default = TextFieldType::Text)]
53    pub field_type: TextFieldType,
54    #[props(into)]
55    pub label: Option<String>,
56    #[props(into)]
57    pub placeholder: Option<String>,
58    #[props(into)]
59    pub prefix: Option<String>,
60    #[props(into)]
61    pub suffix: Option<String>,
62    #[props(into)]
63    pub icon: Option<String>,
64    #[props(into)]
65    pub icon_trailing: Option<String>,
66    #[props(default)]
67    pub disabled: bool,
68    #[props(default)]
69    pub char_counter: bool,
70    #[props(default)]
71    pub outlined: bool,
72    #[props(into)]
73    pub helper: Option<String>,
74    #[props(default)]
75    pub helper_persistent: bool,
76    #[props(default)]
77    pub required: bool,
78    #[props(default)]
79    pub max_length: Option<u64>,
80    #[props(into)]
81    pub validation_message: Option<String>,
82    #[props(into)]
83    pub pattern: Option<String>,
84    /// Type: `number | string` so I'll leave it as a string
85    #[props(into)]
86    pub min: Option<String>,
87    /// Type: `number | string`  so I'll leave it as a string
88    #[props(into)]
89    pub max: Option<String>,
90    // What you doing...
91    #[props(default)]
92    pub size: Option<i64>,
93    // ...step size
94    #[props(default)]
95    pub step: Option<i64>,
96    #[props(default)]
97    pub auto_validate: bool,
98    pub validity_transform: Option<ValidityTransform>,
99    #[props(default)]
100    pub validate_on_initial_render: bool,
101    #[props(into)]
102    // the name cannot start with `on` or dioxus will expect an `EventHandler` which aren't static
103    // and thus cannot be used here
104    pub _oninput: Option<StaticCallback<String>>,
105    _lifetime: Option<PhantomData<&'a ()>>,
106    #[props(into)]
107    pub name: Option<String>,
108
109    #[props(default)]
110    pub webkit_date_picker: bool,
111    #[props(into)]
112    pub _onchange: Option<StaticCallback<String>>,
113
114    #[props(into, default)]
115    pub style: String,
116    #[props(into, default)]
117    pub class: String,
118    #[props(into)]
119    pub slot: Option<String>,
120    #[props(default)]
121    pub dialog_initial_focus: bool,
122}
123
124fn render<'a>(cx: Scope<'a, TextFieldProps<'a>>) -> Element<'a> {
125    let id = crate::use_id(cx, "textfield");
126    let input_listener = cx.use_hook(|| None);
127    let change_listener = cx.use_hook(|| None);
128    let validity_transform_closure = cx.use_hook(|| None);
129    if let Some(elem) = crate::get_elem_by_id(id) {
130        let target = elem.clone();
131        let textfield = JsValue::from(elem).dyn_into::<TextField>().unwrap();
132        textfield.set_type(&JsValue::from(cx.props.field_type.as_str()));
133        textfield.set_value(&JsValue::from_str(
134            cx.props
135                .value
136                .as_ref()
137                .map(|s| s.as_ref())
138                .unwrap_or_default(),
139        ));
140        if let Some(listener) = cx.props._oninput.clone() {
141            *input_listener = Some(set_on_input_handler(&target, listener, |(_, detail)| {
142                detail
143                    .unchecked_into::<MatTextFieldInputEvent>()
144                    .target()
145                    .value()
146            }));
147        }
148        if let Some(listener) = cx.props._onchange.clone() {
149            to_owned![textfield];
150            *change_listener = Some(EventListener::new(&target, "change", move |_| {
151                listener.call(textfield.value())
152            }));
153        }
154        if let (Some(transform), None) = (
155            cx.props.validity_transform.clone(),
156            &validity_transform_closure,
157        ) {
158            *validity_transform_closure = Some(Closure::wrap(Box::new(
159                move |s: String, v: NativeValidityState| -> ValidityStateJS {
160                    transform.0(s, v).into()
161                },
162            )
163                as Box<dyn Fn(String, NativeValidityState) -> ValidityStateJS>));
164            textfield.set_validity_transform(validity_transform_closure.as_ref().unwrap());
165        }
166    }
167    render! {
168        mwc-textfield {
169            id: id,
170
171            open: bool_attr!(cx.props.open),
172            label: optional_string_attr!(cx.props.label),
173            placeholder: optional_string_attr!(cx.props.placeholder),
174            prefix: optional_string_attr!(cx.props.prefix),
175            suffix: optional_string_attr!(cx.props.suffix),
176            icon: optional_string_attr!(cx.props.icon),
177            iconTrailing: optional_string_attr!(cx.props.icon_trailing),
178            disabled: bool_attr!(cx.props.disabled),
179            charCounter: bool_attr!(cx.props.char_counter),
180            outlined: bool_attr!(cx.props.outlined),
181            helper: optional_string_attr!(cx.props.helper),
182            helperPersistent: bool_attr!(cx.props.helper_persistent),
183            required: bool_attr!(cx.props.required),
184            maxLength: cx.props.max_length.map(|v| format_args!("{v}").into_value(cx.bump())).unwrap_or(AttributeValue::None),
185            validationMessage: optional_string_attr!(cx.props.validation_message),
186            pattern: optional_string_attr!(cx.props.pattern),
187            min: optional_string_attr!(cx.props.min),
188            max: optional_string_attr!(cx.props.max),
189            size: cx.props.size.map(|v| format_args!("{v}").into_value(cx.bump())).unwrap_or(AttributeValue::None),
190            step: cx.props.step.map(|v| format_args!("{v}").into_value(cx.bump())).unwrap_or(AttributeValue::None),
191            autoValidate: bool_attr!(cx.props.auto_validate),
192            validateOnInitialRender: bool_attr!(cx.props.validate_on_initial_render),
193            name: optional_string_attr!(cx.props.name),
194            dialogInitialFocus: bool_attr!(cx.props.dialog_initial_focus),
195            webkitDatePicker: bool_attr!(cx.props.webkit_date_picker),
196
197            style: string_attr!(cx.props.style),
198            class: string_attr!(cx.props.class),
199            slot: optional_string_attr!(cx.props.slot),
200            dialogInitialFocus: bool_attr!(cx.props.dialog_initial_focus),
201        }
202    }
203}
204
205component!('a, MatTextField, TextFieldProps, render, TextField, "textfield");
206
207#[wasm_bindgen]
208extern "C" {
209    type MatTextFieldInputEvent;
210
211    #[wasm_bindgen(method, getter)]
212    fn target(this: &MatTextFieldInputEvent) -> TextField;
213}