material_yew/text_inputs/
textarea.rs

1use super::set_on_input_handler;
2use crate::bool_to_option;
3use crate::text_inputs::validity_state::ValidityStateJS;
4use crate::text_inputs::{TextFieldType, ValidityState, ValidityTransform};
5use gloo::events::EventListener;
6use wasm_bindgen::prelude::*;
7use wasm_bindgen::JsCast;
8use web_sys::Node;
9pub use web_sys::ValidityState as NativeValidityState;
10use yew::prelude::*;
11use yew::virtual_dom::AttrValue;
12
13#[wasm_bindgen(module = "/build/mwc-textarea.js")]
14extern "C" {
15    #[derive(Debug)]
16    #[wasm_bindgen(extends = Node)]
17    type TextArea;
18
19    #[wasm_bindgen(getter, static_method_of = TextArea)]
20    fn _dummy_loader() -> JsValue;
21
22    #[wasm_bindgen(method, setter = validityTransform)]
23    fn set_validity_transform(
24        this: &TextArea,
25        val: &Closure<dyn Fn(String, NativeValidityState) -> ValidityStateJS>,
26    );
27
28    #[wasm_bindgen(method, setter)]
29    fn set_type(this: &TextArea, val: &JsValue);
30
31    #[wasm_bindgen(method, getter)]
32    fn value(this: &TextArea) -> String;
33
34    #[wasm_bindgen(method, setter)]
35    fn set_value(this: &TextArea, val: &JsValue);
36}
37
38loader_hack!(TextArea);
39
40/// The `mwc-textarea` component
41///
42/// [MWC Documentation](https://github.com/material-components/material-components-web-components/tree/v0.27.0/packages/textarea)
43pub struct MatTextArea {
44    node_ref: NodeRef,
45    validity_transform_closure:
46        Option<Closure<dyn Fn(String, NativeValidityState) -> ValidityStateJS>>,
47    input_listener: Option<EventListener>,
48}
49
50/// Type for [`TextAreaProps::char_counter`].
51///
52/// Equivalent to `type TextAreaCharCounter = 'external'|'internal';` Typescript
53/// type.
54#[derive(Clone, Copy, PartialEq)]
55pub enum TextAreaCharCounter {
56    Internal,
57    External,
58}
59
60impl TextAreaCharCounter {
61    pub fn as_str(&self) -> &'static str {
62        match self {
63            TextAreaCharCounter::Internal => "internal",
64            TextAreaCharCounter::External => "external",
65        }
66    }
67}
68
69/// Props for [`MatTextArea`]
70///
71/// MWC Documentation:
72///
73/// - [Properties](https://github.com/material-components/material-components-web-components/tree/v0.27.0/packages/checkbox#propertiesattributes)
74#[derive(Properties, PartialEq, Clone)]
75pub struct TextAreaProps {
76    #[prop_or_default]
77    pub rows: Option<i64>,
78    #[prop_or_default]
79    pub cols: Option<i64>,
80    #[prop_or_default]
81    pub value: Option<AttrValue>,
82    #[prop_or(TextFieldType::Text)]
83    pub field_type: TextFieldType,
84    #[prop_or_default]
85    pub label: Option<AttrValue>,
86    #[prop_or_default]
87    pub placeholder: Option<AttrValue>,
88    #[prop_or_default]
89    pub icon: Option<AttrValue>,
90    #[prop_or_default]
91    pub icon_trailing: Option<AttrValue>,
92    #[prop_or_default]
93    pub disabled: bool,
94    /// For boolean value `true`, `TextAreaCharCounter::External` is to be used.
95    /// Boolean value `false` results in character counter not being shown so
96    /// `None` should be used
97    #[prop_or_default]
98    pub char_counter: Option<TextAreaCharCounter>,
99    #[prop_or_default]
100    pub outlined: bool,
101    #[prop_or_default]
102    pub helper: Option<AttrValue>,
103    #[prop_or_default]
104    pub helper_persistent: bool,
105    #[prop_or_default]
106    pub required: bool,
107    #[prop_or_default]
108    pub max_length: Option<u64>,
109    #[prop_or_default]
110    pub validation_message: Option<AttrValue>,
111    /// Type: `number | string` so I'll leave it as a string
112    #[prop_or_default]
113    pub min: Option<AttrValue>,
114    /// Type: `number | string`  so I'll leave it as a string
115    #[prop_or_default]
116    pub max: Option<AttrValue>,
117    #[prop_or_default]
118    pub size: Option<i64>, // --|
119    #[prop_or_default] //   | -- What you doing step size
120    pub step: Option<i64>, // --|
121    #[prop_or_default]
122    pub auto_validate: bool,
123    #[prop_or_default]
124    pub validity_transform: Option<ValidityTransform>,
125    #[prop_or_default]
126    pub validate_on_initial_render: bool,
127    #[prop_or_default]
128    pub oninput: Callback<String>,
129    #[prop_or_default]
130    pub name: Option<AttrValue>,
131}
132
133impl Component for MatTextArea {
134    type Message = ();
135    type Properties = TextAreaProps;
136
137    fn create(_: &Context<Self>) -> Self {
138        TextArea::ensure_loaded();
139        Self {
140            node_ref: NodeRef::default(),
141            validity_transform_closure: None,
142            input_listener: None,
143        }
144    }
145
146    fn view(&self, ctx: &Context<Self>) -> Html {
147        let props = ctx.props();
148        html! {
149             <mwc-textarea
150                 rows={props.rows.map(|v| v.to_string())}
151                 cols={props.cols.map(|v| v.to_string())}
152                 label={props.label.clone()}
153                 placeholder={props.placeholder.clone()}
154                 icon={props.icon.clone()}
155                 iconTrailing={props.icon_trailing.clone()}
156                 disabled={props.disabled}
157                 charCounter={props.char_counter.map(|it| it.as_str())}
158                 outlined={bool_to_option(props.outlined)}
159                 helper={props.helper.clone()}
160                 helperPersistent={bool_to_option(props.helper_persistent)}
161                 required={props.required}
162                 maxLength={props.max_length.map(|v| v.to_string())}
163                 validationMessage={props.validation_message.clone()}
164                 min={props.min.clone()}
165                 max={props.max.clone()}
166                 size={props.size.map(|v| v.to_string())}
167                 step={props.step.map(|v| v.to_string())}
168                 autoValidate={bool_to_option(props.auto_validate)}
169                 validateOnInitialRender={bool_to_option(props.validate_on_initial_render)}
170                 name={props.name.clone()}
171                 ref={self.node_ref.clone()}
172             ></mwc-textarea>
173        }
174    }
175
176    fn rendered(&mut self, ctx: &Context<Self>, first_render: bool) {
177        let props = ctx.props();
178        let element = self.node_ref.cast::<TextArea>().unwrap();
179        element.set_type(&JsValue::from(props.field_type.as_str()));
180        element.set_value(&JsValue::from_str(
181            props.value.as_ref().map(|s| s.as_ref()).unwrap_or_default(),
182        ));
183
184        if self.input_listener.is_none() {
185            self.input_listener = Some(set_on_input_handler(
186                &self.node_ref,
187                props.oninput.clone(),
188                |(_, detail)| {
189                    detail
190                        .unchecked_into::<MatTextAreaInputEvent>()
191                        .target()
192                        .value()
193                },
194            ));
195        };
196
197        if first_render {
198            let this = self.node_ref.cast::<TextArea>().unwrap();
199            if let Some(transform) = props.validity_transform.clone() {
200                self.validity_transform_closure = Some(Closure::wrap(Box::new(
201                    move |s: String, v: NativeValidityState| -> ValidityStateJS {
202                        transform.0(s, v).into()
203                    },
204                )
205                    as Box<dyn Fn(String, NativeValidityState) -> ValidityStateJS>));
206                this.set_validity_transform(self.validity_transform_closure.as_ref().unwrap());
207            }
208        }
209    }
210}
211
212impl MatTextArea {
213    pub fn validity_transform<F: Fn(String, NativeValidityState) -> ValidityState + 'static>(
214        func: F,
215    ) -> ValidityTransform {
216        ValidityTransform::new(func)
217    }
218}
219
220#[wasm_bindgen]
221extern "C" {
222    type MatTextAreaInputEvent;
223
224    #[wasm_bindgen(method, getter)]
225    fn target(this: &MatTextAreaInputEvent) -> TextArea;
226}