Skip to main content

patternfly_yew/components/form/
input.rs

1use crate::ouia;
2use crate::prelude::{
3    Icon, InputState, ValidatingComponent, ValidatingComponentProperties, ValidationContext, focus,
4    use_on_text_change,
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-v6-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
226        .icon
227        .map(|icon| html!(<div class="pf-v6-c-form-control__icon">{ icon }</div>));
228
229    let status_html = if props.state != InputState::Default {
230        Some(
231            html!(<div class="pf-v6-c-form-control__icon pf-m-status">{ props.state.icon() }</div>),
232        )
233    } else {
234        None
235    };
236
237    html! (
238        <div class={classes}>
239            <input
240                ref={input_ref}
241                type={props.r#type}
242                name={&props.name}
243                id={&props.id}
244                size={&props.size}
245                required={props.required}
246                disabled={props.disabled}
247                readonly={props.readonly}
248                aria-describedby={&props.aria_describedby}
249                aria-invalid={aria_invalid.to_string()}
250                value={props.value.clone()}
251                placeholder={&props.placeholder}
252                form={&props.form}
253                autocomplete={&props.autocomplete}
254                {oninput}
255                onkeydown={&props.onkeydown}
256                onblur={&props.onblur}
257                inputmode={&props.inputmode}
258                enterkeyhint={&props.enterkeyhint}
259                data-ouia-component-id={(*ouia_id).clone()}
260                data-ouia-component-type={props.ouia_type}
261                data-ouia-safe={props.ouia_safe}
262            />
263            { None::<VNode> }
264            if icon_html.is_some() || status_html.is_some() {
265                <div class="pf-v6-c-form-control__utilities">
266                    // TODO: Refactor out to component
267                    { icon_html }
268                    { status_html }
269                </div>
270            }
271        </div>
272    )
273}