patternfly_yew/components/form/
group.rs1use crate::prelude::*;
2use std::{marker::PhantomData, rc::Rc};
3use uuid::Uuid;
4use yew::{
5 prelude::*,
6 virtual_dom::{VChild, VNode},
7};
8
9#[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 #[default]
29 None,
30 Help(VChild<PopoverBody>),
32 Children(Html),
34}
35
36#[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
109pub 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#[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}