@bleed-believer/state
TypeScript icon, indicating that this package has built-in type declarations

1.3.1 • Public • Published

@bleed-believer/state

A simple state management using only RxJS to emit changes, orientated to be used in Angular projects. This module includes a variant of FormGroup and of FormArray to create reactive forms without worring to deal with changes emitted endlessly.

Installation

Just execute this command in your terminal:

npm i --save @bleed-believer/state

State class usage

To explain how to use this class, we will implement the following example: You have a counter, and a flag to check if the counter is locked or not. The interface of the counter is the following:

// counter.ts
export interface Counter {
  count:  number;
  locked: boolean;
}

So, the next part is create a class extending the State class (passing the state model as type parameter) with the methods that modify your state:

// counter.state.ts
import { State } from '@bleed-believer/state';
import { Counter } from './counter.ts';

export class CounterState extends State<Counter> {
    constructor() {
        // Sets the initial value
        super({
            count: 0,
            locked: false
        });
    }

    // This method adds +1 to the current counter value
    addOne(): Promise<void> {
        return this.setState(input => {
            if (input.locked) {
                return input;
            } else {
                const count = ++input.count;
                return {
                    ...input,
                    count
                };
            }
        });
    }

    // Locks or unlocks the current counter
    setLockState(locked: boolean): Promise<void> {
        return this.setState(input => ({
            ...input,
            locked
        }));
    }

    // Resets the current counter
    reset(): Promise<void> {
        return this.setState(input => ({
            count: 0,
            locked: false
        }));
    }
}

Finally, create a new instance in your component. To read the current state of your instance, you have a property called state, is a BehaviorSubject<T>. Every changes made with your methods declared previously in your State class, emits a new value:

// counter.component.ts
import { Component } from '@angular/core';
import { CounterState } from './counter.state';

@Component({
  selector: 'app-counter',
  styleUrls: ['./counter.component.scss'],
  template: `
    <h2>{{ (this.counter.state | async)?.count }}</h2>

    <button
    (click)="this.counter.addOne()">
        <span>+1</span>
    </button>

    <button
    (click)="this.counter.setLockState(true)"
    [disabled]="(this.counter.state | async)?.locked">
        <span>Lock</span>
    </button>

    <button
    (click)="this.counter.setLockState(false)"
    [disabled]="!(this.counter.state | async)?.locked">
        <span>Unlock</span>
    </button>

    <button
    (click)="this.counter.reset()">
        <span>Reset</span>
    </button>
  `
})
export class ContrVentaComponent {
  counter = new CounterState();
}

StateFormGroup class usage

In certain cases when you work with Angular, you may need to use state management in conjunction with Reactive Forms. If you have a lot of forms, everyone with a part of the whole state, the control of the value emission could be converted in a painful task. To deal with that, this package includes this class. Escencially, this class extends FormGroup class, adding some methods to avoid emit changes when you don't need that.

For example:

  • test-state.service.ts

    import { Injectable } from '@angular/core';
    import { State } from '@bleed-believer/state';
    
    export interface TestState {
        // ... bla bla bla
        // ... bla bla bla
    
        code: string;
        desc: string;
    }
    
    export class TestStateService extends State<TestState> {
        constructor() {
            super({
                // ... bla bla bla
                // ... bla bla bla
            });
        }
        
        // ... bla bla bla
        // ... bla bla bla
    
        setItem(code: string, desc: string): Promise<void> {
            return this.setState(v => {
                return { ...v, code, desc };
            });
        }
    }
  • test.component.html

    <form
    [formGroup]="this.form">
        <div>
            <label>Code:</label>
            <input type="text" formControlName="code" />
        </div>
        <div>
            <label>Description:</label>
            <input type="text" formControlName="desc" />
        </div>
    </form>
  • test.component.ts

    import {
        ChangeDetectionStrategy, ChangeDetectorRef, Component,
        OnDestroy, OnInit
    } from '@angular/core';
    import { Subscription } from 'rxjs';
    import { StateFormGroup } from '@bleed-believer/state';
    
    import { TestState, TestStateService } from './test-state.service';
    
    @Component({
        selector: 'app-test',
        templateUrl: './test.component.html',
        styleUrls: ['./test.component.scss'],
        changeDetection: ChangeDetectionStrategy.OnPush
    })
    export class TestComponent implements OnInit, OnDestroy {
        #subs: Subscription[];
    
        // Create the instance here
        form = new StateFormGroup<TestState>({
            code:   ['', Validators.required],
            desc:   ['', Validators.required],
        });
    
        constructor(
            private _testState: TestStateService,
        ) {}
    
        ngOnInit(): void {
            this.#subs = [
                // Listens from changes by the user
                this.form
                    .valueChangesByUser
                    .subscribe(this.onFormChanges.bind(this)),
                
                // Listens from changes by the state
                this._testState
                    .state
                    .subscribe(this.onStateChanges.bind(this)),
            ]
        }
    
        onStateChanges(state: TestState): void {
            // Updates the form data without emit a change
            this.form.setValueSilently({
                code: state.code,
                desc: state.desc
            });
        }
    
        onFormChanges(): void {
            // Ignores changes emited in "this.onStateChange"
            if (this.form.invalid) { return; }
    
            // Emit a change
            const { code, desc } = this.form.partialValue;
            this._testServ.setItem(
                code as string,
                desc as string
            );
        }
    }

StateFormArray

This class extends FormArray, with the same utilities given by StateFormGroup. When you create the instance, simply declare the structure of all inner forms, and use the methods setValueSilently or patchValueSilently to rewrite all inner forms (these methods create more forms or delete the leftover forms).

For example:

  • test-state.service.ts

    import { Injectable } from '@angular/core';
    import { State } from '@bleed-believer/state';
    
    export interface TestState {
        code: string;
        desc: string;
    }
    
    export class TestStateService extends State<TestState[]> {
        constructor() {
            super([]);
        }
        
        // ... bla bla bla
        // ... bla bla bla
    
        setData(data: TestState[]): Promise<void> {
            return this.setState(() => {
                return TestState.map(x => { ...x });
            });
        }
    }
  • test.component.html

    <form
    [formArray]="this.form">
        <table>
            <thead>
                <tr>
                    <th>Code</th>
                    <th>Description</th>
                    <th>Actions</th>
                </tr>
            </thead>
            <tbody>
                <tr *ngFor="let form of this.form.controls; let i = index"
                [formGroup]="form">
                    <td>
                        <input type="text" formControlName="code" />
                    </td>
                    <td>
                        <input type="text" formControlName="desc" />
                    </td>
                    <td>
                        <button
                        type="button"
                        (click)="this.form.createAt(i)">
                            <span>Create at</span>
                        </button>
                    </td>
                </tr>
            </tbody>
        </table>
    </form>
  • test.component.ts

    import {
        ChangeDetectionStrategy, ChangeDetectorRef, Component,
        OnDestroy, OnInit
    } from '@angular/core';
    import { Subscription } from 'rxjs';
    import { StateFormArray } from '@bleed-believer/state';
    
    import { TestState, TestStateService } from './test-state.service';
    
    @Component({
        selector: 'app-test',
        templateUrl: './test.component.html',
        styleUrls: ['./test.component.scss'],
        changeDetection: ChangeDetectionStrategy.OnPush
    })
    export class TestComponent implements OnInit, OnDestroy {
        #subs: Subscription[];
    
        // Create the instance here
        form = new StateFormArray<TestState>({
            code:   ['', Validators.required],
            desc:   ['', Validators.required],
        });
    
        constructor(
            private _testState: TestStateService,
        ) {}
    
        ngOnInit(): void {
            this.#subs = [
                // Listens from changes by the user
                this.form
                    .valueChangesByUser
                    .subscribe(this.onFormChanges.bind(this)),
                
                // Listens from changes by the state
                this._testState
                    .state
                    .subscribe(this.onStateChanges.bind(this)),
            ]
        }
    
        onStateChanges(state: TestState[]): void {
            // Updates the form data without emit a change
            this.form.setValueSilently(state);
        }
    
        onFormChanges(): void {
            // Ignores changes emited in "this.onStateChange"
            if (this.form.invalid) { return; }
    
            // Emit a change
            const data = this.form.partialValue;
            this._testServ.setData(data);
        }
    }

Serial class usage

In certain cases, you may need to implement a method that you need a certainty that will be called only once, even if is called while the first call still in progress. So for those cases exist the Serial class.

There's an example:

import { Serial } from '@bleed-believer/state';

export class Dummy {
    static #serial = new Serial();

    someSerialMethod(): Promise<void> {
        return Dummy.#serial.push(async () => {
            console.log('Method started');
            // An irrelevant process that takes
            // 5 segs to be completed...
            console.log('Method ended');
        });
    }
}

Using the class of above, if you make this...

const dummy = new Dummy();

console.log('Begin calls');
dummy.someSerialMethod();   // 1st call launched in paralell
dummy.someSerialMethod();   // 2nd call launched in paralell
dummy.someSerialMethod();   // 3rd call launched in paralell
console.log('End calls');

...the Serial instance will add the someSerialMethod in a queue. Executes the first elemen in the queue, and waits the call end or fail before to execute the next in queue. Taking the code of above and the example class, the text printed in terminal will be:

Begin calls     # Before to call the `someSerialMethod` three times
EndCalls        # After to make the three calls of `someSerialMethod`

Method started  # 1st call initialized
Method ended    # 1st call ended after 5 segs

Method started  # 2nd call initialized
Method ended    # 2nd call ended after 5 segs

Method started  # 3rd call initialized
Method ended    # 3rd call ended after 5 segs

An alternative approach could be, for example, a case when you need that you process will be launched only when the queue is empty. To implement a case like that, you can made something like this:

export class Pulsar {
    static #serial = new Serial();

    onClick(): Promise<void> {
        if (Pulsar.#serial.isBusy) {
            // Doesn't execute the process because
            // the serial instance is busy
            return Promise.resolve();
        }

        return Pulsar.#serial.push(async () => {
            // An irrelevant process that takes
            // 5 segs to be completed...
        });
    }
}

With that example, the process will be executed only when the Serial instance is free. If the process is running, no matter how many times do you press the button, the process won't be launched. When the first call is ended, the Serial instance will be available to receipt the next call.

Package Sidebar

Install

npm i @bleed-believer/state

Weekly Downloads

58

Version

1.3.1

License

MIT

Unpacked Size

32.9 kB

Total Files

30

Last publish

Collaborators

  • sleep-written