Next Phenomenal Microbrewery

    ngrx-action-bundles
    TypeScript icon, indicating that this package has built-in type declarations

    3.2.1 • Public • Published

    NGRX Action Bundles (Dependencies: @ngrx/store, @ngrx/effects)

    Easily Generate NGRX Action Bundles and Easily Connect the Dispatchers and Listeners to your Angular Injectables/Components/Directives/Pipes

    TODO:

    • Create connect service method type guards.
    • Allow connect service methods to work with the default ngrx/store action creator

    USAGE:

    npm install ngrx-action-bundles || yarn add ngrx-action-bundles

    Example:

    app.module.ts

    import { NgModule } from '@angular/core';
    import { reducers } from './+store/reducers';
    import { StoreModule } from '@ngrx/store';
    import { EffectsModule } from '@ngrx/effects';
    import { StoreDevtoolsModule } from '@ngrx/store-devtools';
    
    @NgModule({
      imports: [
        EffectsModule.forRoot([...]),
        StoreModule.forRoot(reducers),
        StoreDevtoolsModule.instrument()
      ],
      ...
    })
    export class AppModule { }

    actions.ts

    import {
      createAsyncBundleWithClear,
      createBundleWithClear,
    } from "ngrx-action-bundles";
    import { IHttpRequestError, ILoadUsersSuccessPayload } from "../interfaces";
    
    const actionNamespace = "[MAIN]" as const;
    
    const loadUsersActionName = "loadUsers" as const;
    const itemActionName = "item" as const;
    
    /* *
     *  <NGRX Action Bundles> Available functions:
     *
     *  - createBundle<NameType, NamespaceType>(actionName, namespace)<ActionPayloadType>()
     *
     *    Creates <actionName> action      --> { type: <[namespace] <actionName>>, payload: ActionPayloadType };
     *
     *    Returns: {
     *      dispatch: {
     *        [<actionName>]: (payload: ActionPayloadType) => void
     *      },
     *      listen: {
     *        [<actionName>$]: Observable<{ type: <[namespace] <actionName>>, payload: ActionPayloadType }>
     *      },
     *      creators: {
     *        [<actionName>]: (payload: ActionPayloadType) => { type: <[namespace] <actionName>>, payload: ActionPayloadType }
     *      }
     *    }
     *
     *  - createBundleWithClear<NameType, NamespaceType)<ActionPayloadType, ClearActionPayloadType>(actionName, namespace>()
     *
     *    Creates set<ActionName> action      --> { type: <[namespace] set<ActionName>>, payload: ActionPayloadType };
     *    Creates clear<ActionName> action    --> { type: <[namespace] clear<ActionName>>, payload: ClearActionPayloadType };
     *
     *    Returns: {
     *      dispatch: {
     *        [set<ActionName>]: (payload: ActionPayloadType) => void,
     *        [clera<ActionName>]: (payload: ClearActionPayloadType) => void
     *      },
     *      listen: {
     *        [set<ActionName>$]: Observable<{ type: <[namespace] set<ActionName>>, payload: ActionPayloadType }>,
     *        [clear<ActionName>$]: Observable<{ type: <[namespace] clear<ActionName>>, payload: ClearActionPayloadType }>,
     *      },
     *      creators: {
     *        [set<ActionName>]: (payload: ActionPayloadType) => { type: <[namespace] set<ActionName>>, payload: ActionPayloadType },
     *        [clear<AtionName>]: (payload: ClearActionPayloadType) => { type: <[namespace] clear<ActionName>>, payload: ClearActionPayloadType },
     *      }
     *    }
     *
     *  - createAsyncBundle<NameType, NamespaceType>(actionName, namespace)<
     *      ActionPayloadType,
     *      ActionSuccessPayloadType,
     *      ActionFailurePayloadType
     *      ActionCancelPayloadType
     *    >()
     *
     *    Creates <actionName>             --> { type: <[namespace] <actionName>>, payload: ActionPayloadType };
     *    Creates <actionName>Success      --> { type: <[namespace] <actionName>Success>, payload: ActionSuccessPayloadType };
     *    Creates <actionName>Failure      --> { type: <[namespace] <actionName>Failure>, payload: ActionFailurePayloadType }
     *    Creates <actionName>Cancel       --> { type: <[namespace] <actionName>Cancel>, payload: ActionCancelPayloadType }
     *
     *    Returns: {
     *      dispatch: {
     *        [<actionName>]: (payload: ActionPayloadType) => void,
     *        [<actionName>Success]: (payload: ActionSuccessPayloadType) => void,
     *        [<actionName>Failure]: (payload: ActionFailurePayloadType) => void,
     *        [<actionName>Cancel]: (payload: ActionCancelPayloadType) => void,
     *      },
     *      listen: {
     *        [<actionName>$]: Observable<{ type: <[namespace] <actionName>>, payload: ActionPayloadType }>,
     *        [<actionName>Success$]: Observable<{ type: <[namespace] <actionName>Success>, payload: ActionSuccessPayloadType }>,
     *        [<actionName>Failure$]: Observable<{ type: <[namespace] <actionName>Failure>, payload: ActionFailurePayloadType }>,
     *        [<actionName>Cancel$]: Observable<{ type: <[namespace] <actionName>Cancel>, payload: ActionCancelPayloadType }>,
     *      },
     *      creators: {
     *        [<actionName>]: (payload: ActionPayloadType) => { type: <[namespace] <actionName>>, payload: ActionPayloadType },
     *        [<actionName>Success]: (payload: ActionSuccessPayloadType) => { type: <[namespace] <actionName>Success>, payload: ActionSuccessPayloadType },
     *        [<actionName>Failure]: (payload: ActionFailurePayloadType) => { type: <[namespace] <actionName>Failure>, payload: ActionFailurePayloadType },
     *        [<actionName>Cancel]: (payload: ActionCancelPayloadType) => { type: <[namespace] <actionName>Cancel>, payload: ActionCancelPayloadType },
     *      }
     *    }
     *
     *  - createAsyncBundleWithClear<NameType, NamespaceType>(actionName, namespace)<
     *      ActionPayloadType, ActionSuccessPayloadType,
     *      ActionFailurePayloadType, ActionCancelPayloadType,
     *      ClearActionPayloadType>()
     *
     *    Creates <actionName>             --> { type: <[namespace] <actionName>>, payload: ActionPayloadType };
     *    Creates <actionName>Success      --> { type: <[namespace] <actionName>Success>, payload: ActionSuccessPayloadType };
     *    Creates <actionName>Failure      --> { type: <[namespace] <actionName>Failure>, payload: ActionFailurePayloadType }
     *    Creates <actionName>Cancel       --> { type: <[namespace] <actionName>Cancel>, payload: ActionCancelPayloadType }
     *    Creates <actionName>Clear        --> { type: <[namespace] <actionName>Clear>, payload: ClearActionPayloadType };
     *
     *    Returns: {
     *      dispatch: {
     *        [<actionName>]: (payload: ActionPayloadType) => void,
     *        [<actionName>Success]: (payload: ActionSuccessPayloadType) => void,
     *        [<actionName>Failure]: (payload: ActionFailurePayloadType) => void,
     *        [<actionName>Cancel]: (payload: ActionCancelPayloadType) => void,
     *        [<actionName>Clear]: (payload: ClearActionPayloadType) => void
     *      },
     *      listen: {
     *        [<actionName>$]: Observable<{ type: <[namespace] <actionName>>, payload: ActionPayloadType }>,
     *        [<actionName>Success$]: Observable<{ type: <[namespace] <actionName>Success>, payload: ActionSuccessPayloadType }>,
     *        [<actionName>Failure$]: Observable<{ type: <[namespace] <actionName>Failure>, payload: ActionFailurePayloadType }>,
     *        [<actionName>Cancel$]: Observable<{ type: <[namespace] <actionName>Cancel>, payload: ActionCancelPayloadType }>,
     *        [<actionName>Clear$]: Observable<{ type: <[namespace] <actionName>Clear>, payload: ClearActionPayloadType }>,
     *      },
     *      creators: {
     *        [<actionName>]: (payload: ActionPayloadType) => { type: <[namespace] <actionName>>, payload: ActionPayloadType },
     *        [<actionName>Success]: (payload: ActionSuccessPayloadType) => { type: <[namespace] <actionName>Success>, payload: ActionSuccessPayloadType },
     *        [<actionName>Failure]: (payload: ActionFailurePayloadType) => { type: <[namespace] <actionName>Failure>, payload: ActionFailurePayloadType },
     *        [<actionName>Cancel]: (payload: ActionCancelPayloadType) => { type: <[namespace] <actionName>Cancel>, payload: ActionCancelPayloadType },
     *        [<actionName>Clear]: (payload: ClearActionPayloadType) => { type: <[namespace] <actionName>Clear>, payload: ClearActionPayloadType },
     *      }
     *    }
     * */
    
    export const loadUsersBundle = createAsyncBundleWithClear(
      loadUsersActionName,
      actionNamespace
    )<void, ILoadUsersSuccessPayload, IHttpRequestError, void, void>();
    
    export const itemBundle = createBundleWithClear(
      setItemActionName,
      actionNamespace
    )();

    reducers.ts

    import { createReducer, on } from "@ngrx/store";
    import { loadUsersBundle, itemBundle } from "./actions";
    import { IUser } from "../interfaces";
    
    export interface IUserListState {
      userList: IUser[] | null;
      item: any;
    }
    
    export const initialState: IUserListState = {
      userList: null,
      item: null,
    };
    
    /* *
     * Here we just use the loadUsers action bundle that we've created in the actions.ts file
     * in order to access the different actions so we can provide them to the NGRX `on` function.
     * */
    
    export const userListReducer = createReducer<IUserListState>(
      initialState,
      on(loadUsersBundle.creators.loadUsers, (state) => {
        return { ...state, userList: null };
      }),
      // since loadUsersSuccess is generated from the bundle we have a payload which contains our data
      on(
        loadUsersBundle.creators.loadUsersSuccess,
        (state, { payload: { users } }) => {
          return { ...state, userList: users };
        }
      ),
      on(
        loadUsersBundle.creators.loadUsersFailure,
        (
          status,
          {
            payload: {
              error: { message },
            },
          }
        ) => {
          return { ...status, error: message };
        }
      ),
      on(loadUsersBundle.creators.loadUsersClear, (status) => {
        return { ...status, userList: null };
      }),
      on(itemBundle.creators.setItem, (state, { payload: { item } }) => {
        return { ...state, item };
      }),
      on(itemBundle.creators.clearItem, (state) => {
        return { ...state, item: null };
      })
    );

    effects.ts

    import { HttpClient } from "@angular/common/http";
    import { Injectable } from "@angular/core";
    import { Actions, createEffect, ofType } from "@ngrx/effects";
    import { map, switchMap, takeUntil } from "rxjs/operators";
    import { IUser } from "../interfaces";
    import { loadUsers } from "./actions";
    
    @Injectable()
    export class UserListEffects {
      /* *
       * Here we use the <NGRX Action Bundles> `connect` service in order to connect
       * our bundles to a property called actions on our injectable.
       *
       * */
    
      actions = this.connect.connectBundles([loadUsersBundle]);
    
      loadUsers = createEffect(() =>
        this.actions.listen.loadUsers$.pipe(
          switchMap(() =>
            this.http
              .get<IUser[]>("https://jsonplaceholder.typicode.com/users")
              .pipe(
                takeUntil(this.actions.listen.loadUsersCancel$),
                map((users) => this.actions.creators.loadUsersSuccess({ users })),
                catchError((error) => [
                  this.actions.creators.loadUsersFailure({ error }),
                ])
              )
          )
        )
      );
    
      constructor(private http: HttpClient, private connect: Connect) {}
    }

    selectors.ts

    import { createSelector } from "@ngrx/store";
    import { ActionReducerMap } from "@ngrx/store";
    
    export interface IRootState {
      readonly main: IMainState;
    }
    
    export const reducers: ActionReducerMap<IRootState> = {
      main: mainReducer,
    };
    
    export const selectMain = (state: IRootState) => state.main;
    
    export const selectMainUserList = createSelector(
      selectMain,
      (state: IMainState) => state.userList
    );
    
    export const selectMainItem = createSelector(
      selectMain,
      (state: IMainState) => state.item
    );

    some.component.ts

    import { Component, OnDestroy, OnInit } from '@angular/core';
    import { Connect } from 'ngrx-action-bundles';
    import { loadUsersBundle, itemBundle } from '../+store/actions';
    import { selectMainUserList, selectMainItem } from '../+store/selectors';
    import { merge, Subscription } from 'rxjs';
    import { mapTo } from 'rxjs/operators';
    
    @Component({
      ...
    })
    export class SomeComponent implements OnInit, OnDestroy {
    
      /* *
       * Here we use the <NGRX Action Bundles> `connect` service in order to connect
       * the bundles to a property called actions on our component.
       *
       * We also use `connectSelectors` to connect all the selectors to the selectors property so we can
       * directly access the rxjs streams from the store.
       * */
    
      subscriptions = new Subscription();
    
      actions = this.connect.connectBundles([
        loadUsersBundle,
        itemBundle
      ]);
    
      selectors = this.connect.connectSelectors({
        userList: selectMainUserList,
        item: selectMainItem
      });
    
      users$ = this.selectors.userList$;
      item$ = this.selectors.item$;
    
      isLoading = false;
    
      constructor(private connect: Connect) {
        this.subscriptions.add(
          merge<any, boolean>(
            this.actions.listen.loadUsers$.pipe(mapTo(true)),
            this.actions.listen.loadUsersSuccess$.pipe(mapTo(false)),
            this.actions.listen.loadUsersFailure$.pipe(mapTo(false)),
          ).subscribe(isLoading => this.isLoading = isLoading)
        );
      }
    
      ngOnInit(): void {
        this.actions.dispatch.loadUsers();
    
        this.subscriptions.add(
          this.actions.listen.loadUsersSuccess$.subscribe(console.log)
        );
        this.subscriptions.add(
          this.actions.listen.loadUsersCancel$.subscribe(console.log)
        );
        this.subscriptions.add(
          this.actions.listen.loadUsersClear$.subscribe(console.log)
        );
      }
    
    
      ngOnDestroy(): void {
        if (this.isLoading) { this.actions.dispatch.loadUsersCancel(); }
        this.actions.dispatch.loadUsersClear();
        this.subscriptions.unsubscribe();
      }
    }

    some.component.html

    <div>
      <h1>User List</h1>
      <div *ngIf="isLoading">Loading...</div>
      <div *ngFor="let user of (users$ | async)">{{user.username}}</div>
      <button (click)="actions.dispatch.loadUsers()">Reload Users</button>
    </div>
    <div>
      <h1>Message Item is: {{item$ | async}}</h1>
      <input #inp type="text" value="" />
      <button
        (click)="actions.dispatch.setItem({ item: inp.value }); inp.value = ''"
      >
        Set item in store
      </button>
      <button (click)="actions.dispatch.clearItem()">Clear item in store</button>
    </div>

    Unique Action Id - Timestamp

    If you need a unique way to distinguish the actions you can take a look into createAsyncTimestampBundleWithClear (check the demo app / loadUsers bundle). You will notice that we have something lile 1647882544485.2952 instead of 1647882544485. The .2952 is added so we can distinguish the actions dispatched right after another (for example in a loop for whatever reason someone would want that).

    Install

    npm i ngrx-action-bundles

    DownloadsWeekly Downloads

    19

    Version

    3.2.1

    License

    MIT

    Unpacked Size

    239 kB

    Total Files

    32

    Last publish

    Collaborators

    • rampage