patternfly_yew/components/form/
mod.rs1mod 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#[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 #[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 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
148pub 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 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#[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}