material_dioxus/text_inputs/
textarea.rs

1use std::fmt;
2use std::marker::PhantomData;
3
4use crate::text_inputs::set_on_input_handler;
5use crate::text_inputs::validity_state::ValidityStateJS;
6use crate::StaticCallback;
7use dioxus::core::AttributeValue;
8use dioxus::prelude::*;
9use wasm_bindgen::prelude::*;
10use wasm_bindgen::JsCast;
11use web_sys::Node;
12use web_sys::ValidityState as NativeValidityState;
13
14use super::TextFieldType;
15use super::ValidityTransform;
16
17#[wasm_bindgen(module = "/build/mwc-textarea.js")]
18extern "C" {
19    #[derive(Debug)]
20    #[wasm_bindgen(extends = Node)]
21    type TextArea;
22
23    #[wasm_bindgen(getter, static_method_of = TextArea)]
24    fn _dummy_loader() -> JsValue;
25
26    #[wasm_bindgen(method, setter = validityTransform)]
27    fn set_validity_transform(
28        this: &TextArea,
29        val: &Closure<dyn Fn(String, NativeValidityState) -> ValidityStateJS>,
30    );
31
32    #[wasm_bindgen(method, setter)]
33    fn set_type(this: &TextArea, val: &JsValue);
34
35    #[wasm_bindgen(method, getter)]
36    fn value(this: &TextArea) -> String;
37
38    #[wasm_bindgen(method, setter)]
39    fn set_value(this: &TextArea, val: &JsValue);
40}
41
42loader_hack!(TextArea);
43
44/// Type for [`TextAreaProps::char_counter`].
45///
46/// Equivalent to `type TextAreaCharCounter = 'external'|'internal';` Typescript
47/// type.
48#[derive(Clone, Copy, PartialEq)]
49pub enum TextAreaCharCounter {
50    Internal,
51    External,
52}
53
54impl TextAreaCharCounter {
55    pub fn as_str(&self) -> &'static str {
56        match self {
57            TextAreaCharCounter::Internal => "internal",
58            TextAreaCharCounter::External => "external",
59        }
60    }
61}
62
63impl fmt::Display for TextAreaCharCounter {
64    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
65        write!(f, "{}", self.as_str())
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// TODO: why do the props icon, iconTrailing, field_type and so on exist?
75#[derive(Props)]
76pub struct TextAreaProps<'a> {
77    #[props(default)]
78    pub rows: Option<i64>,
79    #[props(default)]
80    pub cols: Option<i64>,
81    #[props(into)]
82    pub value: Option<String>,
83    #[props(default = TextFieldType::Text)]
84    pub field_type: TextFieldType,
85    #[props(into)]
86    pub label: Option<String>,
87    #[props(into)]
88    pub placeholder: Option<String>,
89    #[props(into)]
90    pub icon: Option<String>,
91    #[props(into)]
92    pub icon_trailing: Option<String>,
93    #[props(default)]
94    pub disabled: bool,
95    /// For boolean value `true`, `TextAreaCharCounter::External` is to be used.
96    /// Boolean value `false` results in character counter not being shown so
97    /// `None` should be used
98    #[props(default)]
99    pub char_counter: Option<TextAreaCharCounter>,
100    #[props(default)]
101    pub outlined: bool,
102    #[props(into)]
103    pub helper: Option<String>,
104    #[props(default)]
105    pub helper_persistent: bool,
106    #[props(default)]
107    pub required: bool,
108    #[props(default)]
109    pub max_length: Option<u64>,
110    #[props(into)]
111    pub validation_message: Option<String>,
112    /// Type: `number | string` so I'll leave it as a string
113    #[props(into)]
114    pub min: Option<String>,
115    /// Type: `number | string`  so I'll leave it as a string
116    #[props(into)]
117    pub max: Option<String>,
118    #[props(default)]
119    pub size: Option<i64>, // --|
120    #[props(default)] //   | -- What you doing step size
121    pub step: Option<i64>, // --|
122    #[props(default)]
123    pub auto_validate: bool,
124    #[props(default)]
125    pub validity_transform: Option<ValidityTransform>,
126    #[props(default)]
127    pub validate_on_initial_render: bool,
128    #[props(into)]
129    // the name cannot start with `on` or dioxus will expect an `EventHandler` which aren't static
130    // and thus cannot be used here
131    pub _oninput: Option<StaticCallback<String>>,
132    _lifetime: Option<PhantomData<&'a ()>>,
133    #[props(default)]
134    pub name: Option<String>,
135
136    #[props(into, default)]
137    pub style: String,
138    #[props(into, default)]
139    pub class: String,
140    #[props(into)]
141    pub slot: Option<String>,
142    #[props(default)]
143    pub dialog_initial_focus: bool,
144}
145
146fn render<'a>(cx: Scope<'a, TextAreaProps<'a>>) -> Element<'a> {
147    let id = crate::use_id(cx, "textarea");
148    let input_listener = cx.use_hook(|| None);
149    let validity_transform_closure = cx.use_hook(|| None);
150    if let Some(elem) = crate::get_elem_by_id(id) {
151        let target = elem.clone();
152        let textarea = JsValue::from(elem).dyn_into::<TextArea>().unwrap();
153        textarea.set_type(&JsValue::from(cx.props.field_type.as_str()));
154        textarea.set_value(&JsValue::from_str(
155            cx.props
156                .value
157                .as_ref()
158                .map(|s| s.as_ref())
159                .unwrap_or_default(),
160        ));
161        if let Some(listener) = cx.props._oninput.clone() {
162            *input_listener = Some(set_on_input_handler(&target, listener, |(_, detail)| {
163                detail
164                    .unchecked_into::<MatTextAreaInputEvent>()
165                    .target()
166                    .value()
167            }));
168        }
169        if let (Some(transform), None) = (
170            cx.props.validity_transform.clone(),
171            &validity_transform_closure,
172        ) {
173            *validity_transform_closure = Some(Closure::wrap(Box::new(
174                move |s: String, v: NativeValidityState| -> ValidityStateJS {
175                    transform.0(s, v).into()
176                },
177            )
178                as Box<dyn Fn(String, NativeValidityState) -> ValidityStateJS>));
179            textarea.set_validity_transform(validity_transform_closure.as_ref().unwrap());
180        }
181    }
182
183    render! {
184        mwc-textarea {
185            id: id,
186
187            rows: cx.props.rows.map(|v| format_args!("{v}").into_value(cx.bump())).unwrap_or(AttributeValue::None),
188            cols: cx.props.cols.map(|v| format_args!("{v}").into_value(cx.bump())).unwrap_or(AttributeValue::None),
189            label: optional_string_attr!(cx.props.label),
190            placeholder: optional_string_attr!(cx.props.placeholder),
191            icon: optional_string_attr!(cx.props.icon),
192            iconTrailing: optional_string_attr!(cx.props.icon_trailing),
193            disabled: bool_attr!(cx.props.disabled),
194            charCounter: cx.props.char_counter.map(|v| format_args!("{v}").into_value(cx.bump())).unwrap_or(AttributeValue::None),
195            outlined: bool_attr!(cx.props.outlined),
196            helper: optional_string_attr!(cx.props.helper),
197            helperPersistent: bool_attr!(cx.props.helper_persistent),
198            required: bool_attr!(cx.props.required),
199            maxLength: cx.props.max_length.map(|v| format_args!("{v}").into_value(cx.bump())).unwrap_or(AttributeValue::None),
200            validationMessage: optional_string_attr!(cx.props.validation_message),
201            min: optional_string_attr!(cx.props.min),
202            max: optional_string_attr!(cx.props.max),
203            size: cx.props.size.map(|v| format_args!("{v}").into_value(cx.bump())).unwrap_or(AttributeValue::None),
204            step: cx.props.step.map(|v| format_args!("{v}").into_value(cx.bump())).unwrap_or(AttributeValue::None),
205            autoValidate: bool_attr!(cx.props.auto_validate),
206            validateOnInitialRender: bool_attr!(cx.props.validate_on_initial_render),
207            name: optional_string_attr!(cx.props.name),
208
209            style: string_attr!(cx.props.style),
210            class: string_attr!(cx.props.class),
211            slot: optional_string_attr!(cx.props.slot),
212            dialogInitialFocus: bool_attr!(cx.props.dialog_initial_focus),
213        }
214    }
215}
216
217component!('a, MatTextArea, TextAreaProps, render, TextArea, "textarea");
218
219#[wasm_bindgen]
220extern "C" {
221    type MatTextAreaInputEvent;
222
223    #[wasm_bindgen(method, getter)]
224    fn target(this: &MatTextAreaInputEvent) -> TextArea;
225}