Reputation: 1495
I've written a PIN component that is composed of 4 input fields (as it will be re-used in a few places)
<input class="pinBox" type="password" maxlength="1" size="1" onkeypress='return event.charCode >= 48 && event.charCode <= 57' required @bind="@_pinOne"/>
<input class="pinBox" type="password" maxlength="1" size="1" onkeypress='return event.charCode >= 48 && event.charCode <= 57' required @bind="@_pinTwo"/>
<input class="pinBox" type="password" maxlength="1" size="1" onkeypress='return event.charCode >= 48 && event.charCode <= 57' required @bind="@_pinThree"/>
<input class="pinBox" type="password" maxlength="1" size="1" onkeypress='return event.charCode >= 48 && event.charCode <= 57' required @bind="@_pinFour" @oninput="Completion"/>
@code{
[Parameter]
public EventCallback<string> Completed { get; set; }
private string _pinOne;
private string _pinTwo;
private string _pinThree;
private string _pinFour;
private void Completion(ChangeEventArgs e)
{
_pinFour = e.Value.ToString();
Completed.InvokeAsync(_pinOne + _pinTwo + _pinThree + _pinFour);
}
}
I've then created another component that uses 2 of these PIN input components
<PinComponent Completed="@PinCompleted"></PinComponent>
<PinComponent Completed="@ConfirmationPinCompleted"></PinComponent>
@code {
private string _pin;
private string _confirmationPin;
private bool _valid = false;
private void PinCompleted(string pin)
{
_pin = pin;
}
private void ConfirmationPinCompleted(string pin)
{
_confirmationPin = pin;
if (_pin.Equals(_confirmationPin))
{
_valid = true;
}
}
}
Is it possible to use Blazor's ValidationMessage to ensure these 2 components share the same value?
Upvotes: 2
Views: 623
Reputation: 1495
Okay, I decided to use FluentValidation since for some reason I couldn't get custom attributes (or the built-in Compare
attribute) to work whatsoever
PinComponent.Razor
<input class="pinBox" type="password" maxlength="1" size="1" onkeypress='return event.charCode >= 48 && event.charCode <= 57' required @bind="@_pinOne"/>
<input class="pinBox" type="password" maxlength="1" size="1" onkeypress='return event.charCode >= 48 && event.charCode <= 57' required @bind="@_pinTwo"/>
<input class="pinBox" type="password" maxlength="1" size="1" onkeypress='return event.charCode >= 48 && event.charCode <= 57' required @bind="@_pinThree"/>
<input class="pinBox" type="password" maxlength="1" size="1" onkeypress='return event.charCode >= 48 && event.charCode <= 57' required @bind="@_pinFour" @oninput="Completion"/>
@code{
private string _value;
[Parameter]
public string Value
{
get { return Value; }
set
{
if (string.IsNullOrWhiteSpace(value))
{
_pinOne = null;
_pinTwo = null;
_pinThree = null;
_pinFour = null;
}
_value = value;
}
}
[Parameter]
public EventCallback<string> ValueChanged { get; set; }
private string _pinOne;
private string _pinTwo;
private string _pinThree;
private string _pinFour;
private void Completion(ChangeEventArgs e)
{
_pinFour = e.Value.ToString();
ValueChanged.InvokeAsync(_pinOne + _pinTwo + _pinThree + _pinFour);
}
}
PinConfirmationComponent.Razor
@using Application.Validation
<EditForm Model="@_model" OnValidSubmit="@OnValidSubmit" OnInvalidSubmit="@OnInvalidSubmit">
<div class="pinContainer">
<PinComponent @bind-Value="_model.Pin"></PinComponent>
<PinComponent @bind-Value="_model.PinConfirmation"></PinComponent>
</div>
<FluentValidationValidator />
<ValidationSummary />
<input id="btnSubmit" class="btn btnFont" type="submit" value="Register PIN" style="margin-top: 5px;" />
</EditForm>
@code {
private PinModel _model = new PinModel();
void OnValidSubmit()
{
}
void OnInvalidSubmit()
{
_model.Pin = null;
_model.PinConfirmation = null;
StateHasChanged();
}
}
PinModel.cs
public class PinModel
{
public string Pin { get; set; }
public string PinConfirmation { get; set; }
}
Following this example repo I've used FluentValidation
EditContextFluentValidationExtensions.cs
public static class EditContextFluentValidationExtensions
{
public static EditContext AddFluentValidation(this EditContext editContext)
{
if (editContext == null)
{
throw new ArgumentNullException(nameof(editContext));
}
var messages = new ValidationMessageStore(editContext);
editContext.OnValidationRequested +=
(sender, eventArgs) => ValidateModel((EditContext)sender, messages);
editContext.OnFieldChanged +=
(sender, eventArgs) => ValidateField(editContext, messages, eventArgs.FieldIdentifier);
return editContext;
}
private static void ValidateModel(EditContext editContext, ValidationMessageStore messages)
{
var validator = GetValidatorForModel(editContext.Model);
if (validator == null)
return;
var validationResults = validator.Validate(editContext.Model);
messages.Clear();
foreach (var validationResult in validationResults.Errors)
{
messages.Add(editContext.Field(validationResult.PropertyName), validationResult.ErrorMessage);
}
editContext.NotifyValidationStateChanged();
}
private static void ValidateField(EditContext editContext, ValidationMessageStore messages, in FieldIdentifier fieldIdentifier)
{
var properties = new[] { fieldIdentifier.FieldName };
var context = new ValidationContext(fieldIdentifier.Model, new PropertyChain(), new MemberNameValidatorSelector(properties));
var validator = GetValidatorForModel(fieldIdentifier.Model);
if (validator == null)
return;
var validationResults = validator.Validate(context);
messages.Clear(fieldIdentifier);
foreach (var validationResult in validationResults.Errors)
{
messages.Add(editContext.Field(validationResult.PropertyName), validationResult.ErrorMessage);
}
editContext.NotifyValidationStateChanged();
}
private static IValidator GetValidatorForModel(object model)
{
var abstractValidatorType = typeof(AbstractValidator<>).MakeGenericType(model.GetType());
var modelValidatorType = Assembly.GetExecutingAssembly().GetTypes().FirstOrDefault(t => t.IsSubclassOf(abstractValidatorType));
if (modelValidatorType == null)
return null;
var modelValidatorInstance = (IValidator)Activator.CreateInstance(modelValidatorType);
return modelValidatorInstance;
}
}
FluentValidationValidator.cs
public class FluentValidationValidator : ComponentBase
{
[CascadingParameter]
EditContext CurrentEditContext { get; set; }
protected override void OnInitialized()
{
if (CurrentEditContext == null)
{
throw new InvalidOperationException($"{nameof(FluentValidationValidator)} requires a cascading " +
$"parameter of type {nameof(EditContext)}. For example, you can use {nameof(FluentValidationValidator)} " +
$"inside an {nameof(EditForm)}.");
}
CurrentEditContext.AddFluentValidation();
}
}
PinValidator.cs
public class PinValidator : AbstractValidator<PinModel>
{
public PinValidator()
{
RuleFor(p => p.Pin).NotNull().Matches("^[0-9]{4}$");
RuleFor(p => p).Must(PinsAreSame)
.WithMessage("PINs must be the same");
}
private bool PinsAreSame(PinModel pinModel)
{
return (pinModel.Pin.Equals(pinModel.PinConfirmation));
}
}
Upvotes: 1
Reputation: 5560
Pass value and result of validation to your PinComponent
and make that component display validation errors.
<PinComponent Completed="@PinCompleted"></PinComponent>
<PinComponent Completed="@ConfirmationPinCompleted" ValidationMessage="@validationMessage"></PinComponent>
@code {
private string _pin;
private string _confirmationPin;
private bool _valid = false;
private string ValidationMessage => _valid ? string.Empty : "PIN does not match";
private void PinCompleted(string pin)
{
_pin = pin;
}
private void ConfirmationPinCompleted(string pin)
{
_confirmationPin = pin;
if (_pin.Equals(_confirmationPin))
{
_valid = true;
}
}
}
if you want to utilize Blazor Forms validation
class PinModel
{
[Required]
public string Pin {get;set;}
[Required]
[PinTheSame]
public string PinConfirmation {get;set;}
}
[AttributeUsage(AttributeTargets.Property, AllowMultiple = false)]
public class PinTheSameAttirbute: ValidationAttribute
{
protected override ValidationResult IsValid(object value, ValidationContext validationContext)
{
if (value == null) return new ValidationResult("A pin is required.");
// Make sure you change PinModel to whatever the actual name is
if ((validationContext.ObjectType.Name != "PinModel")
return new ValidationResult("This attribute is being used incorrectly.");
if (((PinModel)validationContext.ObjectInstance).ConfirmPin != value.ToString())
return new ValidationResult("Pins must match.");
return ValidationResult.Success;
}
}
and pass Values as model
<EditForm Model="@Model">
<PinComponent Value="@Pin"></PinComponent>
<PinComponent Value="@ConfirmationPin"></PinComponent>
</EditForm>
Last approach not fully complete, but should give you idea about the direction.
Upvotes: 1