Angular wrapper around FsmRx, a Finite State Machine built upon RxJS and Typescript.
FsmRx allows developers to organise code into discrete states, each with their own strongly typed dataset. Transitions between these states are governed by an allowlist giving developers more control and visibility over program flow. Transitions also support the lifecycle hooks onLeave, onEnter and onUpdate. These callbacks are supplied data scoped to the relevant transition states and can be used for the implementation of any required build-up and teardown of state-specific interactions, logic, or calls.
FsmRx greatly reduces code complexity, speeding up development time while resulting in easier-to-maintain bug-free code.
Please visit here for a deep dive into all FsmRx has to offer.
- FsmRx: Core FsmRx package
- NgxFsmRxExamples: Storybook Examples of NgFsmRx in use
For full documentation see:
- FsmRx Public API Compodoc Documentation
- NgxFsmRx Public API Compodoc Documentation
- Deep-dive articles with Angular Storybook examples
ng add ngx-fsm-rx
ng generate ngx-fsm-rx:component
ng generate ngx-fsm-rx:service
type States = "foo" | "bar" | "baz";
interface CommonData extends BaseStateData<States> {
commonProperty: string;
}
interface FooData extends CommonData {
state: "foo";
fooProperty: number;
}
interface BarData extends CommonData {
state: "bar";
barProperty: string;
}
interface BazData extends CommonData {
state: "baz";
bazProperty: boolean;
}
type StateData = FooData | BarData | BazData;
type CanLeaveToMap = {
FSMInit: "foo",
foo: "bar",
bar: "foo" | "baz";
baz: "FSMTerminate";
};
interface CanLeaveToMap extends CanLeaveToStatesMap<States> {
FSMInit: "foo",
foo: "bar",
bar: "foo" | "baz";
baz: "FSMTerminate";
}
@Component({
selector: 'app-foo-bar-baz-fsm',
templateUrl: './foo-bar-baz-fsm.component.html',
standalone: true,
imports: [CommonModule],
styleUrls: ['./foo-bar-baz-fsm.component.scss']
})
export class FooBarBazFSM extends FsmRxComponent<States, StateData, CanLeaveToMap>{
public override stateMap: StateMap<States, StateData, CanLeaveToMap> = {
foo: {
canEnterFromStates: { FSMInit: true, bar: true },
canLeaveToStates: { bar: true }
},
bar: {
canEnterFromStates: { foo: true },
canLeaveToStates: { foo: true, baz: true }
},
baz: {
canEnterFromStates: { bar: true },
canLeaveToStates: { FSMTerminate: true }
}
};
}
@Injectable({
providedIn: 'root'
})
export class FooBarBazFSM extends FsmRxInjectable<States, StateData, CanLeaveToMap>{
public override stateMap: StateMap<States, StateData, CanLeaveToMap> = {
foo: {
canEnterFromStates: { FSMInit: true, bar: true },
canLeaveToStates: { bar: true }
},
bar: {
canEnterFromStates: { foo: true },
canLeaveToStates: { foo: true, baz: true }
},
baz: {
canEnterFromStates: { bar: true },
canLeaveToStates: { FSMTerminate: true }
}
};
}
public constructor() {
super();
this.changeState<"FSMInit">({ state: "foo", commonProperty: "some-string", fooProperty: 5 });
}
public override stateMap: StateMap<States, StateData, CanLeaveToMap> = {
foo:{
...
onEnter: (changes:OnEnterStateChanges<States, "foo", StateData, CanLeaveToMap>) => {
// State buildup logic goes here
},
onLeave: (changes:OnLeaveStateChanges<States, "foo", StateData, CanLeaveToMap>) => {
// State teardown logic goes here
},
onUpdate: (changes:OnUpdateStateChanges<States, "foo", StateData, CanLeaveToMap>) => {
// State update logic goes here
}
}
...
}
public override stateMap: StateMap<States, StateData, CanLeaveToMap> = {
foo:{
...
onEnter: this.handleOnEnterFoo,
onLeave: this.handleOnLeaveFoo,
onUpdate: this.handleOnUpdateFoo
}
...
}
private handleOnEnterFoo(changes: OnEnterStateChanges<States, "foo", StateData, CanLeaveToMap>): void {
// State buildup logic goes here
}
private handleOnLeaveFoo(changes: OnLeaveStateChanges<States, "foo" , StateData, CanLeaveToMap>): void {
// State teardown logic goes here
}
private handleOnUpdateFoo(changes: OnUpdateStateChanges<States, "foo", StateData, CanLeaveToMap>): void {
// State update logic goes here
}
public override stateMap: StateMap<States, StateData, CanLeaveToMap> = {
foo:{
...
onEnter: this.handleOnEnterFooBar,
onLeave: this.handleOnLeaveFooBar,
onUpdate: this.handleOnUpdateFooBar
},
bar:{
...
onEnter: this.handleOnEnterFooBar,
onLeave: this.handleOnLeaveFooBar,
onUpdate: this.handleOnUpdateFooBar
}
...
}
private handleOnEnterFooBar(changes: OnEnterStateChanges<States, "foo" | "bar", StateData, CanLeaveToMap>): void {
// States buildup logic goes here
}
private handleOnLeaveFooBar(changes: OnLeaveStateChanges<States, "foo" | "bar", StateData, CanLeaveToMap>): void {
// States teardown logic goes here
}
private handleOnUpdateFooBar(changes: OnUpdateStateChanges<States, "foo" | "bar", StateData, CanLeaveToMap>): void {
// States update logic goes here
}
this.currentState$.subscribe((currentStateInfo: CurrentStateInfo<States, StateData, CanLeaveToMap>) => {
if (currentStateInfo.state === "FSMInit") { return; }
const currentState: States = currentStateInfo.state;
switch (currentState) {
case "foo":
...
break;
case "bar":
...
break;
case "baz":
...
break;
default:
this.assertCannotReach(currentState);
}
});
this.currentState$.subscribe((currentStateInfo: CurrentStateInfo<States, StateData, CanLeaveToMap>) => {
const { state, stateData } = currentStateInfo;
if (state === "foo") {
const { fooProperty } = stateData;
this.updateState({
...stateData,
fooProperty: fooProperty + 1
});
}
});
public override stateMap: StateMap<States, StateData, CanLeaveToMap> = {
foo:{
...
onEnter: (changes:OnEnterStateChanges<States, "foo", StateData, CanLeaveToMap>) => {
const { stateData } = changes.enteringStateInfo;
const { fooProperty } = stateData;
this.updateState({
...stateData,
fooProperty: fooProperty + 1
});
},
...
}
...
}
this.currentState$.subscribe((currentStateInfo: CurrentStateInfo<States, StateData, CanLeaveToMap>) => {
const { state, canLeaveTo } = currentStateInfo;
if (state === "foo" && canLeaveTo.includes("bar")) {
this.changeState<"foo">({
state: "bar",
commonProperty: "some-string",
barProperty: "some-other-string"
});
}
});
public override stateMap: StateMap<States, StateData, CanLeaveToMap> = {
foo:{
...
onEnter: (changes:OnEnterStateChanges<States, "foo", StateData, CanLeaveToMap>) => {
const { canLeaveTo } = changes.enteringStateInfo;
if (canLeaveTo.includes("bar")) {
this.changeState<"foo">({
state: "bar",
commonProperty: "some-string",
barProperty: "some-other-string"
});
}
},
...
}
...
}
interval(500).pipe(
takeUntil(this.nextChangeStateTransition$), // Unsubscribes on the next change state transition
takeUntil(this.destroy$) // Unsubscribes on destroy
).subscribe(() => {
...
});
Please visit github/issues to submit a bug report or feature request.
For the latest news and community discussions please visit the github/discussions in the core FsmRx package. This is done to not split the community.