patternfly_yew/components/form/
mod.rs

1//! Form controls
2mod area;
3mod checkbox;
4mod group;
5mod input;
6mod radio;
7mod section;
8mod select;
9mod validation;
10
11pub use area::*;
12pub use checkbox::*;
13pub use group::*;
14pub use input::*;
15pub use radio::*;
16pub use section::*;
17pub use select::*;
18use std::collections::BTreeMap;
19pub use validation::*;
20
21use crate::prelude::{Alert, AlertType, AsClasses, Button, ExtendClasses, WithBreakpoints};
22use yew::prelude::*;
23
24#[derive(Clone, Copy, Debug, PartialEq, Eq)]
25pub struct FormHorizontal;
26
27impl AsClasses for FormHorizontal {
28    fn extend_classes(&self, classes: &mut Classes) {
29        classes.push("pf-m-horizontal")
30    }
31}
32
33#[derive(Clone, Debug, PartialEq)]
34pub struct FormAlert {
35    pub r#type: AlertType,
36    pub title: String,
37    pub children: Html,
38}
39
40//
41// Form
42//
43
44/// Properties for [`Form`]
45#[derive(Clone, PartialEq, Properties)]
46pub struct FormProperties {
47    #[prop_or_default]
48    pub id: Option<String>,
49
50    #[prop_or_default]
51    pub horizontal: WithBreakpoints<FormHorizontal>,
52
53    #[prop_or_default]
54    pub action: Option<String>,
55    #[prop_or_default]
56    pub method: Option<String>,
57
58    #[prop_or_default]
59    pub limit_width: bool,
60
61    #[prop_or_default]
62    pub children: Html,
63
64    #[prop_or_default]
65    pub alert: Option<FormAlert>,
66
67    /// Reports the overall validation state
68    #[prop_or_default]
69    pub onvalidated: Callback<InputState>,
70
71    #[prop_or_default]
72    pub validation_warning_title: Option<String>,
73    #[prop_or_default]
74    pub validation_error_title: Option<String>,
75
76    #[prop_or_default]
77    pub onsubmit: Callback<SubmitEvent>,
78}
79
80#[derive(Debug, Default, PartialEq, Eq)]
81pub struct ValidationState {
82    results: BTreeMap<String, ValidationResult>,
83    state: InputState,
84}
85
86impl ValidationState {
87    fn to_state(&self) -> InputState {
88        let mut current = InputState::Default;
89        for r in self.results.values() {
90            if r.state > current {
91                current = r.state;
92            }
93            if current == InputState::Error {
94                break;
95            }
96        }
97        current
98    }
99
100    fn push_state(&mut self, state: GroupValidationResult) -> bool {
101        match state.1 {
102            Some(result) => {
103                self.results.insert(state.0, result);
104            }
105            None => {
106                self.results.remove(&state.0);
107            }
108        }
109
110        // update with diff
111
112        let state = self.to_state();
113        if self.state != state {
114            self.state = state;
115            true
116        } else {
117            false
118        }
119    }
120}
121
122#[derive(Clone, Default, PartialEq)]
123pub struct ValidationFormContext {
124    callback: Callback<GroupValidationResult>,
125    state: InputState,
126}
127
128impl ValidationFormContext {
129    pub fn new(callback: Callback<GroupValidationResult>, state: InputState) -> Self {
130        Self { callback, state }
131    }
132
133    pub fn is_error(&self) -> bool {
134        matches!(self.state, InputState::Error)
135    }
136
137    pub fn push_state(&self, state: GroupValidationResult) {
138        self.callback.emit(state);
139    }
140
141    pub fn clear_state(&self, id: String) {
142        self.callback.emit(GroupValidationResult(id, None));
143    }
144}
145
146pub struct GroupValidationResult(pub String, pub Option<ValidationResult>);
147
148/// The Form component.
149///
150/// > A **form** is a group of elements used to collect information from a user in a variety of contexts including in a modal, in a wizard, or on a page. Use cases for forms include tasks reliant on user-inputted information for completion like logging in, registering, configuring settings, or completing surveys.
151///
152/// See: <https://www.patternfly.org/components/form>
153///
154/// ## Properties
155///
156/// Defined by [`FormProperties`].
157pub struct Form {
158    validation: ValidationState,
159}
160
161#[doc(hidden)]
162pub enum FormMsg {
163    GroupValidationChanged(GroupValidationResult),
164}
165
166impl Component for Form {
167    type Message = FormMsg;
168    type Properties = FormProperties;
169
170    fn create(_ctx: &Context<Self>) -> Self {
171        Self {
172            validation: Default::default(),
173        }
174    }
175
176    fn update(&mut self, ctx: &Context<Self>, msg: Self::Message) -> bool {
177        match msg {
178            FormMsg::GroupValidationChanged(state) => {
179                let changed = self.validation.push_state(state);
180                if changed {
181                    ctx.props().onvalidated.emit(self.validation.state);
182                }
183                changed
184            }
185        }
186    }
187
188    fn view(&self, ctx: &Context<Self>) -> Html {
189        let mut classes = Classes::from("pf-v5-c-form");
190
191        classes.extend_from(&ctx.props().horizontal);
192
193        if ctx.props().limit_width {
194            classes.push("pf-m-limit-width");
195        }
196
197        let alert = &ctx.props().alert;
198        let validation_alert = Self::make_alert(
199            self.validation.state,
200            (
201                ctx.props()
202                    .validation_warning_title
203                    .as_deref()
204                    .unwrap_or("The form contains fields with warnings."),
205                &html!(),
206            ),
207            (
208                ctx.props()
209                    .validation_error_title
210                    .as_deref()
211                    .unwrap_or("The form contains fields with errors."),
212                &html!(),
213            ),
214        );
215
216        // reduce by severity
217
218        let alert = match (alert, &validation_alert) {
219            (None, None) => None,
220            (Some(alert), None) | (None, Some(alert)) => Some(alert),
221            (Some(props), Some(validation)) if validation.r#type > props.r#type => Some(validation),
222            (Some(props), Some(_)) => Some(props),
223        };
224
225        let validation_context = ValidationFormContext::new(
226            ctx.link().callback(FormMsg::GroupValidationChanged),
227            self.validation.state,
228        );
229
230        html! (
231            <ContextProvider<ValidationFormContext> context={validation_context} >
232                <form
233                    novalidate=true
234                    class={classes}
235                    id={ctx.props().id.clone()}
236                    action={ctx.props().action.clone()}
237                    method={ctx.props().method.clone()}
238                    onsubmit={ctx.props().onsubmit.clone()}
239                >
240
241                    if let Some(alert) = alert {
242                        <div class="pf-v5-c-form__alert">
243                            <Alert
244                                inline=true
245                                r#type={alert.r#type}
246                                title={alert.title.clone()}
247                                >
248                                { alert.children.clone() }
249                            </Alert>
250                        </div>
251                    }
252
253                    { ctx.props().children.clone() }
254
255                </form>
256            </ContextProvider<ValidationFormContext>>
257        )
258    }
259}
260
261impl Form {
262    fn make_alert(
263        state: InputState,
264        warning: (&str, &Html),
265        error: (&str, &Html),
266    ) -> Option<FormAlert> {
267        match state {
268            InputState::Default | InputState::Success => None,
269            InputState::Warning => Some(FormAlert {
270                r#type: AlertType::Warning,
271                title: warning.0.to_string(),
272                children: warning.1.clone(),
273            }),
274            InputState::Error => Some(FormAlert {
275                r#type: AlertType::Danger,
276                title: error.0.to_string(),
277                children: error.1.clone(),
278            }),
279        }
280    }
281}
282
283//
284// Action group
285//
286
287/// Properties for [`ActionGroup`]
288#[derive(Clone, PartialEq, Properties)]
289pub struct ActionGroupProperties {
290    pub children: ChildrenWithProps<Button>,
291}
292
293#[function_component(ActionGroup)]
294pub fn action_group(props: &ActionGroupProperties) -> Html {
295    html! {
296        <div class="pf-v5-c-form__group pf-m-action">
297            <div class="pf-v5-c-form__actions">
298                { for props.children.iter() }
299            </div>
300        </div>
301    }
302}