material_dioxus/text_inputs/
textarea.rs1use 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#[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#[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 #[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 #[props(into)]
114 pub min: Option<String>,
115 #[props(into)]
117 pub max: Option<String>,
118 #[props(default)]
119 pub size: Option<i64>, #[props(default)] pub step: Option<i64>, #[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 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}