patternfly_yew/components/form/
area.rs

1use crate::prelude::{
2    focus, use_on_text_change, AsClasses, ExtendClasses, InputState, ValidatingComponent,
3    ValidatingComponentProperties, ValidationContext,
4};
5
6use std::fmt::{Display, Formatter};
7use yew::prelude::*;
8
9//
10// Text area
11//
12
13#[derive(Clone, Default, PartialEq, Eq)]
14pub enum ResizeOrientation {
15    Horizontal,
16    Vertical,
17    #[default]
18    Both,
19}
20
21impl AsClasses for ResizeOrientation {
22    fn extend_classes(&self, classes: &mut Classes) {
23        match self {
24            ResizeOrientation::Horizontal => classes.push("pf-m-resize-horizontal"),
25            ResizeOrientation::Vertical => classes.push("pf-m-resize-vertical"),
26            ResizeOrientation::Both => classes.push("pf-m-resize-both"),
27        }
28    }
29}
30
31#[derive(Clone, Default, PartialEq, Eq)]
32pub enum Wrap {
33    Hard,
34    #[default]
35    Soft,
36    Off,
37}
38
39impl Display for Wrap {
40    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
41        match self {
42            Self::Off => f.write_str("off"),
43            Self::Soft => f.write_str("soft"),
44            Self::Hard => f.write_str("hard"),
45        }
46    }
47}
48
49/// Properties for [`TextArea`]
50#[derive(Clone, PartialEq, Properties)]
51pub struct TextAreaProperties {
52    #[prop_or_default]
53    pub name: Option<AttrValue>,
54    #[prop_or_default]
55    pub id: Option<AttrValue>,
56    #[prop_or_default]
57    pub value: String,
58    #[prop_or_default]
59    pub required: bool,
60    #[prop_or_default]
61    pub disabled: bool,
62    #[prop_or_default]
63    pub readonly: bool,
64    #[prop_or_default]
65    pub state: InputState,
66    #[prop_or_default]
67    pub placeholder: Option<AttrValue>,
68    #[prop_or_default]
69    pub autofocus: bool,
70    #[prop_or_default]
71    pub form: Option<AttrValue>,
72    #[prop_or_default]
73    pub autocomplete: Option<AttrValue>,
74
75    #[prop_or_default]
76    pub spellcheck: Option<AttrValue>,
77    #[prop_or_default]
78    pub wrap: Wrap,
79
80    #[prop_or_default]
81    pub rows: Option<usize>,
82    #[prop_or_default]
83    pub cols: Option<usize>,
84
85    #[prop_or_default]
86    pub resize: ResizeOrientation,
87
88    /// This event is triggered when the element's value changes.
89    ///
90    /// **NOTE:** Contrary to the HTML definition of onchange, the callback provides the full value
91    /// of the input element and fires with every keystroke.
92    #[prop_or_default]
93    pub onchange: Callback<String>,
94    /// The element's oninput event.
95    ///
96    /// **NOTE:** In previous versions `oninput` behaved as does `onchange` now.
97    #[prop_or_default]
98    pub oninput: Callback<InputEvent>,
99    // Called when validation should occur
100    #[prop_or_default]
101    pub onvalidate: Callback<ValidationContext<String>>,
102
103    #[prop_or_default]
104    pub r#ref: NodeRef,
105}
106
107impl ValidatingComponent for TextArea {
108    type Value = String;
109}
110
111impl ValidatingComponentProperties<String> for TextAreaProperties {
112    fn set_onvalidate(&mut self, onvalidate: Callback<ValidationContext<String>>) {
113        self.onvalidate = onvalidate;
114    }
115
116    fn set_input_state(&mut self, state: InputState) {
117        self.state = state;
118    }
119}
120
121/// Text area component
122///
123/// > A **text area** component is used for entering a paragraph of text that is longer than one line.
124///
125/// See: <https://www.patternfly.org/components/text-area>
126///
127/// ## Properties
128///
129/// Defined by [`TextAreaProperties].
130///
131/// ## Change events
132///
133/// The component emits changes of the input value through the `onchange` event once the
134/// component looses the focus (same of plain HTML). It also emits the full input value via the
135/// `oninput` event and does the same using the `onvalidate` event. This duplication is required
136/// to support both change events as well as supporting the [`ValidatingComponent`] trait.
137///
138/// If a value is provided via the `value` property, that value must be updated through the
139/// `oninput` callback. Otherwise the value will be reset immediately and the component will
140/// be effectively read-only:
141///
142/// ```rust
143/// use yew::prelude::*;
144/// use patternfly_yew::prelude::*;
145///
146/// #[function_component(Example)]
147/// fn example() -> Html {
148///   let value = use_state_eq(String::default);
149///   let onchange = {
150///     let value = value.clone();
151///     Callback::from(move |data| value.set(data))
152///   };
153///
154///   html!(<TextArea value={(*value).clone()}/>)
155/// }
156/// ```
157#[function_component(TextArea)]
158pub fn text_area(props: &TextAreaProperties) -> Html {
159    let input_ref = props.r#ref.clone();
160    let mut classes = classes!("pf-v5-c-form-control");
161
162    classes.extend_from(&props.resize);
163
164    if props.readonly {
165        classes.push("pf-m-readonly");
166    }
167
168    // validation
169
170    {
171        let value = props.value.clone();
172        let onvalidate = props.onvalidate.clone();
173        use_effect_with((), move |()| {
174            onvalidate.emit(ValidationContext {
175                value,
176                initial: true,
177            });
178        });
179    }
180
181    let (classes, aria_invalid) = props.state.convert(classes);
182
183    // autofocus
184
185    {
186        let input_ref = input_ref.clone();
187        use_effect_with(props.autofocus, move |autofocus| {
188            if *autofocus {
189                focus(&input_ref)
190            }
191        });
192    }
193
194    // change events
195
196    let onchange = use_callback(
197        (props.onchange.clone(), props.onvalidate.clone()),
198        |new_value: String, (onchange, onvalidate)| {
199            onchange.emit(new_value.clone());
200            onvalidate.emit(new_value.into());
201        },
202    );
203    let oninput = use_on_text_change(input_ref.clone(), props.oninput.clone(), onchange);
204
205    html!(
206        <div class={classes}>
207            <textarea
208                ref={input_ref}
209                name={&props.name}
210                id={&props.id}
211                required={props.required}
212                disabled={props.disabled}
213                readonly={props.readonly}
214                aria-invalid={aria_invalid.to_string()}
215                value={props.value.clone()}
216                placeholder={&props.placeholder}
217                form={&props.form}
218                autocomplete={&props.autocomplete}
219
220                cols={props.cols.as_ref().map(|v|v.to_string())}
221                rows={props.rows.as_ref().map(|v|v.to_string())}
222
223                wrap={props.wrap.to_string()}
224                spellcheck={&props.spellcheck}
225
226                {oninput}
227            />
228            if props.state != InputState::Default {
229                <div class="pf-v5-c-form-control__utilities">
230                    <div class="pf-v5-c-form-control__icon pf-m-status">
231                        {props.state.icon()}
232                    </div>
233                </div>
234            }
235        </div>
236    )
237}