material_yew/text_inputs/
textfield.rs

1use super::set_on_input_handler;
2use crate::text_inputs::{
3    validity_state::ValidityStateJS, TextFieldType, ValidityState, ValidityTransform,
4};
5use crate::{bool_to_option, WeakComponentLink};
6use gloo::events::EventListener;
7use wasm_bindgen::prelude::*;
8use wasm_bindgen::JsCast;
9use web_sys::Node;
10use web_sys::ValidityState as NativeValidityState;
11use yew::prelude::*;
12use yew::virtual_dom::AttrValue;
13
14#[wasm_bindgen(module = "/build/mwc-textfield.js")]
15extern "C" {
16    #[derive(Debug)]
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/// The `mwc-textfield` component
42///
43/// [MWC Documentation](https://github.com/material-components/material-components-web-components/tree/v0.27.0/packages/textfield)
44pub struct MatTextField {
45    node_ref: NodeRef,
46    validity_transform_closure:
47        Option<Closure<dyn Fn(String, NativeValidityState) -> ValidityStateJS>>,
48    input_listener: Option<EventListener>,
49}
50
51/// Props for [`MatTextField`]
52///
53/// MWC Documentation:
54///
55/// - [Properties](https://github.com/material-components/material-components-web-components/tree/v0.27.0/packages/textfield#propertiesattributes)
56#[derive(Properties, PartialEq, Clone)]
57pub struct TextFieldProps {
58    #[prop_or_default]
59    pub open: bool,
60    #[prop_or_default]
61    pub value: Option<AttrValue>,
62    #[prop_or(TextFieldType::Text)]
63    pub field_type: TextFieldType,
64    #[prop_or_default]
65    pub label: Option<AttrValue>,
66    #[prop_or_default]
67    pub placeholder: Option<AttrValue>,
68    #[prop_or_default]
69    pub prefix: Option<AttrValue>,
70    #[prop_or_default]
71    pub suffix: Option<AttrValue>,
72    #[prop_or_default]
73    pub icon: Option<AttrValue>,
74    #[prop_or_default]
75    pub icon_trailing: Option<AttrValue>,
76    #[prop_or_default]
77    pub disabled: bool,
78    #[prop_or_default]
79    pub char_counter: bool,
80    #[prop_or_default]
81    pub outlined: bool,
82    #[prop_or_default]
83    pub helper: Option<AttrValue>,
84    #[prop_or_default]
85    pub helper_persistent: bool,
86    #[prop_or_default]
87    pub required: bool,
88    #[prop_or_default]
89    pub max_length: Option<u64>,
90    #[prop_or_default]
91    pub validation_message: Option<AttrValue>,
92    #[prop_or_default]
93    pub pattern: Option<AttrValue>,
94    /// Type: `number | string` so I'll leave it as a string
95    #[prop_or_default]
96    pub min: Option<AttrValue>,
97    /// Type: `number | string`  so I'll leave it as a string
98    #[prop_or_default]
99    pub max: Option<AttrValue>,
100    // What you doing...
101    #[prop_or_default]
102    pub size: Option<i64>,
103    // ...step size
104    #[prop_or_default]
105    pub step: Option<i64>,
106    #[prop_or_default]
107    pub auto_validate: bool,
108    #[prop_or_default]
109    pub validity_transform: Option<ValidityTransform>,
110    #[prop_or_default]
111    pub validate_on_initial_render: bool,
112    #[prop_or_default]
113    pub oninput: Callback<String>,
114    #[prop_or_default]
115    pub name: Option<AttrValue>,
116    #[prop_or_default]
117    pub component_link: WeakComponentLink<MatTextField>,
118}
119
120impl Component for MatTextField {
121    type Message = ();
122    type Properties = TextFieldProps;
123
124    fn create(ctx: &Context<Self>) -> Self {
125        ctx.props()
126            .component_link
127            .borrow_mut()
128            .replace(ctx.link().clone());
129        TextField::ensure_loaded();
130        Self {
131            node_ref: NodeRef::default(),
132            validity_transform_closure: None,
133            input_listener: None,
134        }
135    }
136
137    fn view(&self, ctx: &Context<Self>) -> Html {
138        let props = ctx.props();
139        html! {
140             <mwc-textfield
141                 open={props.open}
142                 label={props.label.clone()}
143                 placeholder={props.placeholder.clone()}
144                 prefix={props.prefix.clone()}
145                 suffix={props.suffix.clone()}
146                 icon={props.icon.clone()}
147                 iconTrailing={props.icon_trailing.clone()}
148                 disabled={props.disabled}
149                 charCounter={bool_to_option(props.char_counter)}
150                 outlined={bool_to_option(props.outlined)}
151                 helper={props.helper.clone()}
152                 helperPersistent={bool_to_option(props.helper_persistent)}
153                 required={props.required}
154                 maxLength={props.max_length.map(|v| v.to_string())}
155                 validationMessage={props.validation_message.clone()}
156                 pattern={props.pattern.clone()}
157                 min={props.min.clone()}
158                 max={props.max.clone()}
159                 size={props.size.map(|v| v.to_string())}
160                 step={props.step.map(|v| v.to_string())}
161                 autoValidate={bool_to_option(props.auto_validate)}
162                 validateOnInitialRender={bool_to_option(props.validate_on_initial_render)}
163                 name={props.name.clone()}
164                 ref={self.node_ref.clone()}
165             ></mwc-textfield>
166        }
167    }
168
169    fn changed(&mut self, ctx: &Context<Self>, _old_props: &Self::Properties) -> bool {
170        // clear event listeners and update link in case the props changed
171        self.input_listener = None;
172        ctx.props()
173            .component_link
174            .borrow_mut()
175            .replace(ctx.link().clone());
176        true
177    }
178
179    fn rendered(&mut self, ctx: &Context<Self>, first_render: bool) {
180        let props = ctx.props();
181        let element = self.node_ref.cast::<TextField>().unwrap();
182        element.set_type(&JsValue::from(props.field_type.as_str()));
183        element.set_value(&JsValue::from_str(
184            props.value.as_ref().map(|s| s.as_ref()).unwrap_or_default(),
185        ));
186
187        if self.input_listener.is_none() {
188            self.input_listener = Some(set_on_input_handler(
189                &self.node_ref,
190                props.oninput.clone(),
191                |(_, detail)| {
192                    detail
193                        .unchecked_into::<MatTextFieldInputEvent>()
194                        .target()
195                        .value()
196                },
197            ));
198        }
199        if first_render {
200            let this = self.node_ref.cast::<TextField>().unwrap();
201            if let Some(transform) = props.validity_transform.clone() {
202                self.validity_transform_closure = Some(Closure::wrap(Box::new(
203                    move |s: String, v: NativeValidityState| -> ValidityStateJS {
204                        transform.0(s, v).into()
205                    },
206                )
207                    as Box<dyn Fn(String, NativeValidityState) -> ValidityStateJS>));
208                this.set_validity_transform(self.validity_transform_closure.as_ref().unwrap());
209            }
210        }
211    }
212}
213
214impl MatTextField {
215    pub fn validity_transform<F: Fn(String, NativeValidityState) -> ValidityState + 'static>(
216        func: F,
217    ) -> ValidityTransform {
218        ValidityTransform::new(func)
219    }
220}
221
222impl WeakComponentLink<MatTextField> {
223    pub fn value(&self) -> String {
224        self.borrow()
225            .as_ref()
226            .unwrap()
227            .get_component()
228            .unwrap()
229            .node_ref
230            .cast::<TextField>()
231            .unwrap()
232            .value()
233    }
234}
235
236#[wasm_bindgen]
237extern "C" {
238    type MatTextFieldInputEvent;
239
240    #[wasm_bindgen(method, getter)]
241    fn target(this: &MatTextFieldInputEvent) -> TextField;
242}