@ngrx/effects
Advanced tools
Comparing version 1.1.1 to 2.0.0-beta.1
@@ -1,5 +0,4 @@ | ||
import 'rxjs/add/observable/merge'; | ||
import { Store } from '@ngrx/store'; | ||
import { Observable } from 'rxjs/Observable'; | ||
export declare function mergeEffects(...instances: any[]): Observable<any>; | ||
export declare function connectEffectsToStore(store: Store<any>, effects: any[]): () => Promise<boolean>; | ||
export declare function Effect(): PropertyDecorator; | ||
export declare function getEffectKeys(instance: any): string[]; | ||
export declare function mergeEffects(instance: any): Observable<any>; |
"use strict"; | ||
require('rxjs/add/observable/merge'); | ||
var Observable_1 = require('rxjs/Observable'); | ||
var metadata_1 = require('./metadata'); | ||
var util_1 = require('./util'); | ||
function mergeEffects() { | ||
var instances = []; | ||
for (var _i = 0; _i < arguments.length; _i++) { | ||
instances[_i - 0] = arguments[_i]; | ||
var merge_1 = require('rxjs/observable/merge'); | ||
var METADATA_KEY = '@ngrx/effects'; | ||
function Effect() { | ||
return function (target, propertyName) { | ||
if (!Reflect.hasOwnMetadata(METADATA_KEY, target)) { | ||
Reflect.defineMetadata(METADATA_KEY, [], target); | ||
} | ||
var effects = Reflect.getOwnMetadata(METADATA_KEY, target); | ||
Reflect.defineMetadata(METADATA_KEY, effects.concat([propertyName]), target); | ||
}; | ||
} | ||
exports.Effect = Effect; | ||
function getEffectKeys(instance) { | ||
var target = Object.getPrototypeOf(instance); | ||
if (!Reflect.hasOwnMetadata(METADATA_KEY, target)) { | ||
return []; | ||
} | ||
var observables = util_1.flatten(instances).map(function (i) { return metadata_1.getEffectKeys(i).map(function (key) { | ||
if (typeof i[key] === 'function') { | ||
return i[key](); | ||
return Reflect.getOwnMetadata(METADATA_KEY, target); | ||
} | ||
exports.getEffectKeys = getEffectKeys; | ||
function mergeEffects(instance) { | ||
var observables = getEffectKeys(instance).map(function (key) { | ||
if (typeof instance[key] === 'function') { | ||
return instance[key](); | ||
} | ||
return i[key]; | ||
}); }); | ||
return Observable_1.Observable.merge.apply(Observable_1.Observable, util_1.flatten(observables)); | ||
return instance[key]; | ||
}); | ||
return merge_1.merge.apply(void 0, observables); | ||
} | ||
exports.mergeEffects = mergeEffects; | ||
function connectEffectsToStore(store, effects) { | ||
return function () { | ||
mergeEffects.apply(void 0, effects).subscribe(store); | ||
return Promise.resolve(true); | ||
}; | ||
} | ||
exports.connectEffectsToStore = connectEffectsToStore; |
@@ -1,5 +0,3 @@ | ||
export { Effect } from './metadata'; | ||
export { mergeEffects } from './effects'; | ||
export { runEffects } from './ng2'; | ||
export { StateUpdate, StateUpdates } from './state-updates'; | ||
export { toPayload, all } from './util'; | ||
export { Effect, mergeEffects } from './effects'; | ||
export { Actions } from './actions'; | ||
export { EffectsModule } from './module'; |
14
index.js
"use strict"; | ||
var metadata_1 = require('./metadata'); | ||
exports.Effect = metadata_1.Effect; | ||
var effects_1 = require('./effects'); | ||
exports.Effect = effects_1.Effect; | ||
exports.mergeEffects = effects_1.mergeEffects; | ||
var ng2_1 = require('./ng2'); | ||
exports.runEffects = ng2_1.runEffects; | ||
var state_updates_1 = require('./state-updates'); | ||
exports.StateUpdates = state_updates_1.StateUpdates; | ||
var util_1 = require('./util'); | ||
exports.toPayload = util_1.toPayload; | ||
exports.all = util_1.all; | ||
var actions_1 = require('./actions'); | ||
exports.Actions = actions_1.Actions; | ||
var module_1 = require('./module'); | ||
exports.EffectsModule = module_1.EffectsModule; |
{ | ||
"name": "@ngrx/effects", | ||
"version": "1.1.1", | ||
"version": "2.0.0-beta.1", | ||
"description": "Side effect model for @ngrx/store", | ||
@@ -16,19 +16,16 @@ "main": "index.js", | ||
"typings": "typings install", | ||
"clean": "npm-run-all clean:*", | ||
"clean:release": "rm -rf ./release", | ||
"clean:typings": "rm -rf ./typings", | ||
"prebuild": "npm-run-all clean typings karma", | ||
"build": "npm-run-all build:cjs build:es6", | ||
"build:cjs": "tsc --p tsconfig.es5.json --diagnostics --pretty", | ||
"build:es6": "tsc -m es2015 --outDir ./release/esm --target ES6 -d --diagnostics --pretty", | ||
"prepare": "npm-run-all prepare:*", | ||
"prepare:es6": "cp -R ./release/esm ./release/npm", | ||
"prepare:package": "cp ./{package.json,README.md,LICENSE} ./release/npm", | ||
"clean": "rimraf ./release", | ||
"prebuild": "npm-run-all clean karma", | ||
"build": "npm-run-all build:*", | ||
"build:cjs": "ngc", | ||
"build:esm": "ngc -p tsconfig.es2015.json", | ||
"build:ts": "cpy src/*.ts release/src", | ||
"prepare": "cpy ./{package.json,README.md,CHANGELOG.md,LICENSE} ./release", | ||
"test": "npm-run-all clean typings karma", | ||
"karma": "karma start --single-run", | ||
"karma:watch": "karma start", | ||
"changelog": "conventional-changelog -p angular -i CHANGELOG.md -s -r 0", | ||
"postbuild": "npm run prepare", | ||
"changelog": "conventional-changelog -p angular -i CHANGELOG.md -s", | ||
"preversion": "npm run test", | ||
"version": "npm run changelog && git add CHANGELOG.md" | ||
"version": "npm run changelog && git add CHANGELOG.md", | ||
"postversion": "npm run prepare" | ||
}, | ||
@@ -41,3 +38,3 @@ "authors": [ | ||
"rxjs": "5.0.0-beta.6", | ||
"@angular/core": "^2.0.0-rc.1", | ||
"@angular/core": "^2.0.0-rc.5", | ||
"@ngrx/store": "^2.0.0" | ||
@@ -48,6 +45,9 @@ }, | ||
"@angular/compiler": "^2.0.0-rc.1", | ||
"@angular/compiler-cli": "^0.5.0", | ||
"@angular/core": "^2.0.0-rc.1", | ||
"@angular/platform-browser": "^2.0.0-rc.1", | ||
"@angular/platform-browser-dynamic": "^2.0.0-rc.1", | ||
"@angular/platform-server": "^2.0.0-rc.5", | ||
"@ngrx/core": "^1.0.0", | ||
"@types/jasmine": "^2.2.31", | ||
"conventional-changelog-cli": "^1.1.1", | ||
@@ -71,4 +71,3 @@ "core-js": "^2.2.2", | ||
"tslint": "^3.6.0", | ||
"typescript": "^1.8.9", | ||
"typings": "^0.7.12", | ||
"typescript": "^2.0.0", | ||
"webpack": "^1.12.14", | ||
@@ -75,0 +74,0 @@ "zone.js": "^0.6.8" |
200
README.md
@@ -15,26 +15,34 @@ # @ngrx/effects | ||
## Effects | ||
In @ngrx/effects, effects are _sources of actions_. You use the `@Effect()` decorator to hint which observables on a service are action sources, and @ngrx/effects automatically connects your action sources to your store | ||
In @ngrx/effects, effects are _sources of actions_. You use the `@Effect()` decorator to hint which observables on a service are action sources, and @ngrx/effects automatically merges your action streams letting you subscribe them to store. | ||
To help you compose new action sources, @ngrx/effects exports a `StateUpdates` observable service that emits a `StateUpdate` object, containing the current state and dispatched action, for each state update. | ||
To help you compose new action sources, @ngrx/effects exports an `Actions` observable service that emits every action dispatched in your application. | ||
__*Note: Even if there are no changes in your state, every action will cause state to update!*__ | ||
### Example | ||
1. Create an AuthEffects service that describes a source of login actions: | ||
```ts | ||
import { Injectable } from '@angular/core'; | ||
import { Injectable, OnDestroy } from '@angular/core'; | ||
import { Observable } from 'rxjs/Observable'; | ||
import { Action } from '@ngrx/store'; | ||
import { StateUpdates, Effect } from '@ngrx/effects' | ||
import { Subscription } from 'rxjs/Subscription'; | ||
import { Action, Store } from '@ngrx/store'; | ||
import { Actions, Effect, mergeEffects } from '@ngrx/effects'; | ||
@Injectable() | ||
export class AuthEffects { | ||
constructor(private http: Http, private updates$: StateUpdates<any>) { } | ||
export class AuthEffects implements OnDestroy { | ||
subscription: Subscription; | ||
@Effect() login$ = this.updates$ | ||
constructor( | ||
private http: Http, | ||
private actions$: Actions, | ||
private store: Store<State> | ||
) { | ||
// Merge all effects and subscribe them to the store | ||
this.subscription = mergeEffects(this).subscribe(store); | ||
} | ||
@Effect() login$ = this.actions$ | ||
// Listen for the 'LOGIN' action | ||
.whenAction('LOGIN') | ||
.ofType('LOGIN') | ||
// Map the payload into JSON to use as the request body | ||
.map(update => JSON.stringify(update.action.payload)) | ||
.map(action => JSON.stringify(action.payload)) | ||
.switchMap(payload => this.http.post('/auth', payload) | ||
@@ -46,165 +54,25 @@ // If successful, dispatch success action with result | ||
); | ||
// You MUST implement an ngOnDestroy method for your effects to | ||
// automatically start. Use ngOnDestroy to cleanup running effects. | ||
ngOnDestroy() { | ||
this.subscription.unsubscribe(); | ||
} | ||
} | ||
``` | ||
2. Run your effects during application bootstrap: | ||
2. Provide your service in your component's providers array or in an `NgModule` providers array to automatically start your effects: | ||
```ts | ||
import { runEffects } from '@ngrx/effects'; | ||
import { AuthEffects } from './effects/auth'; | ||
bootstrap(App, [ | ||
provideStore(reducer), | ||
runEffects(AuthEffects) | ||
]); | ||
``` | ||
@NgModule({ | ||
providers: [ AuthEffects ] | ||
}) | ||
export class AppModule { } | ||
``` | ||
### Dynamically Running Effects | ||
The `@Effect()` decorator provides metadata to hint which observables on a class should be connected to `Store`. If you want to dynamically run an effect inject the effect class and subscribe the effect to `Store` manually: | ||
```ts | ||
@Injectable() | ||
export class AuthEffects { | ||
@Effect() login$ = this.updates$ | ||
.whenAction('LOGIN') | ||
.mergeMap(...) | ||
} | ||
@Component({ | ||
providers: [ | ||
AuthEffects | ||
] | ||
}) | ||
export class SomeCmp { | ||
subscription: Subscription; | ||
constructor(store: Store<State>, authEffects: AuthEffects) { | ||
this.subscription = authEffects.login$.subscribe(store); | ||
} | ||
} | ||
``` | ||
Unsubscribe to stop the effect: | ||
```ts | ||
ngOnDestroy() { | ||
this.subscription.unsubscribe(); | ||
} | ||
``` | ||
#### Starting Multiple Effects | ||
If you don't want to connect each source manually, you can use the `mergeEffects()` helper function to automatically merge all decorated effects across any number of effect services: | ||
```ts | ||
import { OpaqueToken, Inject } from '@angular/core'; | ||
import { mergeEffects } from '@ngrx/effects'; | ||
const EFFECTS = new OpaqueToken('Effects'); | ||
@Component({ | ||
providers: [ | ||
provide(EFFECTS, { multi: true, useClass: AuthEffects }), | ||
provide(EFFECTS, { multi: true, useClass: AccountEffects }), | ||
provide(EFFECTS, { multi: true, useClass: UserEffects }) | ||
] | ||
}) | ||
export class SomeCmp { | ||
constructor(@Inject(EFFECTS) effects: any[], store: Store<State>) { | ||
mergeEffects(effects).subscribe(store); | ||
} | ||
} | ||
``` | ||
### Testing Effects | ||
To test your effects mock out your effect's dependencies and use the `MockStateUpdates` service to send actions and state changes to your effect: | ||
```ts | ||
import { | ||
MOCK_EFFECTS_PROVIDERS, | ||
MockStateUpdates | ||
} from '@ngrx/effects/testing'; | ||
describe('Auth Effects', function() { | ||
let auth: AuthEffects; | ||
let updates$: MockStateUpdates; | ||
beforeEach(function() { | ||
const injector = ReflectiveInjector.resolveAndCreate([ | ||
AuthEffects, | ||
MOCK_EFFECTS_PROVIDERS, | ||
// Mock out other dependencies (like Http) here | ||
]); | ||
auth = injector.get(AuthEffects); | ||
updates$ = injector.get(MockStateUpdates); | ||
}); | ||
it('should respond in a certain way', function() { | ||
// Add an action in the updates queue | ||
updates$.sendAction({ type: 'LOGIN', payload: { ... } }); | ||
auth.login$.subscribe(function(action) { | ||
/* assert here */ | ||
}); | ||
}); | ||
}); | ||
``` | ||
You can use `MockStateUpdates@sendAction(action)` to send an action with an empty state, `MockStateUpdates@sendState(state)` to send a state change with an empty action, and `MockStateUpdates@send(state, action)` to send both a state change and an action. Note that `MockStateUpdates` is a replay subject with an infinite buffer size letting you queue up multiple actions / state changes to be sent to your effect. | ||
### Migrating from store-saga | ||
@ngrx/effects is heavily inspired by store-saga making it easy to translate sagas into effects. | ||
#### Rewriting Sagas | ||
In store-saga, an `iterable$` observable containing state/action pairs was provided to your saga factory function. Typically you would use the `filter` operator and the `whenAction` helper to listen for specific actions to occur. In @ngrx/effects, an observable named `StateUpdates` offers similar functionality and can be injected into an effect class. To listen to specific actions, @ngrx/effects includes a special `whenAction` operator on the `StateUpdates` observable. | ||
Before: | ||
```ts | ||
// ... other needed imports here ... | ||
import { createSaga, whenAction, toPayload } from 'store-saga'; | ||
const login$ = createSaga(function(http: Http) { | ||
return iterable$ => iterable$ | ||
.filter(whenAction('LOGIN')) | ||
.map(iteration => JSON.stringify(iteration.action.payload)) | ||
.mergeMap(body => http.post('/auth', body) | ||
.map(res => ({ | ||
type: 'LOGIN_SUCCESS', | ||
payload: res.json() | ||
})) | ||
.catch(err => Observable.of({ | ||
type: 'LOGIN_ERROR', | ||
payload: err.json() | ||
})) | ||
); | ||
}, [ Http ]); | ||
``` | ||
After: | ||
```ts | ||
// ... other needed imports here ... | ||
import { Effect, toPayload, StateUpdates } from '@ngrx/effects'; | ||
@Injectable() | ||
export class AuthEffects { | ||
constructor(private http: Http, private updates$: StateUpdates<State>) { } | ||
@Effect() login$ = this.updates$ | ||
.whenAction('LOGIN') | ||
.map(update => JSON.stringify(update.action.payload)) | ||
.mergeMap(body => this.http.post('/auth', body) | ||
.map(res => ({ | ||
type: 'LOGIN_SUCCESS', | ||
payload: res.json() | ||
})) | ||
.catch(err => Observable.of({ | ||
type: 'LOGIN_ERROR', | ||
payload: err.json() | ||
})) | ||
); | ||
} | ||
``` | ||
WIP |
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
Major refactor
Supply chain riskPackage has recently undergone a major refactor. It may be unstable or indicate significant internal changes. Use caution when updating to versions that include significant changes.
Found 1 instance in 1 package
No v1
QualityPackage is not semver >=1. This means it is not stable and does not support ^ ranges.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
32
20633
30
300
2
77
1