Validação do lado do cliente usando Blazor
Eu tenho um EditForm que leva um AnimalList como um modelo que tem uma lista de Animais. Estou usando anotações de dados nos objetos animais. No front-end, tenho a capacidade de exibir a lista de nomes de animais como uma combinação suspensa + entrada de texto e adicionar objetos a essa lista. Portanto, você selecionaria um nome de animal e atribuiria um nome amigável a ele. ou seja, seleciono "Girafa" e dou a ela um nome amigável de girafa, como "Brian". Em seguida, clicar em adicionar mais adicionaria um padrão em branco à lista e poderia continuar etc. Estou tentando usar anotações de dados para obter alguma validação do lado do cliente no campo de entrada de texto em cada linha, no sentido de que se eu deixar o nome amigável em branco, eu deveria recebe uma mensagem dizendo que o campo é obrigatório. De preferência depois de ive clicado dentro do campo de texto e à esquerda (onBlur eu acho?).
Acho que isso deveria ser padrão, mas acho que usar uma lista está dificultando a validação padrão.
Eu obtenho validação se eu inserir algum texto, clicar fora e, em seguida, excluir o texto e clicar fora, o que não é o ideal. O mais importante é que, quando clico em enviar, sinto que o formulário deve ser validado e não enviado, porque deveria haver mensagens, mas isso também não funciona e o envio é muito bom. Já tentei context.validate e seus retornos são verdadeiros?
Devo examinar algum tipo de validação personalizada aqui ou isso deve ser possível?
AddAnimals.Razor Code:
<Css />
<h3>Add your animals</h3>
<EditForm Class="login-form" Model="@AnimalList" OnValidSubmit="SaveAnimals">
<DataAnnotationsValidator/>
@if (!string.IsNullOrEmpty(Error))
{
<Notification Type="@NotificationType.Error" Message="@Error" />
}
<div class="form-group">
<div class="form-row">
<div class="col-6">
<label for="Select">Select Animal</label>
</div>
<div class="col">
<label>Animal Name</label>
</div>
</div>
<ul>
@foreach (var animalSelection in AnimalList.SelectedAnimals)
{
<li>
<div class="form-row">
<div class="col-6">
<select class="form-control" @bind="animalSelection.AnimalName">
<option value="" disabled selected hidden>Select...</option>
@foreach (var name in AnimalNames)
{
<option value="@name">@name</option>
}
</select>
</div>
<div class="col">
<InputText class="form-control" @bind-Value="animalSelection.FriendlyName" />
<ValidationMessage For="@(() => animalSelection.FriendlyName)" />
</div>
</div>
</li>
}
</ul>
</div>
<div class="form-group">
<button type="button" class="btn" @onclick="AddMoreAnimals">+ Add More</button>
</div>
<div class="form-group">
<button type="submit" class="btn btn-primary">Continue</button>
</div>
</EditForm>
@code {
[Parameter]
public AnimalList AnimalList { get; set; } = new AnimalList();
[Parameter]
public List<string> AnimalNames { get; set; } = new List<string>();
public string Error { get; set; }
protected override async Task OnAfterRenderAsync(bool firstRender)
{
if (firstRender)
{
AnimalNames = AnimalsService.GetAnimals();
await GetAnimalList();
if (AnimalList.SelectedAnimals.Count < 1)
{
AddMoreAnimals();
}
StateHasChanged();
}
}
private async Task GetAnimalList()
{
AnimalList.SelectedAnimals = await AnimalsService.GetAnimals();
}
private void AddMoreAnimals()
{
AnimalList.AddDefault();
}
private async void SaveAnimals(EditContext context)
{
AnimalList.TrimList();
foreach (var animal in AnimalsList.SelectedAnimals)
{
var response = await AnimalsService.AddAnimal(animal);
if (response.IsError)
{
Error = response.Error;
StateHasChanged();
break;
}
else
{
Error = string.Empty;
}
}
}
}
AnimalsList
public class AnimalList
{
public List<Animal> SelectedAnimals { get; set; }
public AnimalList()
{
SelectedAnimals = new List<Animal>();
}
public void AddDefault()
{
SelectedAnimals.Add(new Animal());
}
public void TrimList()
{
//Trim away possible unused rows
SelectedAnimals.RemoveAll(
item => string.IsNullOrEmpty(item.FriendlyName) &&
string.IsNullOrEmpty(item.AnimalName));
}
}
Animal
public class Animal
{
[Required(ErrorMessage = "Animal selection is required")]
public string AnimalName { get; set; }
[Required(ErrorMessage = "You must enter a name for your Animal")]
[DisplayFormat(ConvertEmptyStringToNull = true)]
public string FriendlyName { get; set; }
}
Respostas
Eu quebrei isso em uma lista de formulários como sugeri.
Nota: usei code behind pages.
AnimalsPage.razor
@page "/animals"
<h3>Add your animals</h3>
<CascadingValue Value="this">
@foreach (var animal in this.animals)
{
<AnimalEditForm @key="animal" Animal="animal" />
}
</CascadingValue>
<button class="btn mr-1" @onclick="AddAnimalClicked">Add Animal</button>
@if (lastValidateResult)
{
<button class="btn btn-success" @onclick="SaveAllClicked">Save All</button>
}
else
{
<button class="btn btn-primary" @onclick="ContinueClicked">Continue</button>
}
AnimalsPage.razor.cs
public partial class AnimalsPage
{
private void AddAnimalClicked(MouseEventArgs mouseEventArgs)
{
this.animals.Add(new Animal());
this.lastValidateResult = false;
}
private void ContinueClicked(MouseEventArgs mouseEventArgs)
{
//Your "Trim" function
List<Animal> emptyAnimals = this.animals.Where(a => a.AnimalName == default && a.AnimalType == default).ToList();
emptyAnimals.ForEach(a =>
{
this.animals.Remove(a);
this.animalForms.Remove(a);
this.animalValidationResults.Remove(a);
});
this.animalForms.Values.ToList().ForEach(a => a.ShowValidations(true));
StateHasChanged();
}
private void SaveAllClicked()
{
//.....
}
internal void ChildChangedState(Animal animal, bool result)
{
bool prev;
if (!this.animalValidationResults.ContainsKey(animal))
{
prev = false;
this.animalValidationResults[animal] = prev;
}
else
{
prev = this.animalValidationResults[animal];
if (prev != result)
{
this.animalValidationResults[animal] = result;
this.lastValidateResult = this.animalValidationResults.Values.All(a => a);
StateHasChanged();
}
}
}
internal Dictionary<Animal, AnimalEditForm> animalForms = new Dictionary<Animal, AnimalEditForm>();
private readonly Dictionary<Animal, bool> animalValidationResults = new Dictionary<Animal, bool>();
private readonly List<Animal> animals = new List<Animal>();
private bool lastValidateResult = false;
}
AnimalEditForm.razor
<EditForm Model="Animal">
<DataAnnotationsValidator />
@{if (validatorVisible) { { Validate(context); } } }
<div class="form-row mb-1">
<div class="col">
<InputSelect class="form-control" @bind-Value="Animal.AnimalType">
<option value="" selected>Select...</option>
@foreach (var animalType in animalTypes)
{
<option value=@animalType>@animalType</option>
}
</InputSelect>
<ValidationMessage For="@(() => Animal.AnimalType)" />
</div>
<div class="col">
<InputText class="form-control" @bind-Value="Animal.AnimalName" />
<ValidationMessage For="@(() => Animal.AnimalName)" />
</div>
</div>
</EditForm>
AnimalEditForm.razor.cs
public partial class AnimalEditForm
{
[Parameter]
public Animal Animal { get; set; }
[CascadingParameter]
public AnimalsPage AnimalsPage { get; set; }
protected override void OnAfterRender(bool firstRender)
{
if (firstRender)
{
AnimalsPage.animalForms[Animal] = this;
}
base.OnAfterRender(firstRender);
}
private IReadOnlyList<string> animalTypes => new[]
{
"Eagle",
"Elephant",
"Giraffe",
"Hippopotamus",
"Horse",
"Koala",
"Zebra"
};
public bool LastValidateResult { get; private set; }
private bool validatorVisible = false;
public void ShowValidations(bool show) => this.validatorVisible = show;
public void Validate(EditContext editContext)
{
LastValidateResult = editContext.Validate();
AnimalsPage.ChildChangedState(Animal, LastValidateResult);
}
}
Não use OnValidSubmitnem OnInvalidSubmit, apenasOnSubmit
<EditForm Class="login-form" Model="@AnimalList" OnSubmit="SaveAnimals">
Em SaveAnimal, verifique a lista
private async void SaveAnimals(EditContext context)
{
bool valid = context.Validate();
AnimalList.SelectedAnimals.ForEach(x =>
{
var field=FieldIdentifier.Create((() => x.FriendlyName));
context.NotifyFieldChanged(field);
if (valid)
valid = context.GetValidationMessages(field)
.FirstOrDefault(x => !String.IsNullOrEmpty(x)) == null;
});
if (valid)
Console.WriteLine("Save Animals");
}