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