Fluxor.Undo is a library to add redo/undo functionality to Fluxor.
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.
You can download the latest release / pre-release NuGet package from nuget:
Package: | ||
---|---|---|
Fluxor.Undo |
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"><<</button>
<button class="btn btn-secondary" @onclick=@(() => Dispatcher.Dispatch(new UndoAction<UndoableCounterState>())) disabled="@UndoableCounterState.Value.HasNoPast"><</button>
<button class="btn btn-secondary" @onclick=@(() => Dispatcher.Dispatch(new RedoAction<UndoableCounterState>())) disabled="@UndoableCounterState.Value.HasNoFuture">></button>
<button class="btn btn-secondary" @onclick=@(() => Dispatcher.Dispatch(new RedoAllAction<UndoableCounterState>())) disabled="@UndoableCounterState.Value.HasNoFuture">>></button>
Also see example project in solution. Here both the Fluxor counter as Fluxor.Undo counter are implemented.
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
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
- 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!
- If you are using net6; upgrade to net7 so you can use the parameterless ctors and use the required properties :).
See the Releases page.
Fluxor.Undo follows Semantic Versioning 2.0.0 for the releases published to nuget.org.