mrpmorris / Fluxor

Fluxor is a zero boilerplate Flux/Redux library for Microsoft .NET and Blazor.
MIT License
1.24k stars 141 forks source link

Validating my state in EditForm #279

Closed andrewtsybulia closed 2 years ago

andrewtsybulia commented 2 years ago

Hello @mrpmorris, I do like Fluxor and I want to integrate it into my application but I've faced an issue. The problem is that I can't use two-way data binding in my form cuz my state is immutable and I can't validate it. I saw for example this guide https://dev.to/mr_eking/advanced-blazor-state-management-using-fluxor-part-5-14j2 and the author proposes to use DTO with public setters. And I saw your comment down below about "avoid having mutable state" and I agree with it. I wasted a lot of time validating my form manually using DataAnnotation + EditContext, I created a field with a public setter to store property from my state, etc. The best solution that I found for now it's using FluentValidation and validate my model manually. I am pretty new to blazor so maybe you can suggest me a better solution? I created a sample project to illustrate an issue. This is the model that I store in my state:

public record AssetsViewModel
{
    public int Id { get; init; }
    public IEnumerable<AssetModel> AssetsList { get; init; }
}

public record AssetModel
{
    public int Id { get; init; }
    public string? Name { get; init; }
}

I wasn't able to validate the form using DataAnnotation, so I decided to use FluentValidation, here are my rules for the model:

public class AssetValidation : AbstractValidator<AssetModel>
{
    public AssetValidation()
    {
        RuleFor(p => p.Name)
            .NotEmpty().WithMessage("You must enter your first name")
            .MaximumLength(30).WithMessage("Name can be max 30 characters long.");
    }
}

My parent:

<h1>Assets</h1>

@foreach (var asset in State.Value.Settings.AssetsList)
{
    <AssetChildComponent Asset=@asset
                         OnNameChanged=@HandleNameChanged />
}

@code {
    [Inject] protected IState<AssetsState> State { get; set; }
    [Inject] protected IDispatcher Dispatcher { get; set; }

    protected void HandleNameChanged((AssetModel asset, string newName) asset)
    {
        var assets = UpdateName(asset.newName, asset.asset);
        Dispatcher.Dispatch(new AssetsUpdateAction(assets));
    }

    private IEnumerable<AssetModel> UpdateName(string name, AssetModel asset)
    {
        var updatedAssets = new List<AssetModel>();
        foreach (var originalAsset in State.Value.Settings.AssetsList)
        {
            AssetModel newAsset = null;
            if (asset.Id == originalAsset.Id)
            {
                newAsset = originalAsset with { Name = name };
            }
            updatedAssets.Add(newAsset ?? originalAsset);
        }
        return updatedAssets;
    }
}

My child:

InputField is my custom component to handle OnChange + OnInput

<input value="@Value"
       @oninput=@OnInput
       @onchange=@OnChange />
<EditForm Model="Asset">
    <InputField Value=@Asset.Name
                OnChange=@HandleNameChanged
                OnInput=@IsNameUnique/>
</EditForm>

@code {
    private AssetValidation _assetValidation = new();
    [Inject] protected IState<AssetsState> State { get; set; }
    [Inject] protected IDispatcher Dispatcher { get; set; }
    [Parameter] public EventCallback<(AssetModel asset, string newName)> OnNameChanged { get; set; }
    [Parameter] public AssetModel Asset { get; set; }

    protected override void OnParametersSet()
    {
        base.OnParametersSet();
        _assetValidation.Validate(Asset);
    }

    protected async Task HandleNameChanged(ChangeEventArgs e) =>
        await OnNameChanged.InvokeAsync((Asset, e.Value.ToString())); //It calls reducer in parent

    protected Task<bool> IsNameUnique(ChangeEventArgs e)
    {
        //Check if name is unique
    }
}

Best regards.

mrpmorris commented 2 years ago

I do this

  1. Post action GetCustomerForEditAction
  2. Effect fetches a dto from the server
  3. Effect dispatched GetCustomerForEditActionResult containing the dto
  4. Any state that holds Customer info reduces values from the dto to ensure they don't have stale state.
  5. The component that dispatched the Action has an ActionSubscriber injected
  6. When it triggers its event you store a reference to the dto in your component for EditForm to work on... Do not store the dto itself into any feature state.
  7. User edits
  8. User saves, which dispatches a SaveCustomerAction(dto)
  9. Effect talks to the server
  10. Effect dispatches a SaveCustomerActionResult(dto sent back from server)
  11. The values of that dto are reduced into state, not the dto itself.