vikramlearning / blazorbootstrap

An Enterprise-class Blazor Bootstrap Component library built on the Blazor and Bootstrap CSS frameworks.
https://docs.blazorbootstrap.com/
Apache License 2.0
670 stars 32 forks source link

Autocomplete + validation overlap issue #459

Open eholman opened 9 months ago

eholman commented 9 months ago

When using validation on an autocomplete component there is an overlap of validated and clean elements:

image

Besides, is there an option to get the validation message displayed? Bootstrap requires that the message is in the same parent element to get its style applied.

<PackageReference Include="Blazor.Bootstrap" Version="1.10.3" />
gvreddy04 commented 9 months ago

@eholman Thank you for using BlazorBootstrap. To help us better understand the issue you're facing, please share a minimal code sample that reproduces the problem. Additionally, please provide a sample reference for the validation message you're expecting, rather than allowing us to make assumptions.

eholman commented 9 months ago

Thanks for the quick reply! Will get back to you in a bit.

eholman commented 9 months ago

So, don't mind the beauty of the code, it does show the issue. Based on the starter template.

I expect that the (in)validation mark doesn't overlap with the clear button of the component. I assume the most logical way to place the items is the (in)validation mark first, then the clear button of the autocomplete?

image

Home.razor

@page "/"
@using Blazored.FluentValidation
@using FluentValidation

<PageTitle>Home</PageTitle>

<h1>Hello, world!</h1>

<EditForm EditContext="_context" OnSubmit="@Callback">
    <FluentValidationValidator @ref="_validator"/>
    <ValidationSummary></ValidationSummary>
    <div class="container">
        <div class="row">
            <div class="col-4 d-flex flex-column justify-content-center">Item</div>
            <div class="col-8">
                <AutoComplete TItem="AutoCompleteItem" @bind-Value="_viewModel.AutoCompleteValue"
                              DataProvider="DataProvider"
                              PropertyName="@nameof(AutoCompleteItem.Name)"
                              Placeholder="Search..."/>
            </div>
        </div>
    </div>
    <button type="submit">Submit</button>
</EditForm>

@code{
    public class MyViewModel
    {
        public string? AutoCompleteValue { get; set; }
    }

    public class AutoCompleteItem(string name)
    {
        public string Name { get; set; } = name;
    }

    MyViewModel _viewModel = new();
    private EditContext? _context;
    private ValidationMessageStore? _validationMessageStore;
    private FluentValidationValidator? _validator;

    /// <inheritdoc />
    protected override void OnInitialized()
    {
        _context = new EditContext(_viewModel);
        _context.SetFieldCssClassProvider(new ValidatedFieldCssProvider());
        _validationMessageStore = new ValidationMessageStore(_context);
        base.OnInitialized();
    }

    private Task<AutoCompleteDataProviderResult<AutoCompleteItem>> DataProvider(AutoCompleteDataProviderRequest<AutoCompleteItem> request)
    {
        var customers = new List<AutoCompleteItem>
            {
                new("Item 1"), new("Item 2"), new("Item 3")
            };

        return Task.FromResult(new AutoCompleteDataProviderResult<AutoCompleteItem> { Data = customers, TotalCount = 10 });
    }

    public class ValidatedFieldCssProvider : FieldCssClassProvider
    {
        public override string GetFieldCssClass(EditContext editContext, in FieldIdentifier fieldIdentifier)
        {
            var isValid = !editContext.GetValidationMessages(fieldIdentifier).Any();
            if (editContext.IsModified(fieldIdentifier))
            {
                return isValid ? "is-valid" : "is-invalid";
            }
            else
            {
                return isValid ? "" : "is-invalid";
            }
        }
    }
    private Task Callback(EditContext obj)
    {
        _validator!.Validate();
        return Task.CompletedTask;
    }

    public class MyViewModelValidator : AbstractValidator<MyViewModel>
    {
        /// <inheritdoc />
        public MyViewModelValidator()
        {
            RuleFor(model => model.AutoCompleteValue).NotEmpty();
        }
    }
}

csproj:

      <PackageReference Include="Blazor.Bootstrap" Version="1.10.4" />
      <PackageReference Include="Blazored.FluentValidation" Version="2.1.0" />
      <PackageReference Include="FluentValidation" Version="11.8.1" />
gvreddy04 commented 9 months ago

@eholman I'll take a look at your sample code. I have a question: Did you check the AutoComplete validation on our demos website? Please let me know if this information is helpful.

Screenshot:

image

Link: https://demos.blazorbootstrap.com/autocomplete#validations

Code:

@using System.ComponentModel.DataAnnotations

<style>
    .valid.modified:not([type=checkbox]) {
        outline: 1px solid #26b050;
    }

    .invalid {
        outline: 1px solid red;
    }

    .validation-message {
        color: red;
    }
</style>

<EditForm EditContext="@_editContext" OnValidSubmit="HandleOnValidSubmit">
    <DataAnnotationsValidator />

    <div class="form-group row mb-2">
        <label class="col-md-2 col-form-label">Customer:</label>
        <div class="col-md-10">
            <AutoComplete @bind-Value="customerAddress.CustomerName"
                          TItem="Customer2"
                          DataProvider="CustomersDataProvider"
                          PropertyName="CustomerName"
                          Placeholder="Search a customer..."
                          OnChanged="(Customer2 customer) => OnAutoCompleteChanged(customer)" />
            <ValidationMessage For="@(() => customerAddress.CustomerName)" />
        </div>
    </div>

    <div class="form-group row mb-3">
        <label class="col-md-2 col-form-label">Address:</label>
        <div class="col-md-10">
            <InputText class="form-control" @bind-Value="customerAddress.Address" />
            <ValidationMessage For="@(() => customerAddress.Address)" />
        </div>
    </div>

    <div class="row">
        <div class="col-md-12 text-right">
            <button type="submit" class="btn btn-success float-right">Submit</button>
        </div>
    </div>
</EditForm>

@code {
    private CustomerAddress customerAddress = new();
    private EditContext _editContext;

    [Inject] ICustomerService _customerService { get; set; }

    protected override void OnInitialized()
    {
        _editContext = new EditContext(customerAddress);
        base.OnInitialized();
    }

    public void HandleOnValidSubmit()
    {
        Console.WriteLine($"Customer name is {customerAddress.CustomerName} and address is {customerAddress.Address}");
    }

    private async Task<AutoCompleteDataProviderResult<Customer2>> CustomersDataProvider(AutoCompleteDataProviderRequest<Customer2> request)
    {
        var customers = await _customerService.GetCustomersAsync(request.Filter, request.CancellationToken); // API call
        return await Task.FromResult(new AutoCompleteDataProviderResult<Customer2> { Data = customers, TotalCount = customers.Count() });
    }

    private void OnAutoCompleteChanged(Customer2 customer)
    {
        // TODO: handle your own logic

        // NOTE: do null check
        Console.WriteLine($"'{customer?.CustomerName}' selected.");
    }

    public class CustomerAddress
    {
        [Required]
        public string CustomerName { get; set; }

        [Required]
        public string Address { get; set; }
    }
}
eholman commented 9 months ago

Thanks; wasn't aware of the example in the docs. It looks good though!

But, this is an existing project, which uses the default Bootstrap validation style implementation via with the FieldCssClassProvider.