Ecosyste.ms: Awesome
An open API service indexing awesome lists of open source software.
https://github.com/ngxp/store-service
Adds an abstraction layer / facade between Angular components and the @ngrx store
https://github.com/ngxp/store-service
angular hacktoberfest ngrx rxjs
Last synced: 8 days ago
JSON representation
Adds an abstraction layer / facade between Angular components and the @ngrx store
- Host: GitHub
- URL: https://github.com/ngxp/store-service
- Owner: ngxp
- License: mit
- Created: 2018-07-16T18:50:06.000Z (over 6 years ago)
- Default Branch: master
- Last Pushed: 2023-11-17T13:20:04.000Z (about 1 year ago)
- Last Synced: 2024-11-18T22:01:32.829Z (25 days ago)
- Topics: angular, hacktoberfest, ngrx, rxjs
- Language: TypeScript
- Homepage:
- Size: 1.32 MB
- Stars: 25
- Watchers: 4
- Forks: 6
- Open Issues: 11
-
Metadata Files:
- Readme: README.md
- License: LICENSE
Awesome Lists containing this project
- awesome-angular - store-service - Adds an abstraction layer / facade between Angular components and the NgRx store. (Table of contents / Third Party Components)
- fucking-awesome-angular - store-service - Adds an abstraction layer / facade between Angular components and the NgRx store. (Table of contents / Third Party Components)
README
# @ngxp/store-service
Adds an abstraction layer between Angular components and the [@ngrx](https://github.com/ngrx/platform) store and effects. This decouples the components from the store, selectors, actions and effects and makes it easier to test components.
# Table of contents
* [Installation](#installation)
* [Comparison](#comparison)
* [Before](#before)
* [After](#after)
* [Documentation](#documentation)
* [StoreService](#storeservice)
* [Selectors](#selectors)
* [Actions](#actions)
* [Observers](#observers)
* [Observe multiple types](#multiple-types)
* [Use objects with type property](#objects-with-type-property)
* [String action types](#string-action-types)
* [Custom mapper](#custom-mapper)
* [Deprecated Annotations](#deprecated-annotations)
* [Testing](#testing)
* [Testing Components](#testing-components)
* [Testing Selectors](#testing-selectors)
* [Testing Actions](#testing-actions)
* [Testing Observers](#testing-observers)
* [Testing StoreService](#testing-storeservice)
* [Testing StoreService Selectors](#testing-storeservice-selectors)
* [Testing StoreService Actions](#testing-storeservice-actions)
* [Testing StoreService Observers](#testing-storeservice-observers)
* [Examples](#examples)
* [Example Store Service](#example-store-service)
* [Example Tests](#example-tests)# Installation
Get the latest version from NPM
```sh
npm install @ngxp/store-service
```# Comparison
![Dependency diagram comparison](docs/diagram.png)## Before
> Component
```ts
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 { Actions, ofType } from '@ngrx/effects';
import { AppState } from 'src/app/store/appstate.model';
import { getAllBooks, getBook } from 'src/app/store/books/books.selectors';
import { addBookAction, booksLoadedAction } 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$: Observable;
booksLoaded: boolean = false;constructor(
private store: Store
private actions: Actions
) {
this.books$ = this.store.select(getAllBooks);
this.book$ = this.store.select(getBook, { id: 0 });
this.actions
.pipe(
ofType(booksLoadedAction),
map(() => this.loaded = true)
)
.suscribe();
}addBook(book: Book) {
this.store.dispatch(addBookAction({ book }));
}
}
```## After
> Component
```ts
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$: Observable;
booksLoaded: boolean = false;constructor(
private bookStore: BookStoreService // <- StoreService
) {
this.books$ = this.bookStore.getAllBooks(); // <- Selector
this.book$ = this.bookStore.getBook({ id: 0 }); // <- Selector
this.bookStore.booksLoaded$ // <-- Observer / Action stream of type
.pipe(
map(() => this.loaded = true)
)
.subscribe();
}addBook(book: Book) {
this.bookStore.addBook({ book }); // <- Action
}
}
```> BookStoreService
```ts
import { Injectable } from '@angular/core';
import { select, StoreService, dispatch, observe } from '@ngxp/store-service';
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, booksLoadedAction } from 'src/app/store/books/books.actions';@Injectable()
export class BookStoreService extends StoreService {getAllBooks = select(getBooks); // <- Selector
getBook = select(getBook); // <- Selector
addBook = dispatch(addBookAction); // <- Action
booksLoaded$ = observe([booksLoadedAction]); // <- Observer / Action stream
}
```# Documentation
## StoreService
The `BookStoreService` Injectable class has to extend the `StoreService` class where `State` is your ngrx state model.
```ts
import { StoreService } from '@ngxp/store-service';
import { AppState } from 'app/store/state.model';@Injectable()
export class BookStoreService extends StoreService {
...
}
```## Selectors
To use selectors you wrap the ngrx selector inside the `select(...)` function:
```ts
// Define the selector function
export const selectAllBooks = createSelector(
(state: State) => state.books;
};//Or with props
export const selectBook = createSelector(
(state: State, props: { id: number }) => state.books[id];
};
...// Use the selector function inside the select(...) function
allBooks = select(selectAllBooks); // () => Observablebook = select(selectBook); // (props: { id: number }) => Observable
```
The `select(...)` function automatically infers the correct typing according to the props and return type of the selector.## Actions
To dispatch actions add a property with the `dispatch(...)` function.
```ts
// Defined the Action as a class
export const loadBooksAction = createAction('[Books] Load books');export const addBookAction = createAction('[Books] Add book' props<{ book: Book}>())
...
loadBooks = dispatch(loadBooksAction); // () => voidaddBook = dispatch(addBookAction); // (props: { book: Book }) => void
```The `dispatch(...)` function automatically infers the parameters according to the props of the action.
## Observers
Observers are a way to listen for specific action types on the `Actions` stream from [@ngrx/effects](https://github.com/ngrx/platform/blob/master/docs/effects/README.md).
```ts
booksLoaded$ = observe([booksLoadedAction]);
```### Multiple types
You can provide multiple types, just like in the `ofType(...)` pipe.```ts
booksLoaded$ = observe([booksLoadedAction, booksLoadFailedAction]);
```### Custom mapper
The `observe(...)` function has an additional parameter to provide a custom `customMapper` mapping function.
Initially this will be:
```ts
action => action
```To use a custom mapper, provide it as second argument in the `observe(...)` function.
```ts
export const toData = action => action.data;...
dataLoaded$ = observe([dataLoadedAction], toData);
```## Deprecated Annotations
Before Version 12 the Store Service used annotations instead of functions. This old way still works but is deprecated and _will be removed in the future_.
```ts
import { Select, StoreService, Dispatch, Selector, Dispatcher, Observe } from '@ngxp/store-service';@Select(getBooks)
getAllBooks: Selector;@Dispatch(addBookAction)
addBook: Dispatcher;@Observe([dataLoadedAction], toData)
dataLoaded$: Observable;
```# Testing
Testing your components and the StoreService is easy. The `@ngxp/store-service/testing` package provides useful test-helpers to reduce testing friction.## Testing Components
### Testing Selectors
To test selectors you provide the `StoreService` using the `provideStoreServiceMock` method in the testing module of your component. Then get the `StoreServiceMock` instance using the `getStoreServiceMock` helper function.
```ts
import { provideStoreServiceMock, StoreServiceMock, getStoreServiceMock } from '@ngxp/store-service/testing';
...
let bookStoreService: StoreServiceMock;
...
TestBed.configureTestingModule({
declarations: [
BookListComponent
],
providers: [
provideStoreServiceMock(BookStoreService)
]
})
...
bookStoreService = getStoreServiceMock(BookStoreService);
```The `StoreServiceMock` class replaces all selector functions on the store service class with a `BehaviorSubject`. So now you can do the following to emit new values to the observables:
```ts
bookStoreService.getAllBooks().next(newBooks);
```The `BehaviorSubject` 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`.
```ts
import { provideStoreServiceMock, StoreServiceMock, getStoreServiceMock } from '@ngxp/store-service/testing';
...
let bookStoreService: StoreServiceMock;
...
TestBed.configureTestingModule({
declarations: [
BookListComponent
],
providers: [
provideStoreServiceMock(BookStoreService, {
getAllBooks: []
})
]
})
...
bookStoreService = getStoreServiceMock(BookStoreService);
```The `BehaviorSubject` for `getAllBooks` is now initialized with an empty array instead of `undefined`.
### Testing Actions
To test if a component calls the dispatch methods you provide the `StoreService` using the `provideStoreServiceMock` method in the testing module of your component. Then get the `StoreServiceMock` instance using the `getStoreServiceMock` helper function.
You can then spy on the method as usual.
```ts
import { provideStoreServiceMock, StoreServiceMock } from '@ngxp/store-service/testing';
...
let bookStoreService: StoreServiceMock;
...
TestBed.configureTestingModule({
declarations: [
NewBookComponent
]
imports: [
provideStoreServiceMock(BookStoreService)
]
})
...
it('adds a new book', () => {
const book: Book = getBook();
const addBookSpy = jest.spyOn(bookStoreService, 'addBook');component.book = book;
component.addBook();expect(addBookSpy).toHaveBeenCalledWith({ book });
});
```### Testing Observers
To test observers inside components you provide the `StoreService` using the `provideStoreServiceMock` method in the testing module of your component. Then get the `StoreServiceMock` instance using the `getStoreServiceMock` helper function.
```ts
import { provideStoreServiceMock, StoreServiceMock, getStoreServiceMock } from '@ngxp/store-service/testing';
...
let bookStoreService: StoreServiceMock;
...
TestBed.configureTestingModule({
declarations: [
BookListComponent
],
providers: [
provideStoreServiceMock(BookStoreService)
]
})
...
bookStoreService = getStoreServiceMock(BookStoreService);
```The `StoreServiceMock` class replaces all observer properties on the store service class with a `BehaviorSubject`. So now you can do the following to emit new values to the subscribers:
```ts
bookStoreService.booksLoaded$().next(true);
```The `BehaviorSubject` 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 observer property, e.g. `booksLoaded$`.
```ts
import { provideStoreServiceMock, StoreServiceMock, getStoreServiceMock } from '@ngxp/store-service/testing';
...
let bookStoreService: StoreServiceMock;
...
TestBed.configureTestingModule({
declarations: [
BookListComponent
],
providers: [
provideStoreServiceMock(BookStoreService, {
booksLoaded$: false
})
]
})
...
bookStoreService = getStoreServiceMock(BookStoreService);
```The `BehaviorSubject` for `booksLoaded$` is now initialized with `false` instead of `undefined`.
## Testing StoreService
To test the `StoreService` itself you use the provided test helpers from `@ngrx/store/testing` and `@ngrx/effects/testing`.
### Testing StoreService Selectors
You can provide mocks for selectors with the `provideMockStore` from `@ngrx/store/testing` a. See (@ngrx/store Testing)[https://ngrx.io/guide/store/testing] for their documentation.
Mock the selectors using the `provideMockStore` function and check if the `StoreService` returns an Observable with the mocked value.
```ts
import { MockStore, provideMockStore, getStoreServiceMock } from '@ngrx/store/testing';
import { BookStoreService } from 'src/app/shared/books/book-store.service';
import { selectBook, selectBooks } from '../../store/books/books.selectors';describe('BookStoreService', () => {
let bookStoreService: BookStoreService;
let mockStore: MockStore<{ books: BookState }>;const books: Book[] = [
{
author: 'Joost',
title: 'Testing the StoreService',
year: 2019
}
];beforeEach(waitForAsync(() => {
TestBed.configureTestingModule({
providers: [
BookStoreService,
provideMockStore({
selectors: [
{
selector: selectBooks,
value: books
},
{
selector: selectBook,
value: books[0]
}
]
})
]
});
}));beforeEach(() => {
bookStoreService = getStoreServiceMock(BookStoreService);
mockStore = TestBed.inject(Mockstore);
});it('executes the getBooks Selector', () => {
const expected = cold('a', { a: books });expect(bookStoreService.getAllBooks()).toBeObservable(expected);
});
it('executes the getBook Selector', () => {
const expected = cold('a', { a: books[0] });expect(bookStoreService.getBook({ id: 0 })).toBeObservable(expected);
});
});
```### Testing StoreService Actions
You can provide mocks for selectors with the `provideMockStore` from `@ngrx/store/testing` a. See (@ngrx/store Testing)[https://ngrx.io/guide/store/testing] for their documentation.
Mock the selectors using the `provideMockStore` function and check if the `StoreService` returns an Observable with the mocked value.
To test if the `StoreService` dispatches the correct actions the `MockStore` from @ngrx has a property called `scannedActions$`. This is an Observable of all dispatched actions to check if an action was dispatched correctly.
```ts
import { MockStore, provideMockStore, getStoreServiceMock } from '@ngrx/store/testing';
import { cold } from 'jest-marbles';
import { BookStoreService } from 'src/app/shared/books/book-store.service';
import { addBookAction, loadBooksAction } from '../../store/books/books.actions';describe('BookStoreService', () => {
let bookStoreService: BookStoreService;
let mockStore: MockStore<{ books: BookState }>;beforeEach(waitForAsync(() => {
TestBed.configureTestingModule({
providers: [
BookStoreService,
provideMockStore()
]
});
}));beforeEach(() => {
bookStoreService = getStoreServiceMock(BookStoreService);
mockStore = TestBed.inject(MockStore);
});it('dispatches a new addBookAction', () => {
const book: Book = getBook();
bookStoreService.addBook({ book });const expected = cold('a', { a: addBookAction({ book }) });
expect(mockStore.scannedActions$).toBeObservable(expected);
});
it('dispatches a new loadBooksAction', () => {
bookStoreService.loadBooks();const expected = cold('a', { a: loadBooksAction() });
expect(mockStore.scannedActions$).toBeObservable(expected);
});
});
```### Testing StoreService Observers
To test the observers / actions stream, you import the `provideMockActions` from `@ngrx/effects/testing` inside the testing module.
Then check if the Observer filters the correct actions.```ts
import { getStoreServiceMock } from '@ngxp/store-service/testing';
import { provideMockActions } from '@ngrx/effects/testing';
import { BehaviorSubject } from 'rxjs';
import { BookStoreService } from 'src/app/shared/books/book-store.service';
import { booksLoadedAction } from '../../store/books/books.actions';describe('BookStoreService', () => {
let bookStoreService: BookStoreService;
const mockActions = new BehaviorSubject(undefined);beforeEach(waitForAsync(() => {
TestBed.configureTestingModule({
providers: [
BookStoreService,
provideMockActions(mockActions)
]
});
}));beforeEach(() => {
bookStoreService = getStoreServiceMock(BookStoreService);
});it('filters the BooksLoadedActions in booksLoaded$', () => {
const expectedValue: Book[] = [{
author: 'Author',
title: 'Title',
year: 2018
}];const action = booksLoadedAction({ books: expectedValue });
mockActions.next(action);const expected = cold('a', { a: action });
expect(bookStoreService.booksLoaded$()).toBeObservable(expected);
});
});```
# Examples
For detailed examples of all this have a look at the Angular Project in [the apps/store-service-sample folder](apps/store-service-sample/src/app).
## Example Store Service
Have a look at the [BookStoreService](apps/store-service-sample/src/app/shared/books/book-store.service.ts)
## Example Tests
For examples on Component Tests please have look at the test for the [BookListComponent](apps/store-service-sample/src/app/components/book-list/book-list.component.spec.ts) and the [NewBookComponent](apps/store-service-sample/src/app/components/new-book/new-book.component.spec.ts)
Testing the `StoreService` is also very easy. For an example have a look at the [BookStoreService](apps/store-service-sample/src/app/shared/books/book-store.service.spec.ts)