@ -14,13 +14,19 @@ public abstract class FormComponent<T, U> : UIComponent, IFormComponent, IDispos
{
{
private Converter < T , U > _converter ;
private Converter < T , U > _converter ;
/// <summary>
/// Invoked whenever the string value cannot be converted
/// </summary>
public event EventHandler < string > ConversionErrorOccured ;
protected FormComponent ( Converter < T , U > converter )
protected FormComponent ( Converter < T , U > converter )
{
{
_converter = converter ? ? throw new ArgumentNullException ( nameof ( converter ) ) ;
_converter = converter ? ? throw new ArgumentNullException ( nameof ( converter ) ) ;
_converter . OnError = OnConversionError ;
_converter . ErrorOccured + = ( s , e ) = > OnConversionError ( e ) ;
}
}
[CascadingParameter] internal IForm Form { get ; set ; }
[CascadingParameter]
internal IForm ? Form { get ; set ; }
/// <summary>
/// <summary>
/// If true, this is a top-level form component. If false, this input is a sub-component of another input (i.e. TextField, Select, etc).
/// If true, this is a top-level form component. If false, this input is a sub-component of another input (i.e. TextField, Select, etc).
@ -33,77 +39,77 @@ public abstract class FormComponent<T, U> : UIComponent, IFormComponent, IDispos
/// If true, this form input is required to be filled out.
/// If true, this form input is required to be filled out.
/// </summary>
/// </summary>
[Parameter]
[Parameter]
[Category(CategoryTypes.FormComponent.Validation)]
public bool Required { get ; set ; }
public bool Required { get ; set ; }
/// <summary>
/// <summary>
/// The error text that will be displayed if the input is not filled out but required.
/// The error text that will be displayed if the input is not filled out but required.
/// </summary>
/// </summary>
[Parameter]
[Parameter]
[Category(CategoryTypes.FormComponent.Validation)]
public string RequiredError { get ; set ; } = "Required" ;
public string RequiredError { get ; set ; } = "Required" ;
/// <summary>
/// <summary>
/// The ErrorText that will be displayed if Error true.
/// The ErrorText that will be displayed if <see cref="Has Error"/> is set to true.
/// </summary>
/// </summary>
[Parameter]
[Parameter]
[Category(CategoryTypes.FormComponent.Validation)]
public string ErrorText { get ; set ; }
public string ErrorText { get ; set ; }
/// <summary>
/// <summary>
/// If true, the label will be displayed in an error state.
/// If true, the label will be displayed in an error state.
/// </summary>
/// </summary>
[Parameter]
[Parameter]
[Category(CategoryTypes.FormComponent.Validation)]
public bool HasError { get ; set ; }
public bool Error { get ; set ; }
/// <summary>
/// <summary>
/// The ErrorId that will be used by aria-describedby if Error true
/// The ErrorId that will be used by aria-describedby if <see cref="Has Error"/> is true
/// </summary>
/// </summary>
[Parameter]
[Parameter]
[Category(CategoryTypes.FormComponent.Validation)]
public string ErrorId { get ; set ; }
public string ErrorId { get ; set ; }
/// <summary>
/// <summary>
/// The generic converter of the component.
/// The generic converter of the component.
/// </summary>
/// </summary>
[Parameter]
[Parameter]
[Category(CategoryTypes.FormComponent.Behavior)]
public Converter < T , U > Converter
public Converter < T , U > Converter
{
{
get = > _converter ;
get = > _converter ;
set = > SetConverter ( value ) ;
set = > SetConverter ( value ) ;
}
}
protected virtual bool SetConverter ( Converter < T , U > value )
{
var changed = ( _converter ! = value ) ;
if ( changed )
{
_converter = value ? ? throw new ArgumentNullException ( nameof ( value ) ) ; // converter is mandatory at all times
_converter . OnError = OnConversionError ;
}
return changed ;
}
/// <summary>
/// <summary>
/// The culture of the component .
/// The culture of the component. Also sets the culture of the <see cref="Converter"/> .
/// </summary>
/// </summary>
[Parameter]
[Parameter]
[Category(CategoryTypes.FormComponent.Behavior)]
public CultureInfo Culture
public CultureInfo Culture
{
{
get = > _converter . Culture ;
get = > _converter . Culture ;
set = > SetCulture ( value ) ;
set = > SetCulture ( value ) ;
}
}
protected virtual bool SetCulture ( CultureInfo value )
private string _conversionError { get ; set ; }
protected virtual bool SetConverter ( Converter < T , U > value )
{
{
var changed = ( _converter . Culture ! = value ) ;
var changed = _converter ! = value ;
if ( changed )
if ( changed )
{
{
_converter . Culture = value ;
/ *
* Converter is mandatory at all times
* /
_converter = value ? ? throw new ArgumentNullException ( nameof ( value ) ) ;
_converter . ErrorOccured + = ( s , e ) = > OnConversionError ( e ) ;
}
}
return changed ;
}
protected virtual bool SetCulture ( CultureInfo value )
{
var changed = _converter . Culture ! = value ;
if ( changed )
_converter . Culture = value ;
return changed ;
return changed ;
}
}
@ -111,37 +117,38 @@ public abstract class FormComponent<T, U> : UIComponent, IFormComponent, IDispos
{
{
// note: we need to update the form here because the conversion error might lead to not updating the value
// note: we need to update the form here because the conversion error might lead to not updating the value
// ... which leads to not updating the form
// ... which leads to not updating the form
Touched = true ;
//TODO Why does the form need to be updated?
Modified = true ;
_conversionError = error ;
Form ? . Update ( this ) ;
Form ? . Update ( this ) ;
OnConversionErrorOccurred ( error ) ;
}
protected virtual void OnConversionErrorOccurred ( string error )
ConversionErrorOccured ? . Invoke ( this , error ) ;
{
/* Descendants can override this method to catch conversion errors */
}
}
/// <summary>
/// <summary>
/// True if the conversion from string to T failed
/// True if the conversion from string to T failed
/// </summary>
/// </summary>
public bool ConversionError = > _converter . GetError ;
public bool ConversionError = > ! string . IsNullOrWhiteSpace ( _conversionError ) ;
/// <summary>
/// <summary>
/// The error message of the conversion error from string to T. Null otherwise
/// The error message of the conversion error from string to T. Null otherwise
/// </summary>
/// </summary>
public string ConversionErrorMessage = > _conver ter. GetErrorMessage ;
public string ConversionErrorMessage = > _conver sionError ;
/// <summary>
/// <summary>
/// True if the input has any of the following errors: An error set from outside, a conversion error or
/// True if the input has any of the following errors: An error set from outside, a conversion error or
/// one or more validation errors
/// one or more validation errors
/// </summary>
/// </summary>
public bool HasErrors = > Error | | ConversionError | | ValidationErrors . Count > 0 ;
public bool HasErrors = > Has Error | | ConversionError | | ValidationErrors . Count > 0 ;
/// <summary>
/// <summary>
/// Return the validation error text or the conversion error message.
/// Return the validation error text or the conversion error message.
/// </summary>
/// </summary>
/// <returns>Error text/message</returns>
/// <returns>Error text/message</returns>
public string GetErrorText ( )
public string ? GetErrorText ( )
{
{
// ErrorText is either set from outside or the first validation error
// ErrorText is either set from outside or the first validation error
if ( ! IsNullOrWhiteSpace ( ErrorText ) )
if ( ! IsNullOrWhiteSpace ( ErrorText ) )
@ -154,13 +161,13 @@ public abstract class FormComponent<T, U> : UIComponent, IFormComponent, IDispos
}
}
/// <summary>
/// <summary>
/// This manages the state of having been "touched" by the user. A form control always starts out untouch ed
/// This manages the state of having been modified by the user. A form control always starts out unmodifi ed
/// but becomes touch ed when the user performed input or the blur event was raised.
/// but becomes modifi ed when the user performed input or the blur event was raised.
///
///
/// The touched state is only relevant for inputs that have no value (i.e. empty text fields). Being untouch ed will
/// The modified state is only relevant for inputs that have no value (i.e. empty text fields). Being unmodifi ed will
/// suppress RequiredError
/// suppress the display of the <see cref=" RequiredError"/>
/// </summary>
/// </summary>
public bool Touch ed { get ; protected set ; }
public bool Modifi ed { get ; protected set ; }
#region MudForm Validation
#region MudForm Validation
@ -182,10 +189,20 @@ public abstract class FormComponent<T, U> : UIComponent, IFormComponent, IDispos
[Category(CategoryTypes.FormComponent.Validation)]
[Category(CategoryTypes.FormComponent.Validation)]
public object Validation { get ; set ; }
public object Validation { get ; set ; }
private T __value ;
/// <summary>
/// <summary>
/// This is the form component's value.
/// This is the form component's value.
/// </summary>
/// </summary>
protected T _value ;
protected T _value
{
get = > __value ;
set
{
__value = value ;
_conversionError = null ;
}
}
// These are the fire-and-forget methods to launch an async validation process.
// These are the fire-and-forget methods to launch an async validation process.
// After each async step, we make sure the current Value of the component has not changed while
// After each async step, we make sure the current Value of the component has not changed while
@ -232,7 +249,7 @@ public abstract class FormComponent<T, U> : UIComponent, IFormComponent, IDispos
// when a validation is forced, we must set Touched to true, because for untouched fields with
// when a validation is forced, we must set Touched to true, because for untouched fields with
// no value, validation does nothing due to the way forms are expected to work (display errors
// no value, validation does nothing due to the way forms are expected to work (display errors
// only after fields have been touched).
// only after fields have been touched).
Touch ed = true ;
Modifi ed = true ;
return ValidateValue ( ) ;
return ValidateValue ( ) ;
}
}
@ -284,7 +301,7 @@ public abstract class FormComponent<T, U> : UIComponent, IFormComponent, IDispos
// required error (must be last, because it is least important!)
// required error (must be last, because it is least important!)
if ( Required )
if ( Required )
{
{
if ( Touch ed & & ! HasValue ( _value ) )
if ( Modifi ed & & ! HasValue ( _value ) )
errors . Add ( RequiredError ) ;
errors . Add ( RequiredError ) ;
}
}
}
}
@ -297,7 +314,7 @@ public abstract class FormComponent<T, U> : UIComponent, IFormComponent, IDispos
// if Error and ErrorText are set by the user, setting them here will have no effect.
// if Error and ErrorText are set by the user, setting them here will have no effect.
// if Error, create an error id that can be used by aria-describedby on input control
// if Error, create an error id that can be used by aria-describedby on input control
ValidationErrors = errors ;
ValidationErrors = errors ;
Error = errors . Count > 0 ;
Has Error = errors . Count > 0 ;
ErrorText = errors . FirstOrDefault ( ) ;
ErrorText = errors . FirstOrDefault ( ) ;
ErrorId = HasErrors ? Guid . NewGuid ( ) . ToString ( ) : null ;
ErrorId = HasErrors ? Guid . NewGuid ( ) . ToString ( ) : null ;
Form ? . Update ( this ) ;
Form ? . Update ( this ) ;
@ -487,7 +504,8 @@ public abstract class FormComponent<T, U> : UIComponent, IFormComponent, IDispos
{
{
/* to be overridden */
/* to be overridden */
_value = default ;
_value = default ;
Touched = false ;
_conversionError = null ;
Modified = false ;
StateHasChanged ( ) ;
StateHasChanged ( ) ;
}
}
@ -496,7 +514,7 @@ public abstract class FormComponent<T, U> : UIComponent, IFormComponent, IDispos
/// </summary>
/// </summary>
public void ResetValidation ( )
public void ResetValidation ( )
{
{
Error = false ;
Has Error = false ;
ValidationErrors . Clear ( ) ;
ValidationErrors . Clear ( ) ;
ErrorText = null ;
ErrorText = null ;
StateHasChanged ( ) ;
StateHasChanged ( ) ;
@ -527,28 +545,26 @@ public abstract class FormComponent<T, U> : UIComponent, IFormComponent, IDispos
/// <summary>
/// <summary>
/// Specify an expression which returns the model's field for which validation messages should be displayed.
/// Specify an expression which returns the model's field for which validation messages should be displayed.
/// </summary>
/// </summary>
#nullable enable
[Parameter]
[Parameter]
[Category(CategoryTypes.FormComponent.Validation)]
public Expression < Func < T > > ? For { get ; set ; }
public Expression < Func < T > > ? For { get ; set ; }
#nullable disable
public bool IsForNull = > For = = null ;
public bool IsForNull = > For = = null ;
/// <summary>
/// <summary>
/// Stores the list of validation attributes attached to the property targeted by <seealso cref="For"/>. If <seealso cref="For"/> is null, this property is null too.
/// Stores the list of validation attributes attached to the property targeted by <seealso cref="For"/>. If <seealso cref="For"/> is null, this property is null too.
/// </summary>
/// </summary>
#nullable enable
private IEnumerable < ValidationAttribute > ? _validationAttrsFor ;
private IEnumerable < ValidationAttribute > ? _validationAttrsFor ;
#nullable disable
private void OnValidationStateChanged ( object sender , ValidationStateChangedEventArgs e )
private void OnValidationStateChanged ( object sender , ValidationStateChangedEventArgs e )
{
{
if ( EditContext ! = null & & ! _fieldIdentifier . Equals ( default ( FieldIdentifier ) ) )
if ( EditContext ! = null & & ! _fieldIdentifier . Equals ( default ( FieldIdentifier ) ) )
{
{
var error_msgs = EditContext . GetValidationMessages ( _fieldIdentifier ) . ToArray ( ) ;
var error_msgs = EditContext . GetValidationMessages ( _fieldIdentifier ) . ToArray ( ) ;
Error = error_msgs . Length > 0 ;
Has Error = error_msgs . Length > 0 ;
ErrorText = ( Error ? error_msgs [ 0 ] : null ) ;
ErrorText = ( Has Error ? error_msgs [ 0 ] : null ) ;
StateHasChanged ( ) ;
StateHasChanged ( ) ;
}
}
}
}
@ -561,16 +577,13 @@ public abstract class FormComponent<T, U> : UIComponent, IFormComponent, IDispos
/// <summary>
/// <summary>
/// To find out whether or not For parameter has changed we keep a separate reference
/// To find out whether or not For parameter has changed we keep a separate reference
/// </summary>
/// </summary>
#nullable enable
private Expression < Func < T > > ? _currentFor ;
private Expression < Func < T > > ? _currentFor ;
#nullable disable
/// <summary>
/// <summary>
/// To find out whether or not EditContext parameter has changed we keep a separate reference
/// To find out whether or not EditContext parameter has changed we keep a separate reference
/// </summary>
/// </summary>
#nullable enable
private EditContext ? _currentEditContext ;
private EditContext ? _currentEditContext ;
#nullable disable
protected override void OnParametersSet ( )
protected override void OnParametersSet ( )
{
{