Skip to content

Pjotrtje/Fluxor.Undo

Repository files navigation

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.