p3tch
p3tch

Reputation: 1495

Validate 2 components have the same value

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

Answers (2)

p3tch
p3tch

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

codevision
codevision

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

Related Questions