UNPKG

@harmowatch/ngx-redux-core

Version:

Decorator driven redux integration for Angular 2+

696 lines (521 loc) 18.8 kB
# [WIP] @harmowatch/ngx-redux-core Hey there, the package is still work in progress, please [check the open tasks](https://github.com/HarmoWatch/ngx-redux/projects/1). ## Decorator driven redux integration for Angular 2+ * What is ... * [Redux?](#what-is-redux) * [ngx-redux?](#what-is-ngx-redux) * [Installation](#installation) * [Usage](#usage) * [1. Import the root `ReduxModule`](#1-import-the-root-reduxmodule) * [1.1 Bootstrap your own Redux Store](#11-bootstrap-your-own-redux-store) * [2. Describe your state](#2-describe-your-state) * [3. Create a class representing your module state](#3-create-a-class-representing-your-module-state) * [4. Register the state](#4-register-the-state) * [5. Select data from the state](#5-select-data-from-the-state) * [6. Dispatch an Redux Action](#6-dispatch-an-redux-action) * [7. Reduce the State](#7-reduce-the-state) * [Known violations / conflicts](#known-violations--conflicts) ## What is Redux? Redux is a popular and common approach to manage a application state. The three principles of redux are: - [Single source of truth](http://redux.js.org/docs/introduction/ThreePrinciples.html#single-source-of-truth) - [State is read-only](http://redux.js.org/docs/introduction/ThreePrinciples.html#state-is-read-only) - [Changes are made with pure functions](http://redux.js.org/docs/introduction/ThreePrinciples.html#changes-are-made-with-pure-functions) [Read more about Redux](http://redux.js.org/) ## What is ngx-redux? This package helps you to integrate Redux in your Angular 2+ application. By using *ngx-redux* you'll get the following benefits: - support for [lazy loaded NgModules](https://angular.io/guide/ngmodule#lazy-loading-modules-with-the-router) - [Ahead-of-Time Compilation (AOT)](https://angular.io/guide/aot-compiler) support - a [Angular Pipe](https://angular.io/guide/pipes) to select the values from the state - better typescript and refactoring support - a decorator and module driven approach - easy to test ## Installation First you need to install - "redux" as peer dependency - the "@harmowatch/ngx-redux-core" package itself ```sh npm install redux @harmowatch/ngx-redux-core --save ``` ## Usage ### 1. Import the root `ReduxModule`: To use *ngx-redux* in your Angular project you have to import `ReduxModule.forRoot()` in the root NgModule of your application. The static [`forRoot`](https://angular.io/docs/ts/latest/guide/ngmodule.html#!#core-for-root) method is a convention that provides and configures services at the same time. Make sure you call this method in your root NgModule, only! ###### Example ```ts import { NgModule } from '@angular/core'; import { BrowserModule } from '@angular/platform-browser'; import { ReduxModule } from '@harmowatch/ngx-redux-core'; import { AppComponent } from './app.component'; @NgModule({ declarations: [ AppComponent ], imports: [ BrowserModule, ReduxModule.forRoot(), ], bootstrap: [ AppComponent ] }) export class AppModule { } ``` #### 1.1 Bootstrap your own [Redux Store](http://redux.js.org/docs/basics/Store.html) By default *ngx-redux* will bootstrap a [Redux Store](http://redux.js.org/docs/basics/Store.html) for you. Is the app running in [devMode](https://angular.io/api/core/isDevMode), the default store is prepared to work together with the [Redux DevTools](https://github.com/gaearon/redux-devtools). If you want to add a [Middleware](http://redux.js.org/docs/advanced/Middleware.html) like logging, you've to provide a custom *stateFactory*. ###### Example ```ts import { NgModule } from '@angular/core'; import { BrowserModule } from '@angular/platform-browser'; import { ReduxModule, ReduxModuleRootReducer } from '@harmowatch/ngx-redux-core'; import { applyMiddleware, createStore, Store, StoreEnhancer } from 'redux'; import logger from 'redux-logger'; import { AppComponent } from './app.component'; export function enhancerFactory(): StoreEnhancer<{}> { return applyMiddleware(logger); } export function storeFactory(): Store<{}> { return createStore( ReduxModuleRootReducer.reduce, {}, enhancerFactory() ); } @NgModule({ declarations: [ AppComponent ], imports: [ BrowserModule, ReduxModule.forRoot({ storeFactory, }), ], bootstrap: [ AppComponent ] }) export class AppModule { } ``` ### 2. Describe your state Ok, now you've to create a interface to describe the structure of your state. ###### Example ```ts export interface AppModuleStateInterface { todo: { items: string[]; }; } ``` ### 3. Create a class representing your module state Before you can register your state to redux, you need to create a class that represents your state. This class is responsible to resolve the initial state. #### As you can see in the example below, the class ... ##### ... is decorated by `@ReduxState` You need to decorate your class by `@ReduxState` and to provide a application wide unique state `name` to it. If you can not be sure that your name is unique enough, then you can add a unique id to it (*as in the example shown below*). ##### ... implements `ReduxStateInterface` The `@ReduxState` decorator is only valid for classes which implement the `ReduxStateInterface`. This is an generic interface where you've to provide your previously created `AppModuleStateInterface`. The `ReduxStateInterface` compels you to implement a public method `getInitialState`. This method is responsible to know, how the initial state can be computed and will return it as an `Promise`, `Observable` or an implementation of the state interface directly. > Note: The method `getInitialState` is called by *ngx-redux* automatically! Your state will be registered to the root state **after** the initial state was resolved successfully. ###### Example 1) Interface implementation ```ts import { ReduxState, ReduxStateInterface } from '@harmowatch/ngx-redux-core'; import { AppModuleStateInterface } from './app.module.state.interface'; @ReduxState({ name: 'app-module-7c66b613-20bd-4d35-8611-5181ca4a0b72' }) export class AppModuleState implements ReduxStateInterface<AppModuleStateInterface> { getInitialState(): AppModuleStateInterface { return { todo: { items: [ 'Item 1', 'Item 2' ], } }; } } ``` ###### Example 2) Promise ```ts import { ReduxState, ReduxStateInterface } from '@harmowatch/ngx-redux-core'; import { AppModuleStateInterface } from './app.module.state.interface'; @ReduxState({ name: 'app-module-7c66b613-20bd-4d35-8611-5181ca4a0b72' }) export class AppModuleState implements ReduxStateInterface<AppModuleStateInterface> { getInitialState(): Promise<AppModuleStateInterface> { return Promise.resolve({ todo: { items: [ 'Item 1', 'Item 2' ], } }); } } ``` > Note: If you return a unresolved `Promise` your state is never registered! ###### Example 3) Observable ```ts import { ReduxState, ReduxStateInterface } from '@harmowatch/ngx-redux-core'; import { AppModuleStateInterface } from './app.module.state.interface'; import { BehaviorSubject } from 'rxjs/BehaviorSubject'; import { Observable } from 'rxjs/Observable'; @ReduxState({ name: 'app-module-7c66b613-20bd-4d35-8611-5181ca4a0b72' }) export class AppModuleState implements ReduxStateInterface<AppModuleStateInterface> { getInitialState(): Observable<AppModuleStateInterface> { const subject = new BehaviorSubject<AppModuleStateInterface>({ todo: { items: [ 'Item 1', 'Item 2' ], } }); subject.complete(); return subject.asObservable(); } } ``` > Note: If you return a uncompleted `Observable` your state is never registered! ### 4. Register the state: The next thing you need to do, is to register your state. For that *ngx-redux* accepts a configuration property `state`. ###### Example ```ts import { NgModule } from '@angular/core'; import { BrowserModule } from '@angular/platform-browser'; import { ReduxModule } from '@harmowatch/ngx-redux-core'; import { AppComponent } from './app.component'; import { AppModuleState } from './app.module.state'; @NgModule({ declarations: [ AppComponent ], imports: [ BrowserModule, ReduxModule.forRoot({ state: { provider: AppModuleState, } }), ], providers: [], bootstrap: [ AppComponent ] }) export class AppModule { } ``` > Note: For lazy loaded modules you've to use `forChild`. Your redux module is ready to run now. Once your initial state was resolved, your redux module is registered to the global redux state like this: ```json { "app-module-7c66b613-20bd-4d35-8611-5181ca4a0b72": { "todo": { "items": [ "Item 1", "Item 2" ] } } } ``` ### 5. Select data from the state To select values from the state you can choose between this three options: - a [Angular Pipe](https://angular.io/guide/pipes) - a Annotation - a Class Each selector will accept a relative *todo/items* or an absolute path */app-module-7c66b613-20bd-4d35-8611-5181ca4a0b72/todo/items*. It's recommended to use relative paths only. The absolute path is only there to give you a maximum of flexibility. #### 5.1 Using the `reduxSelect` pipe The easiest way to get values from the state, is to use the `reduxSelect` pipe together with Angular's `async` pipe. The right state is determined automatically, because you're in a Angular context. ###### Example 1) Relative path (recommended) ```angular2html <pre>{{ 'todo/items' | reduxSelect | async | json }}</pre> ``` ###### Example 2) Absolute path (avoid) ```angular2html <pre>{{ '/app-module-7c66b613-20bd-4d35-8611-5181ca4a0b72/todo/items' | reduxSelect | async | json }}</pre> ``` #### 5.2 Using the `@ReduxSelect` annotation If you want to access the state values in your component you can use the `@ReduxSelect` decorator. *ngx-redux* can not determine which state you mean automatically, because decorators run outside the Angular context. For that you've to pass in a reference to your state class as 2nd argument. When you specify an absolute path, you don't need the 2nd argument anymore. ###### Example 1) Relative path (recommended) ```ts import { Component } from '@angular/core'; import { ReduxSelect } from '@harmowatch/ngx-redux-core'; import { AppModuleState } from './app.module.state'; import { Observable } from 'rxjs/Observable'; @Component({ selector: 'app-root', templateUrl: './app.component.html', styleUrls: [ './app.component.css' ] }) export class AppComponent { @ReduxSelect('todo/items', AppModuleState) private todoItems: Observable<string[]>; constructor() { this.todoItems.subscribe((items) => console.log('ITEMS', items)); } } ``` ###### Example 2) Absolute path (avoid) ```ts import { Component } from '@angular/core'; import { ReduxSelect } from '@harmowatch/ngx-redux-core'; import { Observable } from 'rxjs/Observable'; @Component({ selector: 'app-root', templateUrl: './app.component.html', styleUrls: [ './app.component.css' ] }) export class AppComponent { @ReduxSelect('/app-module-7c66b613-20bd-4d35-8611-5181ca4a0b72/todo/items') private todoItems: Observable<string[]>; constructor() { this.todoItems.subscribe((items) => console.log('ITEMS', items)); } } ``` #### 5.3 Using the `ReduxStateSelector` class ###### Example 1) Relative path (recommended) ```ts import { Component } from '@angular/core'; import { ReduxStateSelector } from '@harmowatch/ngx-redux-core'; import { AppModuleState } from './app.module.state'; @Component({ selector: 'app-root', templateUrl: './app.component.html', styleUrls: [ './app.component.css' ] }) export class AppComponent { constructor() { const selector = new ReduxStateSelector('todo/items', AppModuleState); selector.getSubject().subscribe((items) => console.log('ITEMS', items)); // or selector.getReplaySubject().subscribe((items) => console.log('ITEMS', items)); // or selector.getObservable().subscribe((items) => console.log('ITEMS', items)); // or selector.getBehaviorSubject([ 'Default Item' ]).subscribe((items) => console.log('ITEMS', items)); } } ``` ###### Example 2) Absolute path (avoid) ```ts import { Component } from '@angular/core'; import { ReduxStateSelector } from '@harmowatch/ngx-redux-core'; @Component({ selector: 'app-root', templateUrl: './app.component.html', styleUrls: [ './app.component.css' ] }) export class AppComponent { constructor() { const selector = new ReduxStateSelector('/app-module-7c66b613-20bd-4d35-8611-5181ca4a0b72/todo/items'); selector.getSubject().subscribe((items) => console.log('ITEMS', items)); // or selector.getReplaySubject().subscribe((items) => console.log('ITEMS', items)); // or selector.getObservable().subscribe((items) => console.log('ITEMS', items)); // or selector.getBehaviorSubject([ 'Default Item' ]).subscribe((items) => console.log('ITEMS', items)); } } ``` ### 6. Dispatch an [Redux Action](http://redux.js.org/docs/basics/Actions.html) To dispatch an action is very easy. Just annotate your class method by `@ReduxAction`. Everytime your method is called *ngx-redux* will dispatch a [Redux Action](http://redux.js.org/docs/basics/Actions.html) for you automatically! The `return` value of the decorated method will become the payload of the action and the name of the method is used as the action type. > Note: It's very useful to write a provider, where the action method(s) are delivered by. See the example below. ###### Example ```ts import { Injectable } from '@angular/core'; import { ReduxAction } from '@harmowatch/ngx-redux-core'; @Injectable() export class AppActions { @ReduxAction() public addTodo(todo: string): string { return todo; } } ``` Then register the provider to your module: ```ts import { NgModule } from '@angular/core'; import { BrowserModule } from '@angular/platform-browser'; import { ReduxModule } from '@harmowatch/ngx-redux-core'; import { AppActions } from './app.actions'; // (1) Add the import import { AppComponent } from './app.component'; import { AppModuleState } from './app.module.state'; @NgModule({ declarations: [ AppComponent ], imports: [ BrowserModule, ReduxModule.forRoot({ state: { provider: AppModuleState, } }), ], providers: [ AppActions ], // (2) Add to the provider list bootstrap: [ AppComponent ] }) export class AppModule { } ``` Now you can inject the provider to your component: ###### Example ```ts import { Component } from '@angular/core'; import { AppActions } from './app.actions'; @Component({ selector: 'app-root', templateUrl: './app.component.html', styleUrls: [ './app.component.css' ] }) export class AppComponent { constructor(appActions: AppActions) { appActions.addTodo('SampleTodo'); } } ``` The example above will dispatch the following action: ```json { "payload": "SampleTodo", "type": "addTodo" } ``` Ok that's cool, but there's no information in the action type that this was an `AppActions` action, right? But don't worry you can follow two different and very easy ways to fix that. ###### Example 1) Provide a action type ```ts import { Injectable } from '@angular/core'; import { ReduxAction } from '@harmowatch/ngx-redux-core'; @Injectable() export class AppActions { @ReduxAction({ type: 'AppActions://addTodo' }) public addTodo(todo: string): string { return todo; } } ``` `addTodo` will dispatch the following action from now on: ```json { "payload": "SampleTodo", "type": "AppActions://addTodo" } ``` #### Example 2) Provide a action context (recommended) ```ts import { Injectable } from '@angular/core'; import { ReduxAction, ReduxActionContext } from '@harmowatch/ngx-redux-core'; @ReduxActionContext({ prefix: 'AppActions://' }) @Injectable() export class AppActions { @ReduxAction() public addTodo(todo: string): string { return todo; } } ``` `addTodo` will dispatch the following action from now on: ```json { "type": "todo/add", "payload": "SomeTodo" } ``` ### Example 3) Combine the ReduxContext and action type ```ts import { Injectable } from '@angular/core'; import { ReduxAction, ReduxActionContext } from '@harmowatch/ngx-redux-core'; @ReduxActionContext({ prefix: 'AppActions://' }) @Injectable() export class AppActions { @ReduxAction({ type: 'add-todo' }) public addTodo(todo: string): string { return todo; } } ``` `addTodo` will dispatch the following action from now on: ```json { "payload": "SampleTodo", "type": "AppActions://add-todo" } ``` ### 7. Reduce the State We have no way to manipulate the data that are stored in the [Redux Store](http://redux.js.org/docs/basics/Store.html) yet. For that we need a reducer. ###### Example ```ts import { ActionInterface, ReduxReducer } from '@harmowatch/ngx-redux-core'; import { AppActions } from './app.actions'; import { AppModuleStateInterface } from './app.module.state.interface'; export class AppModuleReducer { @ReduxReducer(AppActions.prototype.addTodo) static addTodo(state: AppModuleStateInterface, action: ActionInterface<string>) { return { ...state, todo : { ...state.todo, items : state.todo.items.concat(action.payload) } } } } ``` The last thing you need to do, is to wire the reducer against the state: ###### Example ```ts import { NgModule } from '@angular/core'; import { BrowserModule } from '@angular/platform-browser'; import { ReduxModule } from '@harmowatch/ngx-redux-core'; import { AppActions } from './app.actions'; import { AppComponent } from './app.component'; import { AppModuleReducer } from './app.module.reducer'; // (1) Add the import import { AppModuleState } from './app.module.state'; @NgModule({ declarations: [ AppComponent ], imports: [ BrowserModule, ReduxModule.forRoot({ state: { provider: AppModuleState, reducers: [ AppModuleReducer ] // (2) Register the reducer } }), ], providers: [ AppActions ], bootstrap: [ AppComponent ] }) export class AppModule { } ``` ## Known violations / conflicts ### Redux principles #### [Changes are made with pure functions](http://redux.js.org/docs/introduction/ThreePrinciples.html#changes-are-made-with-pure-functions) One of the principles of Redux is to change the state using **pure functions**, only. Unfortunately there is **no typescript support** to decorate pure functions right now. That's the reason why *ngx-redux* uses classes where the reducer functions are shipped by. To find a viable solution the reducer functions shall be written as static methods.