Skip to main content

patternfly_yew/components/form/
group.rs

1use crate::prelude::*;
2use std::{marker::PhantomData, rc::Rc};
3use uuid::Uuid;
4use yew::{
5    prelude::*,
6    virtual_dom::{VChild, VNode},
7};
8
9// form group
10
11/// Properties for [`FormGroup`]
12#[derive(Clone, PartialEq, Properties)]
13pub struct FormGroupProperties {
14    pub children: Html,
15    #[prop_or_default]
16    pub label: String,
17    #[prop_or_default]
18    pub required: bool,
19    #[prop_or_default]
20    pub label_icon: LabelIcon,
21    #[prop_or_default]
22    pub helper_text: Option<FormHelperText>,
23}
24
25#[derive(Clone, Default, PartialEq)]
26pub enum LabelIcon {
27    /// No label icon
28    #[default]
29    None,
30    /// Help
31    Help(VChild<PopoverBody>),
32    /// Any children
33    Children(Html),
34}
35
36/// Helper text information for a [`FormGroup`]
37#[derive(Clone, Debug, PartialEq, Eq)]
38pub struct FormHelperText {
39    pub input_state: InputState,
40    pub custom_icon: Option<Icon>,
41    pub no_icon: bool,
42    pub is_dynamic: bool,
43    pub message: String,
44}
45
46impl From<&FormHelperText> for VNode {
47    fn from(text: &FormHelperText) -> Self {
48        let mut classes = Classes::from("pf-v6-c-helper-text__item");
49
50        classes.extend(text.input_state.as_classes());
51
52        if text.is_dynamic {
53            classes.push("pf-m-dynamic");
54        }
55
56        html!(
57            <div class={classes}>
58                if !text.no_icon {
59                    <span class="pf-v6-c-helper-text__item-icon">
60                        { text.custom_icon.unwrap_or_else(|| text.input_state.icon() ) }
61                    </span>
62                }
63                <span
64                    class="pf-v6-c-helper-text__item-text"
65                >
66                    { &text.message }
67                </span>
68            </div>
69        )
70    }
71}
72
73impl From<&str> for FormHelperText {
74    fn from(text: &str) -> Self {
75        FormHelperText {
76            input_state: Default::default(),
77            custom_icon: None,
78            no_icon: true,
79            is_dynamic: false,
80            message: text.into(),
81        }
82    }
83}
84
85impl From<(String, InputState)> for FormHelperText {
86    fn from((message, input_state): (String, InputState)) -> Self {
87        Self {
88            input_state,
89            custom_icon: None,
90            no_icon: false,
91            is_dynamic: false,
92            message,
93        }
94    }
95}
96
97impl From<(&str, InputState)> for FormHelperText {
98    fn from((message, input_state): (&str, InputState)) -> Self {
99        Self {
100            input_state,
101            custom_icon: None,
102            no_icon: false,
103            is_dynamic: false,
104            message: message.to_string(),
105        }
106    }
107}
108
109/// A group of components building a field in a [`Form`](crate::prelude::Form)
110///
111/// ## Properties
112///
113/// Defined by [`FormGroupProperties`].
114pub struct FormGroup {}
115
116impl Component for FormGroup {
117    type Message = ();
118    type Properties = FormGroupProperties;
119
120    fn create(_: &Context<Self>) -> Self {
121        Self {}
122    }
123
124    fn view(&self, ctx: &Context<Self>) -> Html {
125        let classes = Classes::from("pf-v6-c-form__group");
126
127        html! (
128            <div class={classes}>
129                if !ctx.props().label.is_empty() {
130                    <div class="pf-v6-c-form__group-label">
131                        <label class="pf-v6-c-form__label">
132                            <span class="pf-v6-c-form__label-text">{ &ctx.props().label }</span>
133                            if ctx.props().required {
134                                { " " }
135                                <span class="pf-v6-c-form__label-required" aria-hidden="true">
136                                    { "*" }
137                                </span>
138                            }
139                        </label>
140                        { match &ctx.props().label_icon  {
141                                LabelIcon::None => html!(),
142                                LabelIcon::Help(popover) => html!(
143                                    <span
144                                        class="pf-v6-c-form__group-label-help"
145                                        role="button"
146                                        type="button"
147                                        tabindex=0
148                                    >
149                                        {" "}
150                                        <Popover target={html!(Icon::QuestionCircle)} body={popover.clone()} />
151                                    </span>
152                                ),
153                                LabelIcon::Children(children) => children.clone(),
154                            } }
155                    </div>
156                }
157                <div
158                    class="pf-v6-c-form__group-control"
159                >
160                    { ctx.props().children.clone() }
161                    if let Some(text) = &ctx.props().helper_text {
162                        { FormGroupHelpText(text) }
163                    }
164                </div>
165            </div>
166        )
167    }
168}
169
170pub struct FormGroupHelpText<'a>(pub &'a FormHelperText);
171
172impl<'a> FormGroupHelpText<'a> {}
173
174impl<'a> From<FormGroupHelpText<'a>> for VNode {
175    fn from(text: FormGroupHelpText<'a>) -> Self {
176        let mut classes = classes!("pf-v6-c-helper-text__item");
177
178        classes.extend(text.0.input_state.as_classes());
179
180        let icon = match text.0.no_icon {
181            true => None,
182            false => Some(
183                text.0
184                    .custom_icon
185                    .unwrap_or_else(|| text.0.input_state.icon()),
186            ),
187        };
188
189        html!(
190            <div class="pf-v6-c-form__helper-text" aria-live="polite">
191                <div class="pf-v6-c-helper-text">
192                    <div class={classes} id="form-help-text-info-helper">
193                        if let Some(icon) = icon {
194                            <span class="pf-v6-c-helper-text__item-icon">{ icon }</span>
195                        }
196                        <span
197                            class="pf-v6-c-helper-text__item-text"
198                        >
199                            { &text.0.message }
200                        </span>
201                    </div>
202                </div>
203            </div>
204        )
205    }
206}
207
208// with validation
209
210/// Properties for [`FormGroupValidated`]
211#[derive(Clone, Properties)]
212pub struct FormGroupValidatedProperties<C>
213where
214    C: BaseComponent + ValidatingComponent,
215{
216    #[prop_or_default]
217    pub children: ChildrenWithProps<C>,
218    #[prop_or_default]
219    pub label: String,
220    #[prop_or_default]
221    pub label_icon: LabelIcon,
222    #[prop_or_default]
223    pub required: bool,
224    pub validator: Validator<C::Value, ValidationResult>,
225
226    #[prop_or_default]
227    pub onvalidated: Callback<ValidationResult>,
228}
229
230#[doc(hidden)]
231pub enum FormGroupValidatedMsg<C>
232where
233    C: ValidatingComponent,
234{
235    Validate(ValidationContext<C::Value>),
236}
237
238impl<C> PartialEq for FormGroupValidatedProperties<C>
239where
240    C: BaseComponent + ValidatingComponent,
241{
242    fn eq(&self, other: &Self) -> bool {
243        self.required == other.required
244            && self.label == other.label
245            && self.children == other.children
246    }
247}
248
249pub struct FormGroupValidated<C>
250where
251    C: BaseComponent,
252{
253    _marker: PhantomData<C>,
254
255    id: String,
256    state: Option<ValidationResult>,
257}
258
259impl<C> Component for FormGroupValidated<C>
260where
261    C: BaseComponent + ValidatingComponent,
262    <C as BaseComponent>::Properties: ValidatingComponentProperties<C::Value> + Clone,
263{
264    type Message = FormGroupValidatedMsg<C>;
265    type Properties = FormGroupValidatedProperties<C>;
266
267    fn create(_: &Context<Self>) -> Self {
268        Self {
269            _marker: Default::default(),
270            id: Uuid::new_v4().to_string(),
271            state: None,
272        }
273    }
274
275    fn update(&mut self, ctx: &Context<Self>, msg: Self::Message) -> bool {
276        match msg {
277            Self::Message::Validate(value) => {
278                let state = ctx.props().validator.run(value);
279                if self.state != state {
280                    self.state = state;
281                    ctx.props()
282                        .onvalidated
283                        .emit(self.state.clone().unwrap_or_default());
284                    if let Some((validation_ctx, _)) = ctx
285                        .link()
286                        .context::<ValidationFormContext>(Callback::noop())
287                    {
288                        validation_ctx
289                            .push_state(GroupValidationResult(self.id.clone(), self.state.clone()));
290                    }
291                }
292            }
293        }
294        true
295    }
296
297    fn view(&self, ctx: &Context<Self>) -> Html {
298        let onvalidate = ctx.link().callback(|v| FormGroupValidatedMsg::Validate(v));
299
300        html!(
301            <FormGroup
302                label={ctx.props().label.clone()}
303                label_icon={ctx.props().label_icon.clone()}
304                required={ctx.props().required}
305                helper_text={self.state.clone().and_then(|s|s.into())}
306            >
307                { for ctx.props().children.iter().map(|mut c|{
308                    let props = Rc::make_mut(&mut c.props);
309                    props.set_onvalidate(onvalidate.clone());
310                    props.set_input_state(self.state.as_ref().map(|s|s.state).unwrap_or_default());
311                    c
312                }) }
313            </FormGroup>
314        )
315    }
316
317    fn destroy(&mut self, ctx: &Context<Self>) {
318        if let Some((ctx, _)) = ctx
319            .link()
320            .context::<ValidationFormContext>(Callback::noop())
321        {
322            ctx.clear_state(self.id.clone());
323        }
324    }
325}