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-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
105pub 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#[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}