Introducing Socket Firewall: Free, Proactive Protection for Your Software Supply Chain.Learn More
Socket
Book a DemoInstallSign in
Socket

@ngx-patterns/store-service

Package Overview
Dependencies
Maintainers
3
Versions
8
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

@ngx-patterns/store-service

Adds an abstraction layer between Angular component and the ngrx store with powerful testing helpers

Source
npmnpm
Version
2.0.2
Version published
Weekly downloads
1
Maintainers
3
Weekly downloads
 
Created
Source

@ngx-patterns/store-service

Adds an abstraction layer between Angular components and the @ngrx store. This decouples the components from the store, selectors and actions and makes it easier to test components.

Install

Get the latest version from NPM

npm install @ngx-patterns/store-service

Comparison

Before

Component

import { Component } from '@angular/core';
import { Observable } from 'rxjs';
import { Book } from 'src/app/shared/books/book.model';
// Tight coupling to ngrx, state model, selectors and actions
import { Store } from '@ngrx/store'; 
import { AppState } from 'src/app/store/appstate.model';
import { getAllBooks } from 'src/app/store/books/books.selectors'; 
import { AddBookAction } from 'src/app/store/books/books.actions'; 
 
@Component({
    selector: 'nss-book-list',
    templateUrl: './book-list.component.html',
    styleUrls: ['./book-list.component.scss']
})
export class BookListComponent {

    books$: Observable<Book[]>;

    constructor(
        private store: Store<AppState>
    ) {
        this.books$ = this.store.select(getAllBooks());
    }

    addBook(book: Book) {
        this.store.dispatch(new AddBookAction(book));
    }
}

After

Component

import { Component } from '@angular/core';
import { Observable } from 'rxjs';
import { Book } from 'src/app/shared/books/book.model';
import { BookStoreService } from 'src/app/shared/books/book-store.service'; // <- Reduced to just one dependency. Loose coupling

@Component({
    selector: 'nss-book-list',
    templateUrl: './book-list.component.html',
    styleUrls: ['./book-list.component.scss']
})
export class BookListComponent {

    books$: Observable<Book[]>;

    constructor(
        private bookStore: BookStoreService // <- StoreService
    ) {
        this.books$ = this.bookStore.getAllBooks(); // <- Selector
    }

    addBook(book: Book) {
        this.bookStore.addBook(book); // <- Action
    }
}

BookStoreService

import { Injectable } from '@angular/core';
import { Selector, StoreService, Action } from '@ngx-patterns/store-service';
import { Observable } from 'rxjs';
import { Book } from 'src/app/shared/books/book.model';
import { getBooks } from 'src/app/store/books/books.selectors';
import { State } from 'src/app/store/store.model';
import { AddBookAction } from 'src/app/store/books/books.actions';

@Injectable()
export class BookStoreService extends StoreService<State> {

    @Selector(getBooks) // <- Selector
    getAllBooks: () => Observable<Book[]>;

    @Action(AddBookAction) // <- Action
    addBook: (book: Book) => void;
}

How to use

StoreService

The BookStoreService Injectable class should extend the StoreService<State> class where State is your ngrx state model.

import { StoreService } from '@ngx-patterns/store-service';
import { AppState } from 'app/store/state.model';

@Injectable()
export class BookStoreService extends StoreService<AppState> {
    ...
}

Selectors

To use selectors you have to use the @Selector(...) decorator inside the StoreService. Add the selector function inside the @Selector(...) annotation:

// Define the selector function
export function selectAllBooks() {
    return state => state.books;
}

...

// Use the selector function inside the @Selector(...) annotation
@Selector(selectAllBooks)
allBooks: () => Observable<Book[]>;

The selector needs to be a function.

Be sure to use correct typing for the property inside the StoreService. If a parameter is required inside the selector function it also has to be required in the property typing.

export function selectBookById(id: number) {
                              ^^^^^^^^^^^^
    return state => state.books[id];
}

...

@Selector(selectBookById)
getBook: (id: number) => Observable<Book>;
         ^^^^^^^^^^^^
// The typing of the selector function and the property have to match!

Actions

To dispatch actions a similar approach as mentioned in the selectors is used. Add a property with the @Action(...) annotation.

// Defined the Action as a class
export class LoadBooksAction implements Action {
    public type = '[Books] Load books';
}

...
// Use the Action class inside the @Action(...) annotation
@Action(LoadBooksAction)
loadBooks: () => void;

If the Action class expects parameters, the typings on the property inside the StoreService have to match the class constructor.

export class AddBookAction implements Action {
    public type = '[Books] Add book';
    constructor(
        public payload: Book
               ^^^^^^^^^^^^^
    ) {}
}

...
@Action(AddBookAction)
addBook: (book: Book) => void;
         ^^^^^^^^^^^^
// The typing of the action constructor and the property have to match!

Complete BookStoreService

The finished BookStoreService looks like this:

import { Action, Selector, StoreService } from '@ngx-patterns/store-service';
import { AppState } from 'app/store/state.model';
import { selectAllBooks, selectBookById } from 'app/store/books/books.selectors';
import { LoadBooksAction, AddBookAction } from 'app/store/books/books.actions';

@Injectable()
export class BookStoreService extends StoreService<AppState> {

    @Selector(selectAllBooks)
    allBooks: () => Observable<Book[]>;

    @Selector(selectBookById)
    getBook: (id: number) => Observable<Book>;

    @Action(LoadBooksAction)
    loadBooks: () => void;

    @Action(AddBookAction)
    addBook: (book: Book) => void;
}

Prerequisites

Selectors should be functions

// This will not work
const selector = state => state.property;
// This works
function selector() {
    return state => state.property;
}

If the selector is not a function, the typing of the StoreService Class won't work: () => Observable<any>

Actions should be classes

export class LoadAction implements Action {
    public type: 'Load action';
    constructor(
        public payload: any
    ) { }
}

This is mandatory because the actions are instantiated using the new keyword.

Testing

Testing your components and the StoreService is made easy. The @ngx-patterns/store-service/testing package provides helpful test-helpers to reduce testing friction.

Selectors

To test selectors you provide the StoreService using the provideStoreServiceMock method in the testing module of your component. Then cast the store service instance using the StoreServiceMock<T> class to get the correct typings.

import { provideStoreServiceMock, StoreServiceMock } from '@ngx-patterns/store-service/testing';
...
let bookStoreService: StoreServiceMock<BookStoreService>;
...
TestBed.configureTestingModule({
    imports: [AppModule],
    providers: [
        provideStoreServiceMock(BookStoreService)
    ]
})
...
bookStoreService = TestBed.get(BookStoreService);

The StoreServiceMock class replaces all selector functions on the store service class with a BehaviourSubject. So now you can do the following to emit new values to the observables:

bookStoreService.getAllBooks().next(newBooks);

The BehaviourSubject is initialized with the value being undefined. If you want a custom initial value, the provideStoreServiceMock method offers an optional parameter. This is an object of key value pairs where the key is the name of the selector function, e.g. getAllBooks.

import { provideStoreServiceMock, StoreServiceMock } from '@ngx-patterns/store-service/testing';
...
let bookStoreService: StoreServiceMock<BookStoreService>;
...
TestBed.configureTestingModule({
    imports: [AppModule],
    providers: [
        provideStoreServiceMock(BookStoreService, {
            getAllBooks: []
        })
    ]
})
...
bookStoreService = TestBed.get(BookStoreService);

The BehaviourSubject for getAllBooks is now initialized with an empty array instead of undefined.

Actions

To test if a component dispatches actions, you import the NgrxStoreServiceTestingModule inside the testing module.

To get the injected Store instance use the MockStore class for proper typings.

import { NgrxStoreServiceTestingModule, MockStore } from '@ngx-patterns/store-service/testing';
...
let mockStore: MockStore;
...
TestBed.configureTestingModule({
    imports: [
        NgrxStoreServiceTestingModule
    ]
})
...
mockStore = TestBed.get(Store);

Optionally use the withState(...) function on the NgrxStoreServiceTestingModule to provide an object that should be used as the state.

import { NgrxStoreServiceTestingModule} from '@ngx-patterns/store-service/testing';
...
const state = {
    books: []
}
...
TestBed.configureTestingModule({
    imports: [
        NgrxStoreServiceTestingModule.withState(state)
    ]
})

The MockStore class has a dispatchedActions property which is an array of all dispatched actions. The last dispatched action is appended at the end.

const lastDispatchedAction = mockStore.dispatchedActions[mockStore.dispatchedActions.length - 1];

// Or with lodash

const lastDispatchedAction = last(mockStore.dispatchedActions);

Examples

For an example of all this have a look at the Angular Project in the src/app folder.

Store Service

Have a look at the BookStoreService

Testing

For examples on Component Tests please have look at the test for the BookListComponent and the NewBookComponent

Testing the StoreService is also very easy. For an example have a look at the BookStoreService

Keywords

ngrx

FAQs

Package last updated on 20 Jul 2018

Did you know?

Socket

Socket for GitHub automatically highlights issues in each pull request and monitors the health of all your open source dependencies. Discover the contents of your packages and block harmful activity before you install or update your dependencies.

Install

Related posts