Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Adds FieldIdentifier parameter to FluentInputBase #2114

Merged
merged 2 commits into from
May 28, 2024

Conversation

chris-gander
Copy link
Contributor

Pull Request

📖 Description

This PR adds a new parameter Field (of type FieldIdentifier) to FluentInputBase. I'm working on a system that allows users to create their own dynamic forms, this means my model contains a list of fields and each of those has a base class where the value is stored as an object? and inheriting classes override this with a specific type. When passing to input components the Value parameter gets cast to the correct type, but the FluentValidationMessage component can only be passed the object? version due to FieldIdentifiers not supporting the UnaryExpression that is created by casting. This means that the FluentInputBase has a different internal FieldIdentifier due to the unboxing, and means the validation message shows correctly when the value is invalid but the field stays showing as valid. By allowing the FieldIdentifier to be manually set this limitation can be overcome.

In normal statically typed models this additional parameter is not needed as everything will already work. However for dynamic use cases like mine, where design-time accessors are not possible, a FieldIdentifier works best. ValueExpression will also not work in this case as it also requires statically typed accessors.

#1489 added a similar feature for FluentValidationMessage so I largely used the same code.

Example

This is a basic example of how this works, it uses the FluentValidation library to simplify validating the dynamic model. This example is simpler than my use case but demonstrates the same issue:

<EditForm Model="FormState">
    <FluentValidationValidator />
    @foreach(var field in FormState.Fields)
    {
        @if(field.FieldType == "int")
        {
            <FluentNumberField Id="@field.FieldName"
                TValue="int?"
                Label="@field.FieldName" 
                Required="true"
                Placeholder="Enter a number"
                Value="(int?)field.Value"
                Field="@(FieldIdentifier.Create(() => field.Value))"
                ValueChanged="@(value => OnValueChanged(field, value))" />
        } else if (field.FieldType == "string")
        {
            <FluentTextField Id="@field.FieldName"
                Label="@field.FieldName"
                Required="true"
                Placeholder="Enter a string"
                Value="@((string?)field.Value)"
                Field="@(FieldIdentifier.Create(() => field.Value))"
                ValueChanged="@(value => OnValueChanged(field, value))" />
        }
        <FluentValidationMessage For="@(() => field.Value)" />
    }
</EditForm>

@code {
    private MyFormState FormState = new()
    {
        Fields = new()
        {
            new() {FieldType = "string", FieldName = "StringField", Value = ""},
            new() {FieldType = "int", FieldName = "IntField", Value = 1}
        }
    };

    private void OnValueChanged(MyFormField field, object? value)
    {
        field.Value = value;
    }

    public class MyFormState
    {
        public List<MyFormField> Fields { get; set; } = [];
    }

    public class MyFormField
    {
        public string FieldType { get; set; } = "string";
        public required string FieldName { get; set; }
        public object? Value { get; set; }
    }

    public class MyFormStateValidator : AbstractValidator<MyFormState>
    {
        public MyFormStateValidator()
        {
            RuleForEach(x => x.Fields).SetValidator(new MyFormFieldValidator());
        }
    }

    public class MyFormFieldValidator : AbstractValidator<MyFormField>
    {
        public MyFormFieldValidator()
        {
            RuleFor(x => (int?)x.Value).GreaterThan(2).WithName(x => x.FieldName).When(x => x.FieldType == "int");
            RuleFor(x => (string?)x.Value).NotEmpty().WithName(x => x.FieldName).When(x => x.FieldType == "string");
        }
    }
}

Without using Field and invalid the fields are still marked as valid
image

When using Field and invalid the fields are now correctly marked as invalid
image

🎫 Issues

No issues currently reported, but for this niche use case it is required

👩‍💻 Reviewer Notes

📑 Test Plan

No tests affected by this extra parameter, but all existing tests for FluentInputBase components pass. I have tested this code by creating custom versions of the number and text inputs in my project and verifying it works in both the above example and a more complex scenario, as well as making sure existing forms that don't use this new parameter still work.

✅ Checklist

General

  • I have added tests for my changes.
  • I have tested my changes.
  • I have updated the project documentation to reflect my changes.
  • I have read the CONTRIBUTING documentation and followed the standards for this project.

Component-specific

  • I have added a new component
  • I have added Unit Tests for my new compontent
  • I have modified an existing component
  • I have validated the Unit Tests for an existing component

⏭ Next Steps

@chris-gander
Copy link
Contributor Author

@microsoft-github-policy-service agree

@dvoituron dvoituron merged commit 9757396 into microsoft:dev May 28, 2024
2 checks passed
@chris-gander chris-gander deleted the feat-fluentinputbase-field branch May 28, 2024 17:16
vnbaaij added a commit that referenced this pull request Jun 12, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants