Fluxor.Undo 1.0.1

dotnet add package Fluxor.Undo --version 1.0.1                
NuGet\Install-Package Fluxor.Undo -Version 1.0.1                
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="Fluxor.Undo" Version="1.0.1" />                
For projects that support PackageReference, copy this XML node into the project file to reference the package.
paket add Fluxor.Undo --version 1.0.1                
#r "nuget: Fluxor.Undo, 1.0.1"                
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
// Install Fluxor.Undo as a Cake Addin
#addin nuget:?package=Fluxor.Undo&version=1.0.1

// Install Fluxor.Undo as a Cake Tool
#tool nuget:?package=Fluxor.Undo&version=1.0.1                

Fluxor.Undo

Icon

Fluxor.Undo is a library to add redo/undo functionality to Fluxor.

Azure DevOps builds (branch)

Demo

Goal

The aim of Fluxor.Undo is removing the hassle of implementing your own undo/redo functionality. The idea is inspired by redux-undo although the implementation is completely different.

Installation

You can download the latest release / pre-release NuGet package from nuget:

Package:
Fluxor.Undo Fluxor.Undo on NuGet Fluxor.Undo downloads on NuGet

Setup undoable state

Steps to change your regular state to an undoable state:

1) Change your Feature to an Undoable feature

Change your state with FeatureStateAtrribute

[FeatureState(Name = "Counter", CreateInitialStateMethodName = nameof(CreateInitialState))]
public sealed record CounterState(int ClickCount)
{
    public static CounterState CreateInitialState()
        => new(0);
}

to

public sealed record CounterState(int ClickCount);

[FeatureState(Name = "Counter", CreateInitialStateMethodName = nameof(CreateInitialState))]
public sealed record UndoableCounterState : Undoable<UndoableCounterState, CounterState>
{
    public static UndoableCounterState CreateInitialState()
        => new() { Present = new(0) };
}

// Or when net6:
public sealed record CounterState(int ClickCount);

[FeatureState(Name = "Counter", CreateInitialStateMethodName = nameof(CreateInitialState))]
public sealed record UndoableCounterState(CounterState Present) : Undoable<UndoableCounterState, CounterState>(Present)
{
    public static UndoableCounterState CreateInitialState()
        => new(new CounterState(0));
};

When using Feature baseclass change

public sealed record CounterState(int ClickCount);

public sealed class CounterFeature : Feature<CounterState>
{
    public override string GetName()
        => "Counter";

    protected override CounterState GetInitialState()
        => new(0);
}

to

public sealed record CounterState(int ClickCount);
public sealed record UndoableCounterState : Undoable<UndoableCounterState, CounterState>;

public sealed class UndoableCounterFeature : Feature<UndoableCounterState>
{
    public override string GetName()
        => "Counter";

    protected override UndoableCounterState GetInitialState()
        => new() { Present = new(0) };
}

// Or when net6:
public sealed record CounterState(int ClickCount);
public sealed record UndoableCounterState(CounterState Present) : Undoable<UndoableCounterState, CounterState>(Present);

public sealed class UndoableCounterFeature : Feature<UndoableCounterState>
{
    public override string GetName()
        => "Counter";

    protected override UndoableCounterState GetInitialState()
        => new(new CounterState(0));
}

2) Update your reducer Change your reducer from

public static class Reducers
{
    [ReducerMethod]
    public static CounterState ReduceIncrementCounterAction(CounterState state, IncrementCounterAction action)
        => state with
        {
            ClickCount = state.ClickCount + action.Amount,
        };
}

to

public class Reducers : UndoableReducers<UndoableCounterState>
{
    [ReducerMethod]
    public static UndoableCounterState ReduceIncrementCounterAction(UndoableCounterState state, IncrementCounterAction action)
        => state.WithNewPresent(p => p with
        {
            ClickCount = p.ClickCount + action.Amount,
        });
}

3) Update your injected IState properties Change setting of properties in your Razor pages from

    [Inject]
    private IState<CounterState> CounterState { get; set; } = null!;

to

    [Inject]
    private IState<UndoableCounterState> UndoableCounterState { get; set; } = null!;

4) Update usages of your state Change usage in your Razor pages from

<p>Current count: @CounterState.Value.ClickCount</p>

to

<p>Current count: @UndoableCounterState.Value.Present.ClickCount</p>

5) Add some navigation buttons

<button class="btn btn-secondary" @onclick=@(() => Dispatcher.Dispatch(new UndoAllAction<UndoableCounterState>())) disabled="@UndoableCounterState.Value.HasNoPast">&lt;&lt;</button>
<button class="btn btn-secondary" @onclick=@(() => Dispatcher.Dispatch(new UndoAction<UndoableCounterState>())) disabled="@UndoableCounterState.Value.HasNoPast">&lt;</button>
<button class="btn btn-secondary" @onclick=@(() => Dispatcher.Dispatch(new RedoAction<UndoableCounterState>())) disabled="@UndoableCounterState.Value.HasNoFuture">&gt;</button>
<button class="btn btn-secondary" @onclick=@(() => Dispatcher.Dispatch(new RedoAllAction<UndoableCounterState>())) disabled="@UndoableCounterState.Value.HasNoFuture">&gt;&gt;</button>

Also see example project in solution. Here both the Fluxor counter as Fluxor.Undo counter are implemented.

Available undo/redo actions

Dispatcher.Dispatch(new UndoAction<T>()); // undo the last action
Dispatcher.Dispatch(new UndoAllAction<T>()); // undo all actions

Dispatcher.Dispatch(new RedoAction<T>()); // redo the last action
Dispatcher.Dispatch(new RedoAllAction<T>()); // redo all actions

Dispatcher.Dispatch(new JumpAction<T>(-2)); // undo 2 steps
Dispatcher.Dispatch(new JumpAction<T>(5)); // redo 5 steps

Helper methods on state

public sealed record CounterState(int ClickCount);
public sealed record UndoableCounterState : Undoable<UndoableCounterState, CounterState>;
var state = new UndoableCounterState { Present = new CounterState { ClickCount = 0}};

var newState1 = state.WithNewPresent(p => p with { ClickCount = p.ClickCount + 1 }); // Moves current present to past and sets new present
var newState2 = state.WithNewPresent(new CounterState { ClickCount = 1}); // Moves current present to past and sets new present
var newState3 = state.WithInlineEditedPresent(p => p with { ClickCount = p.ClickCount + 1 }); // Does NOT move current present to past; it will replace current present
var newState4 = state.WithInlineEditedPresent(new CounterState { ClickCount = 1}); // Does NOT move current present to past; it will replace current present

Tips

  1. When you are allowing undo/redo, the undo/redo is done on client side. So make sure that user knows that undo-ing does not alter data on server. There is a basic implementation in the example project in solution; page: Fluxor.Undo (Persist). Can be used as inspiration! Demo
  2. If you are using net6; upgrade to net7 so you can use the parameterless ctors and use the required properties 😃.

Release notes

See the Releases page.

Versioning

Fluxor.Undo follows Semantic Versioning 2.0.0 for the releases published to nuget.org.

Product Compatible and additional computed target framework versions.
.NET net6.0 is compatible.  net6.0-android was computed.  net6.0-ios was computed.  net6.0-maccatalyst was computed.  net6.0-macos was computed.  net6.0-tvos was computed.  net6.0-windows was computed.  net7.0 is compatible.  net7.0-android was computed.  net7.0-ios was computed.  net7.0-maccatalyst was computed.  net7.0-macos was computed.  net7.0-tvos was computed.  net7.0-windows was computed.  net8.0 was computed.  net8.0-android was computed.  net8.0-browser was computed.  net8.0-ios was computed.  net8.0-maccatalyst was computed.  net8.0-macos was computed.  net8.0-tvos was computed.  net8.0-windows was computed. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.

NuGet packages

This package is not used by any NuGet packages.

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last updated
1.0.1 442 12/30/2022
1.0.1-prerelease-2022122820... 151 12/28/2022
1.0.0 525 12/28/2022
1.0.0-prerelease-2022122815... 151 12/28/2022