From 670dd923296fa737a89c83b5fca06a503d4d62b2 Mon Sep 17 00:00:00 2001 From: Dietmar Maurer Date: Sun, 22 Oct 2023 15:51:02 +0200 Subject: [PATCH] FormContext: replace submit_converter with SubmitValidateFn Simply let the validation function return the value to be submitted. --- src/widget/form/boolean.rs | 1 - src/widget/form/checkbox.rs | 1 - src/widget/form/context.rs | 155 +++++++++++++--------------- src/widget/form/field.rs | 18 ++-- src/widget/form/hidden.rs | 1 - src/widget/form/managed_field.rs | 28 ++--- src/widget/form/mod.rs | 3 + src/widget/form/number.rs | 33 +----- src/widget/form/selector.rs | 18 ++-- src/widget/form/submit_validate.rs | 62 +++++++++++ src/widget/form/textarea.rs | 12 +-- src/widget/form/tristate_boolean.rs | 5 +- src/widget/menu/menu_checkbox.rs | 1 - 13 files changed, 181 insertions(+), 157 deletions(-) create mode 100644 src/widget/form/submit_validate.rs diff --git a/src/widget/form/boolean.rs b/src/widget/form/boolean.rs index 239a0ff..a8cbd1a 100644 --- a/src/widget/form/boolean.rs +++ b/src/widget/form/boolean.rs @@ -84,7 +84,6 @@ impl ManagedField for BooleanField { value: value.into(), valid, default, radio_group: false, unique: false, - submit_converter: None, } } diff --git a/src/widget/form/checkbox.rs b/src/widget/form/checkbox.rs index 87b1f51..a0aa954 100644 --- a/src/widget/form/checkbox.rs +++ b/src/widget/form/checkbox.rs @@ -103,7 +103,6 @@ impl ManagedField for CheckboxField { default: default.into(), radio_group: props.radio_group, unique: false, - submit_converter: None, } } diff --git a/src/widget/form/context.rs b/src/widget/form/context.rs index 89de3e3..e953cc6 100644 --- a/src/widget/form/context.rs +++ b/src/widget/form/context.rs @@ -15,7 +15,7 @@ use yew::prelude::*; use crate::state::optional_rc_ptr_eq; -use super::ValidateFn; +use super::SubmitValidateFn; /// Basic field options used inside [FormContext]. /// @@ -39,7 +39,7 @@ struct FieldRegistration { // Field name. pub name: AttrValue, /// The validation function - pub validate: Option>, + pub validate: Option>, /// Radio group flag. Set this when the field is part of a radio group. pub radio_group: bool, /// Do not allow multiple fields with the same name. @@ -50,29 +50,43 @@ struct FieldRegistration { pub options: FieldOptions, /// Field value pub value: Value, + // Submit value (value returned by validate) + submit_value: Option, /// Field default value. pub default: Value, /// Validation result. pub valid: Result<(), String>, - /// Optional conversion hook called by [FormContext::get_submit_data] - /// - /// Should return `None` if value is invalid. - pub submit_converter: Option>>, } impl FieldRegistration { fn is_dirty(&self) -> bool { // we need to compare the value that will be submitted - let submit_value = match &self.submit_converter { - Some(convert) => { - match convert.emit(self.value.clone()) { - Some(v) => v, - None => return true, + match &self.submit_value { + Some(submit_value) => &self.default != submit_value, + None => true, + } + } + + fn apply_value(&mut self, value: Value) { + let (valid, submit_value); + if let Some(validate) = &self.validate { + match validate.apply(&value).map_err(|e| e.to_string()) { + Ok(value) => { + submit_value = Some(value); + valid = Ok(()); + } + Err(e) => { + submit_value = None; + valid = Err(e.to_string()); } } - None => self.value.clone(), - }; - self.default != submit_value + } else { + submit_value = Some(value.clone()); + valid = Ok(()); + } + self.value = value; + self.valid = valid; + self.submit_value = submit_value; } } /// Shared form data ([Rc]<[RefCell]<[FormContextState]>>) @@ -182,7 +196,7 @@ impl FieldHandle { self.write().validate_field_by_slab_key(key); } /// Update validation function and trigger re-validation - pub fn update_validate(&mut self, validate: Option>) { + pub fn update_validate(&mut self, validate: Option>) { let key = self.key; self.write() .update_field_validate_by_slab_key(key, validate); @@ -274,10 +288,9 @@ impl FormContext { value: Value, default: Value, radio_group: bool, - validate: Option>, + validate: Option>, options: FieldOptions, unique: bool, - submit_converter: Option>>, ) -> FieldHandle { let key = self.inner.borrow_mut().register_field( name, @@ -287,7 +300,6 @@ impl FormContext { validate, options, unique, - submit_converter, ); FieldHandle { @@ -419,32 +431,28 @@ impl FormContextState { value: Value, default: Value, radio_group: bool, - validate: Option>, + validate: Option>, options: FieldOptions, unique: bool, - submit_converter: Option>>, ) -> usize { let name = name.into_prop_value(); let unique = if radio_group { false } else { unique }; - let mut valid = Ok(()); - if let Some(validate) = &validate { - valid = validate.apply(&value).map_err(|e| e.to_string()); - } - - let field = FieldRegistration { + let mut field = FieldRegistration { name: name.clone(), validate, radio_group, unique, options, - value, + value: Value::Null, // set by apply_value below + submit_value: None, // set by apply_value below default: default.clone(), - valid, - submit_converter, + valid: Ok(()), // set by apply_value below }; + field.apply_value(value); + let slab_key; if unique { @@ -610,14 +618,7 @@ impl FormContextState { self.version += 1; } if value != field.value { - let mut valid = Ok(()); - if let Some(validate) = &field.validate { - valid = validate.apply(&value).map_err(|e| e.to_string()); - } - - field.value = value; - field.valid = valid; - + field.apply_value(value); self.version += 1; } } @@ -641,12 +642,7 @@ impl FormContextState { let field = &mut self.fields[slab_key]; if field.value != field.default { self.version += 1; - field.value = field.default.clone(); - if let Some(validate) = &field.validate { - field.valid = validate.apply(&field.value).map_err(|e| e.to_string()); - } else { - field.valid = Ok(()); - } + field.apply_value(field.default.clone()); } } @@ -680,12 +676,7 @@ impl FormContextState { for (_key, field) in self.fields.iter_mut() { if field.value != field.default { changes = true; - field.value = field.default.clone(); - let mut valid = Ok(()); - if let Some(validate) = &field.validate { - valid = validate.apply(&field.value).map_err(|e| e.to_string()); - } - field.valid = valid; + field.apply_value(field.default.clone()); } } if changes { @@ -713,21 +704,37 @@ impl FormContextState { if field.radio_group { // fixme: do something ? } else { - let mut valid = Ok(()); + let (valid, submit_value); if let Some(validate) = &field.validate { - valid = validate.apply(&field.value).map_err(|e| e.to_string()); + match validate.apply(&field.value) { + Ok(value) => { + submit_value = Some(value); + valid = Ok(()); + } + Err(e) => { + submit_value = None; + valid = Err(e.to_string()); + } + } + } else { + submit_value = Some(field.value.clone()); + valid = Ok(()); } if valid != field.valid { self.version += 1; field.valid = valid; } - } + if submit_value != field.submit_value { + self.version += 1; + field.submit_value = submit_value; + } + } } fn update_field_validate_by_slab_key( &mut self, slab_key: usize, - validate: Option>, + validate: Option>, ) { let field = &mut self.fields[slab_key]; field.validate = validate; @@ -863,22 +870,15 @@ impl FormContextState { let field = &self.fields[key]; let submit_empty = field.options.submit_empty; if field.valid.is_ok() && field.options.submit { - let mut value = field.value.clone(); - if !submit_empty && value_is_empty(&value) { - continue; - } - if let Some(submit_converter) = &field.submit_converter { - value = match submit_converter.emit(value) { - Some(value) => { - if !submit_empty & value_is_empty(&value) { - continue; - } - value + match &field.submit_value { + None => continue, + Some(value) => { + if !submit_empty & value_is_empty(&value) { + continue; } - None => continue, // should not happen - }; + data[name.deref()] = value.clone(); + } } - data[name.deref()] = value; } continue; } @@ -890,22 +890,15 @@ impl FormContextState { let field = &self.fields[key]; let submit_empty = field.options.submit_empty; if field.valid.is_ok() && field.options.submit { - let mut value = field.value.clone(); - if !submit_empty && value_is_empty(&value) { - continue; - } - if let Some(submit_converter) = &field.submit_converter { - value = match submit_converter.emit(value) { - Some(value) => { - if !submit_empty && value_is_empty(&value) { - continue; - } - value + match &field.submit_value { + None => continue, + Some(value) => { + if !submit_empty & value_is_empty(&value) { + continue; } - None => continue, // should not happen - }; + list.push(value.clone()); + } } - list.push(value); } } if !list.is_empty() { diff --git a/src/widget/form/field.rs b/src/widget/form/field.rs index 5014c81..d8199b0 100644 --- a/src/widget/form/field.rs +++ b/src/widget/form/field.rs @@ -301,23 +301,19 @@ impl ManagedField for StandardField { } } - fn validator(props: &Self::ValidateClosure, value: &Value) -> Result<(), Error> { + fn validator(props: &Self::ValidateClosure, value: &Value) -> Result { let value = match value { Value::Null => String::new(), Value::Number(n) => n.to_string(), Value::String(v) => v.clone(), - _ => { - // should not happen - log::error!("PwtField: got wrong data type in validate!"); - String::new() - } + _ => return Err(Error::msg(tr!("got wrong data type."))), }; if value.is_empty() { if props.required { return Err(Error::msg(tr!("Field may not be empty."))); } else { - return Ok(()); + return Ok(Value::String(String::new())); } } @@ -344,10 +340,11 @@ impl ManagedField for StandardField { } } - match &props.validate { - Some(validate) => validate.apply(&value), - None => Ok(()), + if let Some(validate) = &props.validate { + validate.apply(&value)?; } + + Ok(Value::String(value)) } fn setup(props: &Self::Properties) -> ManagedFieldState { @@ -370,7 +367,6 @@ impl ManagedField for StandardField { default, radio_group: false, unique: false, - submit_converter: None, } } diff --git a/src/widget/form/hidden.rs b/src/widget/form/hidden.rs index 868b9dd..fe35d4d 100644 --- a/src/widget/form/hidden.rs +++ b/src/widget/form/hidden.rs @@ -67,7 +67,6 @@ impl ManagedField for HiddenField { default, radio_group: false, unique: false, - submit_converter: None, } } diff --git a/src/widget/form/managed_field.rs b/src/widget/form/managed_field.rs index d57da73..e1a0831 100644 --- a/src/widget/form/managed_field.rs +++ b/src/widget/form/managed_field.rs @@ -5,7 +5,9 @@ use wasm_bindgen::{closure::Closure, JsCast}; use yew::html::Scope; use yew::prelude::*; -use super::{FieldHandle, FieldOptions, FormContext, FormContextObserver, ValidateFn}; +use super::{ + FieldHandle, FieldOptions, FormContext, FormContextObserver, SubmitValidateFn, +}; use crate::props::FieldBuilder; /// Managed field state. @@ -33,9 +35,6 @@ pub struct ManagedFieldState { /// /// Instead, use the same state for all of those fields. pub unique: bool, - - /// Optional conversion called by [FormContext::get_submit_data] - pub submit_converter: Option>>, } /// Managed field context. @@ -167,8 +166,9 @@ pub trait ManagedField: Sized { /// The validation function. /// /// Gets the result of [`validation_args`](Self::validation_args) and the value as parameter. - fn validator(_props: &Self::ValidateClosure, _value: &Value) -> Result<(), Error> { - Ok(()) + /// If valid, it should return the value to be submitted. + fn validator(_props: &Self::ValidateClosure, value: &Value) -> Result { + Ok(value.clone()) } /// Returns the initial field setup. @@ -229,7 +229,7 @@ pub struct ManagedFieldMaster { label_clicked_closure: Option>, /// The validation function - validate: ValidateFn, + validate: SubmitValidateFn, } impl ManagedFieldMaster { @@ -278,7 +278,6 @@ impl ManagedFieldMaster { Some(self.validate.clone()), options, self.comp_state.unique, - self.comp_state.submit_converter.clone(), ); // FormContext may already have field data (i.e for unique fields), so sync back @@ -299,11 +298,12 @@ impl Component for ManagedFieldMaster { let props = ctx.props(); let validation_args = MF::validation_args(props); - let validate = ValidateFn::new(move |value| MF::validator(&validation_args, value)); + let validate = SubmitValidateFn::new(move |value| MF::validator(&validation_args, value)); let mut comp_state = MF::setup(props); comp_state.valid = validate .apply(&comp_state.value) + .map(|_| ()) .map_err(|err| err.to_string()); let sub_context = ManagedFieldContext::new(ctx, &comp_state); @@ -354,8 +354,9 @@ impl Component for ManagedFieldMaster { } let value = value.unwrap_or(self.comp_state.value.clone()); - let valid = valid - .unwrap_or_else(|| self.validate.apply(&value).map_err(|e| e.to_string())); + let valid = + valid.unwrap_or_else(|| self.validate.apply(&value) + .map(|_| ()).map_err(|e| e.to_string())); let value_changed = value != self.comp_state.value; let valid_changed = valid != self.comp_state.valid; @@ -370,7 +371,7 @@ impl Component for ManagedFieldMaster { true } Msg::UpdateValue(value) => { - let valid = self.validate.apply(&value).map_err(|e| e.to_string()); + let valid = self.validate.apply(&value).map(|_| ()).map_err(|e| e.to_string()); let value_changed = value != self.comp_state.value; let valid_changed = valid != self.comp_state.valid; @@ -395,6 +396,7 @@ impl Component for ManagedFieldMaster { let valid = self .validate .apply(&self.comp_state.value) + .map(|_| ()) .map_err(|e| e.to_string()); let valid_changed = valid != self.comp_state.valid; @@ -477,7 +479,7 @@ impl Component for ManagedFieldMaster { if validation_args != old_validation_args { log::info!("UPDATE VF {:?}", props.as_input_props().name); - let validate = ValidateFn::new(move |value| MF::validator(&validation_args, value)); + let validate = SubmitValidateFn::new(move |value| MF::validator(&validation_args, value)); refresh1 = true; self.validate = validate.clone(); if let Some(field_handle) = &mut self.field_handle { diff --git a/src/widget/form/mod.rs b/src/widget/form/mod.rs index 241ba00..721018a 100644 --- a/src/widget/form/mod.rs +++ b/src/widget/form/mod.rs @@ -50,6 +50,9 @@ pub use submit_button::{PwtSubmitButton, SubmitButton}; mod textarea; pub use textarea::{PwtTextArea, TextArea}; +mod submit_validate; +pub use submit_validate::{IntoSubmitValidateFn, SubmitValidateFn}; + mod validate; pub use validate::{IntoValidateFn, ValidateFn}; diff --git a/src/widget/form/number.rs b/src/widget/form/number.rs index 2fefdba..e074dc8 100644 --- a/src/widget/form/number.rs +++ b/src/widget/form/number.rs @@ -383,28 +383,6 @@ pub struct ValidateClosure { validate: Option>, } -impl NumberField { - // Note: This is called on submit, but only for valid fields - fn submit_convert(value: Value) -> Option { - match &value { - Value::Number(_) | Value::Null => Some(value), - Value::String(text) => { - if text.is_empty() { - return Some(Value::Null); - } - match T::value_to_number(&value) { - Ok(n) => Some(n.into()), - Err(err) => { - log::error!("NumberField: submit_convert failed - {err}"); - None - } - } - } - _ => None, - } - } -} - impl ManagedField for NumberField { type Properties = Number; type Message = Msg; @@ -419,7 +397,7 @@ impl ManagedField for NumberField { } } - fn validator(props: &Self::ValidateClosure, value: &Value) -> Result<(), Error> { + fn validator(props: &Self::ValidateClosure, value: &Value) -> Result { let is_empty = match value { Value::Null => true, Value::Number(_) => false, @@ -431,7 +409,7 @@ impl ManagedField for NumberField { if props.required { return Err(Error::msg(tr!("Field may not be empty."))); } else { - return Ok(()); + return Ok(Value::Null); } } @@ -457,10 +435,10 @@ impl ManagedField for NumberField { } } - match &props.validate { - Some(validate) => validate.apply(&number), - None => Ok(()), + if let Some(validate) = &props.validate { + validate.apply(&number)?; } + Ok(number.into()) } fn setup(props: &Self::Properties) -> ManagedFieldState { @@ -486,7 +464,6 @@ impl ManagedField for NumberField { default, radio_group: false, unique: false, - submit_converter: Some(Callback::from(Self::submit_convert)), } } diff --git a/src/widget/form/selector.rs b/src/widget/form/selector.rs index c70187c..fbf62c1 100644 --- a/src/widget/form/selector.rs +++ b/src/widget/form/selector.rs @@ -194,33 +194,30 @@ impl ManagedField for SelectorField { } } - fn validator(props: &Self::ValidateClosure, value: &Value) -> Result<(), Error> { + fn validator(props: &Self::ValidateClosure, value: &Value) -> Result { let value = match value { Value::Null => String::new(), Value::String(v) => v.clone(), - _ => { - // should not happen - log::error!("PwtField: got wrong data type in validate!"); - String::new() - } + _ => return Err(Error::msg(tr!("got wrong data type."))), }; if value.is_empty() { if props.required { bail!("Field may not be empty."); } else { - return Ok(()); + return Ok(Value::String(String::new())); } } if !props.store.is_empty() { - match &props.validate { - Some(validate) => validate.apply(&(value.into(), props.store.clone())), - None => Ok(()), + if let Some(validate) = &props.validate { + validate.apply(&(value.clone().into(), props.store.clone()))?; } } else { bail!("no data loaded"); } + + Ok(Value::String(value)) } fn setup(props: &Self::Properties) -> ManagedFieldState { @@ -233,7 +230,6 @@ impl ManagedField for SelectorField { default, radio_group: false, unique: false, - submit_converter: None, } } diff --git a/src/widget/form/submit_validate.rs b/src/widget/form/submit_validate.rs new file mode 100644 index 0000000..e60be75 --- /dev/null +++ b/src/widget/form/submit_validate.rs @@ -0,0 +1,62 @@ +use std::rc::Rc; + +use anyhow::Error; +use serde_json::Value; + +/// A [SubmitValidateFn] function is a callback that detrermines if the +/// passed record is valid, and return the value to be submitted. +/// +/// Wraps `Rc` around `Fn` so it can be passed as a prop. +pub struct SubmitValidateFn(Rc Result>); + +impl Clone for SubmitValidateFn { + fn clone(&self) -> Self { + Self(Rc::clone(&self.0)) + } +} + +impl PartialEq for SubmitValidateFn { + fn eq(&self, other: &Self) -> bool { + Rc::ptr_eq(&self.0, &other.0) + } +} + +impl SubmitValidateFn { + /// Creates a new [`SubmitValidateFn`] + pub fn new(validate: impl 'static + Fn(&T) -> Result) -> Self { + Self(Rc::new(validate)) + } + + /// Apply the validation function + pub fn apply(&self, data: &T) -> Result { + (self.0)(data) + } +} + +impl std::fmt::Debug for SubmitValidateFn { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "SubmitValidateFn({:p})", self.0) + } +} + +impl Result> From for SubmitValidateFn { + fn from(f: F) -> Self { + SubmitValidateFn::new(f) + } +} + +/// Helper trait to create an optional [SubmitValidateFn] property. +pub trait IntoSubmitValidateFn { + fn into_submit_validate_fn(self) -> Option>; +} + +impl>> IntoSubmitValidateFn for V { + fn into_submit_validate_fn(self) -> Option> { + Some(self.into()) + } +} +impl>> IntoSubmitValidateFn for Option { + fn into_submit_validate_fn(self) -> Option> { + self.map(|v| v.into()) + } +} diff --git a/src/widget/form/textarea.rs b/src/widget/form/textarea.rs index e89245b..bf888bf 100644 --- a/src/widget/form/textarea.rs +++ b/src/widget/form/textarea.rs @@ -120,7 +120,7 @@ impl ManagedField for TextAreaField { } } - fn validator(props: &Self::ValidateClosure, value: &Value) -> Result<(), Error> { + fn validator(props: &Self::ValidateClosure, value: &Value) -> Result { let value = match value { Value::Null => String::new(), Value::String(v) => v.clone(), @@ -135,14 +135,15 @@ impl ManagedField for TextAreaField { if props.required { return Err(Error::msg(tr!("Field may not be empty."))); } else { - return Ok(()); + return Ok(Value::String(String::new())); } } - match &props.validate { - Some(validate) => validate.apply(&value), - None => Ok(()), + if let Some(validate) = &props.validate { + validate.apply(&value)?; } + + Ok(Value::String(value)) } fn setup(props: &Self::Properties) -> ManagedFieldState { @@ -165,7 +166,6 @@ impl ManagedField for TextAreaField { default, radio_group: false, unique: false, - submit_converter: None, } } diff --git a/src/widget/form/tristate_boolean.rs b/src/widget/form/tristate_boolean.rs index 717778b..fe1b667 100644 --- a/src/widget/form/tristate_boolean.rs +++ b/src/widget/form/tristate_boolean.rs @@ -105,9 +105,9 @@ impl ManagedField for PwtTristateBoolean { fn validation_args(_props: &Self::Properties) -> Self::ValidateClosure {} - fn validator(_props: &Self::ValidateClosure, value: &Value) -> Result<(), Error> { + fn validator(_props: &Self::ValidateClosure, value: &Value) -> Result { match value { - Value::Null | Value::Bool(_) => Ok(()), + Value::Null | Value::Bool(_) => Ok(value.clone()), _ => Err(Error::msg(tr!("Got wrong data type!"))), } } @@ -136,7 +136,6 @@ impl ManagedField for PwtTristateBoolean { default, radio_group: false, unique: false, - submit_converter: None, } } diff --git a/src/widget/menu/menu_checkbox.rs b/src/widget/menu/menu_checkbox.rs index 58c6772..42ef3f2 100644 --- a/src/widget/menu/menu_checkbox.rs +++ b/src/widget/menu/menu_checkbox.rs @@ -135,7 +135,6 @@ impl ManagedField for MenuCheckboxField { default: default.into(), radio_group: props.radio_group, unique: true, - submit_converter: None, } } -- 2.39.5