ngx-fsm-rx
TypeScript icon, indicating that this package has built-in type declarations

1.0.5 • Public • Published

NgxFsmRx

About

Angular wrapper around FsmRx, a Finite State Machine built upon RxJS and Typescript.

What is FsmRx

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.

Related projects

Further Documentation

For full documentation see:

Installation

ng add ngx-fsm-rx

Quick Start Guide - Schematics

Component

ng generate ngx-fsm-rx:component

Gif showing ng generate schematic for NgxFsmRx components

Service

ng generate ngx-fsm-rx:service

Quick Start Guide - Manual

Create FSM states union

type States = "foo" | "bar" | "baz";

Create FSM state data union

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;

Create CanLeaveToMap

As Type

type CanLeaveToMap = {
    FSMInit: "foo",
    foo: "bar",
    bar: "foo" | "baz";
    baz: "FSMTerminate";
};

As Interface

interface CanLeaveToMap extends CanLeaveToStatesMap<States> {
    FSMInit: "foo",
    foo: "bar",
    bar: "foo" | "baz";
    baz: "FSMTerminate";
}

Extend FsmRx and Define StateMap

Component

@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 }
        }
    };
}

Service

@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 }
        }
    };
}

Define The Constructor and Transition to the First State

 public constructor() {
    super();
    this.changeState<"FSMInit">({ state: "foo", commonProperty: "some-string", fooProperty: 5 });     
}

Define onEnter, onLeave and onUpdate callbacks

Inline function

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 
        }
    }
    ...
}

Regular function

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 
}

Regular function with multiple states

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  
}

Get Current State

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);
    }
});

Update State

From 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
        });
    }
});

From Transition Callback

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
            });
        },
        ...
    }
    ...
}

Change State

From currentState$

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"
        });
    }
});

From Transition Callback

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"
                });
            }
        },
        ...
    }
    ...
}

Unsubscribe Rxjs Helpers

interval(500).pipe(
    takeUntil(this.nextChangeStateTransition$), // Unsubscribes on the next change state transition 
    takeUntil(this.destroy$) // Unsubscribes on destroy
).subscribe(() => {
    ...
});

Get in contact

Submit a bug report

Please visit github/issues to submit a bug report or feature request.

Community

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.

Package Sidebar

Install

npm i ngx-fsm-rx

Weekly Downloads

2

Version

1.0.5

License

ISC

Unpacked Size

233 kB

Total Files

61

Last publish

Collaborators

  • ionic234