@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.
How to use
Before
Component
import { Component } from '@angular/core';
import { Observable } from 'rxjs';
import { Book } from 'src/app/shared/books/book.model';
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';
@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
) {
this.books$ = this.bookStore.getAllBooks();
}
addBook(book: Book) {
this.bookStore.addBook(book);
}
}
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)
getAllBooks: () => Observable<Book[]>;
@Action(AddBookAction)
addBook: (book: Book) => void;
}
StoreService
The BookStoreService
Injectable class should extend the StoreService<State>
class where State
is your ngrx state model.
Selectors
To use selectors you have to use the @Selector(...)
decorator inside the StoreService
. Add the selector function inside the @Selector(...)
annotation:
export function selectAllBooks() {
return state => state.books;
}
...
@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>;
^^^^^^^^^^^^
Actions
To dispatch actions a similar approach as mentioned in the selectors is used. Add a property with the @Action(...)
annotation.
export class LoadBooksAction implements Action {
public type = '[Books] Load books';
}
...
@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;
^^^^^^^^^^^^
Prerequisites
Selectors need to be functions
const selector = state => state.property;
function selector() {
return state => state.property;
}
Otherwise the typing of the StoreService Class won't work: () => Observable<any>
Actions need to be classes
export class LoadAction implements Action {
public type: 'Load action';
constructor(
public payload: any
) { }
}
The actions are instantiated using the new
keyword.
Testing
Selectors
Testing is made easy inside your components. You don't need to import the whole StoreModule.forRoot(...)
.
Simply provide the StoreService
using the provideStoreServiceMock
method. 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: [
{
provide: BookStoreService,
useValue: provideStoreServiceMock(BookStoreService)
}
]
})
...
bookStoreService = TestBed.get(BookStoreService);
The StoreServiceMock
class replaces all selector function on the store service class with BehaviourSubjects
. So now you can do the following:
bookStoreService.getAllBooks().next(newBooks);
To emit a new list of books to the components observable. Super easy testing.
Actions
To test if a component dispatches actions import the NgrxStoreServiceTestingModule
inside your Testing Module.
To get the injected Store instance use the MockStore
class to get the correct 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 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 dispatched actions. The last dispatched action is added at the end.
const lastDispatchedAction = mockStore.dispatchedActions[mockStore.dispatchedActions.length - 1];
const lastDispatchedAction = last(mockStore.dispatchedActions);
Example
For and 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