patternfly_yew/components/form/
input.rs

1use crate::ouia;
2use crate::prelude::{
3    focus, use_on_text_change, Icon, InputState, ValidatingComponent,
4    ValidatingComponentProperties, ValidationContext,
5};
6use crate::utils::{Ouia, OuiaComponentType, OuiaSafe};
7use yew::html::IntoPropValue;
8use yew::prelude::*;
9use yew::virtual_dom::VNode;
10
11const OUIA: Ouia = ouia!("TextInput");
12
13#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
14pub enum TextInputType {
15    Date,
16    DateTimeLocal,
17    Email,
18    Month,
19    Number,
20    Password,
21    Search,
22    #[default]
23    Text,
24    Tel,
25    Time,
26    Url,
27}
28
29impl IntoPropValue<Option<AttrValue>> for TextInputType {
30    fn into_prop_value(self) -> Option<AttrValue> {
31        Some(AttrValue::Static(match self {
32            Self::Date => "date",
33            Self::DateTimeLocal => "datetime-local",
34            Self::Email => "email",
35            Self::Month => "month",
36            Self::Number => "number",
37            Self::Password => "password",
38            Self::Search => "search",
39            Self::Text => "text",
40            Self::Tel => "tel",
41            Self::Time => "time",
42            Self::Url => "url",
43        }))
44    }
45}
46
47/// Properties for [`TextInput`]
48#[derive(Clone, PartialEq, Properties)]
49pub struct TextInputProperties {
50    #[prop_or_default]
51    pub class: Classes,
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: AttrValue,
58    #[prop_or_default]
59    pub size: Option<AttrValue>,
60    #[prop_or_default]
61    pub required: bool,
62    #[prop_or_default]
63    pub disabled: bool,
64    #[prop_or_default]
65    pub readonly: bool,
66    #[prop_or_default]
67    pub state: InputState,
68    #[prop_or_default]
69    pub icon: Option<Icon>,
70    #[prop_or_default]
71    pub r#type: TextInputType,
72    #[prop_or_default]
73    pub placeholder: Option<AttrValue>,
74    #[prop_or_default]
75    pub autofocus: bool,
76    #[prop_or_default]
77    pub form: Option<AttrValue>,
78    #[prop_or_default]
79    pub autocomplete: Option<AttrValue>,
80    #[prop_or_default]
81    pub inputmode: Option<AttrValue>,
82    #[prop_or_default]
83    pub enterkeyhint: Option<AttrValue>,
84    #[prop_or_default]
85    pub aria_describedby: Option<AttrValue>,
86
87    /// This event is triggered when the element's value changes.
88    ///
89    /// **NOTE:** Contrary to the HTML definition of onchange, the callback provides the full value
90    /// of the input element and fires with every keystroke.
91    #[prop_or_default]
92    pub onchange: Callback<String>,
93    /// The element's oninput event.
94    ///
95    /// **NOTE:** In previous versions `oninput` behaved as does `onchange` now.
96    #[prop_or_default]
97    pub oninput: Callback<InputEvent>,
98
99    // Called when validation should occur
100    #[prop_or_default]
101    pub onvalidate: Callback<ValidationContext<String>>,
102
103    #[prop_or_default]
104    pub onkeydown: Callback<KeyboardEvent>,
105
106    #[prop_or_default]
107    pub onblur: Callback<FocusEvent>,
108
109    #[prop_or_default]
110    pub r#ref: NodeRef,
111
112    /// OUIA Component id
113    #[prop_or_default]
114    pub ouia_id: Option<String>,
115    /// OUIA Component Type
116    #[prop_or(OUIA.component_type())]
117    pub ouia_type: OuiaComponentType,
118    /// OUIA Component Safe
119    #[prop_or(OuiaSafe::TRUE)]
120    pub ouia_safe: OuiaSafe,
121}
122
123impl ValidatingComponent for TextInput {
124    type Value = String;
125}
126
127impl ValidatingComponentProperties<String> for TextInputProperties {
128    fn set_onvalidate(&mut self, onvalidate: Callback<ValidationContext<String>>) {
129        self.onvalidate = onvalidate;
130    }
131
132    fn set_input_state(&mut self, state: InputState) {
133        self.state = state;
134    }
135}
136
137/// Text input component
138///
139/// > A **text input** is used to gather free-form text from a user.
140///
141/// See: <https://www.patternfly.org/components/text-input>
142///
143/// ## Properties
144///
145/// Defined by [`TextInputProperties].
146///
147/// ## Change events
148///
149/// The component emits changes of the input value through the `onchange` event whenever the
150/// value changes It also emits the full input value via the `onvalidate` event. This duplication
151/// is required to support both change events as well as supporting the [`ValidatingComponent`]
152/// trait.
153///
154/// If a value is provided via the `value` property, that value must be updated through the
155/// `onchange` callback. Otherwise the value will be reset immediately and the component will
156/// be effectively read-only:
157///
158/// ```rust
159/// use yew::prelude::*;
160/// use patternfly_yew::prelude::*;
161///
162/// #[function_component(Example)]
163/// fn example() -> Html {
164///   let value = use_state_eq(String::default);
165///   let onchange = use_callback(value.clone(), |new_value, value| value.set(new_value));
166///
167///   html!(<TextInput {onchange} value={(*value).clone()} />)
168/// }
169/// ```
170#[function_component(TextInput)]
171pub fn text_input(props: &TextInputProperties) -> Html {
172    let ouia_id = use_memo(props.ouia_id.clone(), |id| {
173        id.clone().unwrap_or(OUIA.generated_id())
174    });
175    let input_ref = props.r#ref.clone();
176    let mut classes = classes!("pf-v5-c-form-control", props.class.clone());
177
178    if props.disabled {
179        classes.push("pf-m-disabled")
180    }
181
182    if props.readonly {
183        classes.push("pf-m-readonly")
184    }
185
186    if props.icon.is_some() {
187        classes.push("pf-m-icon");
188    }
189
190    // validation
191    {
192        let value = props.value.to_string();
193        let onvalidate = props.onvalidate.clone();
194        use_effect_with((), move |()| {
195            onvalidate.emit(ValidationContext {
196                value,
197                initial: true,
198            });
199        });
200    }
201
202    let (classes, aria_invalid) = props.state.convert(classes);
203
204    // autofocus
205
206    {
207        let input_ref = input_ref.clone();
208        use_effect_with(props.autofocus, move |autofocus| {
209            if *autofocus {
210                focus(&input_ref)
211            }
212        });
213    }
214
215    // change events
216    let onchange = use_callback(
217        (props.onchange.clone(), props.onvalidate.clone()),
218        |new_value: String, (onchange, onvalidate)| {
219            onchange.emit(new_value.clone());
220            onvalidate.emit(new_value.into());
221        },
222    );
223    let oninput = use_on_text_change(input_ref.clone(), props.oninput.clone(), onchange);
224
225    let icon_html = props.icon.map(|icon| {
226        html!(
227           <div class="pf-v5-c-form-control__icon">
228                { icon }
229            </div>
230        )
231    });
232
233    let status_html = if props.state != InputState::Default {
234        Some(html!(
235            <div class="pf-v5-c-form-control__icon pf-m-status">
236                {props.state.icon()}
237            </div>
238        ))
239    } else {
240        None
241    };
242
243    html! (
244        <div class={classes}>
245            <input
246                ref={input_ref}
247                type={props.r#type}
248                name={&props.name}
249                id={&props.id}
250                size={&props.size}
251                required={props.required}
252                disabled={props.disabled}
253                readonly={props.readonly}
254                aria-describedby={&props.aria_describedby}
255                aria-invalid={aria_invalid.to_string()}
256                value={props.value.clone()}
257                placeholder={&props.placeholder}
258                form={&props.form}
259                autocomplete={&props.autocomplete}
260                {oninput}
261                onkeydown={&props.onkeydown}
262                onblur={&props.onblur}
263                inputmode={&props.inputmode}
264                enterkeyhint={&props.enterkeyhint}
265                data-ouia-component-id={(*ouia_id).clone()}
266                data-ouia-component-type={props.ouia_type}
267                data-ouia-safe={props.ouia_safe}
268            />
269
270                { None::<VNode> }
271
272            if icon_html.is_some() || status_html.is_some() {
273                <div class="pf-v5-c-form-control__utilities"> // TODO: Refactor out to component
274                    { icon_html }
275                    { status_html }
276                </div>
277            }
278        </div>
279    )
280}