enisn / UraniumUI

Uranium is a Free & Open-Source UI Kit for MAUI.
Apache License 2.0
985 stars 110 forks source link

Add a HasError property to the input controls #606

Open DeliriumCode opened 2 months ago

DeliriumCode commented 2 months ago

Can you please add a HasError property to the input controls? A lot of maui devs use mvvm architecture, so the validation is done on the view model rather than the view. for instance: In the viewmodel

public string Value { get; set; }
public bool Value_HasError => string.isnullorempty(Value);

In the View <entry Text="{Binding Value}" HasError={Binding_HasError}/>

I would expect it to change the colour slightly i.e. a red underline under the value etc.

If validators are used in the view i.e.

   <material:TextField Title="blah" Text="{Binding Value}" HasError= "{Binding Value_HasError}" >
       <validation:RequiredValidation  />
   </material:TextField>

I would expect these to be shown if HasError is true, and hidden if HasError is false, and the previous normal validation if HasError is not set at all.

enisn commented 2 months ago

FormView itself has IsValidated property that can be used with binding but there is no input specific bindable property currently.

<input:FormView SubmitCommand="{Binding SubmitCommand}" IsValidated="{Binding IsFormValid}">
   <material:TextField Title="blah" Text="{Binding Value}">
       <validation:RequiredValidation  />
   </material:TextField>
</input:FormView>

In that case, SubmitCommand won't be executed until the form is valid.

I know this property doesn't cover input specific validation state, and it can be used in only one-way.

I'll think about a bindable property that manages state from outside.

-- For the customization:

Text and icon uses Error and ErrorDark colors from app colors: https://github.com/enisn/UraniumUI/blob/3a1101b01a3407dfeec013d3a3c281f88c515905/src/UraniumUI.Material/Controls/InputField.Validation.cs#L30

You can override those colors in your application to change it. I'll add more options to customize them easily for formatting etc.

DeliriumCode commented 2 months ago

So I'm potentially going to use the following workaround, the behaviour of this means that when the form is first opened, no validation errors are visible, if they start entering data errors will be shown as they edit/change focus. When the user clicks save, i trigger validation from my viewmodel. It does mean duplicating the validation atm mind, although i may switch to data attributes on the fields for validation instead, its really not ideal, i will probably make an attached behaviour shortly if so i will post it here as well.

In the viewmodel:

public bool ShowValidation { get; set; }

  public Command Save=> new Command(async () =>
  {
      ShowValidation = true;
      RaisePropertyChanged(nameof(ShowValidation));
      //Do validation here
      if (invalid) {
             await _viewService.ShowMessageAsync("Incomplete Form", "Please complete all required fields!","Ok");
             return;
      }
      //Perform save
      //Close form
      await _viewService.PopAsync()
  });

In the view:

    <ContentPage.ToolbarItems>
        <ToolbarItem Text="Save" Command="{Binding Save}"/>
    </ContentPage.ToolbarItems>
    <controls:UraniumUIStack ShowValidation="{Binding ShowValidation}">
<material:TextField Title="Name *" Text="{Binding Name}" >
    <validation:RequiredValidation  />
</material:TextField>

New custom control:

public class UraniumUIStack: StackLayout
{
    public static readonly BindableProperty ShowValidationProperty = BindableProperty.Create(nameof(ShowValidation), typeof(bool), typeof(UraniumUIStack), false, propertyChanged: ShowValidationChanged);
    public bool ShowValidation
    {
        get { return (bool)GetValue(ShowValidationProperty); }
        set { SetValue(ShowValidationProperty, value); }
    }
    public static void ShowValidationChanged(BindableObject bindable, object oldValue, object newValue)
    {
        if ((bool)newValue)
        {
            foreach (var item in GetChildValitables((UraniumUIStack)bindable))
            {
                item.DisplayValidation();
            }
        }
        else
        {
            foreach (var item in GetChildValitables((UraniumUIStack)bindable))
            {
                item.ResetValidation();
            }
        }
    }

    private static IEnumerable<IValidatable> GetChildValitables(Layout layout)
    {
        foreach (View item in layout.Children)
        {
            if (item is IValidatable)
            {
                yield return (IValidatable)item;
            }
            else if (item is Layout la)
            {
                foreach (var child in GetChildValitables(la))
                {
                    yield return child;
                }
            }
        }
    }
}
DeliriumCode commented 2 months ago

As a side note, one of the reasons i want to do the validation in the viewmodel, is that i have conditional fields, so my validation logic is a bit more complex.

DeliriumCode commented 2 months ago

The following allows you to add :

<material:TextField Title="Name *" Text="{Binding Name}" controls:UraniumUI.HasErrors="{Binding Name_HasError">

There is a small bug with the fact that im adding a validation with no message that messes up the layout slightly, but it allows you to control the validation from within code, with the validator messages been controlled in the view.

public class UraniumUI
{
    public static readonly BindableProperty HasErrorsProperty = BindableProperty.CreateAttached("HasErrors", typeof(bool), typeof(ValidationBehavior), false, propertyChanged: OnHasErrorsChanged);

    public static bool GetHasErrors(BindableObject view) => (bool)view.GetValue(HasErrorsProperty);
    public static void SetHasErrors(BindableObject view, bool value) => view.SetValue(HasErrorsProperty, value);

    private static void OnHasErrorsChanged(BindableObject bindable, object oldValue, object newValue)
    {
        if (bindable is not IValidatable validatableControl) return;

        bool hasErrors = (bool)newValue;
        if (hasErrors)
        {

            if (!validatableControl.Validations.Any(m=>m is HasErrorValidation))
            {
                validatableControl.Validations.Add(new HasErrorValidation());
                validatableControl.DisplayValidation();
            }
        }
        else
        {
            validatableControl.Validations.RemoveAll(m => m is HasErrorValidation);
            validatableControl.ResetValidation();
        }
    }
}
public class HasErrorValidation : BindableObject, IValidation
{
    public string Message => "";

    public bool Validate(object value)
    {
        return false;
    }
}
andreas-spindler-mw commented 3 weeks ago

Oh yeah! A bindable Validator would be awesome. Or even a way to bind the error message.