@angular-redux/store
Version:
Angular bindings for Redux
668 lines (650 loc) • 22.4 kB
JavaScript
import { applyMiddleware, compose, createStore } from 'redux';
import { ApplicationRef, Injectable, NgZone, NgModule } from '@angular/core';
import { BehaviorSubject, Observable } from 'rxjs';
import { distinctUntilChanged, map, filter, switchMap } from 'rxjs/operators';
/**
* @fileoverview added by tsickle
* @suppress {checkTypes,extraRequire,missingReturn,unusedPrivateMembers,uselessCode} checked by tsc
*/
/**
* This is the public interface of \@angular-redux/store. It wraps the global
* redux store and adds a few other add on methods. It's what you'll inject
* into your Angular application as a service.
* @abstract
* @template RootState
*/
class NgRedux {
}
/**
* \@hidden, \@deprecated
*/
NgRedux.instance = undefined;
/**
* @fileoverview added by tsickle
* @suppress {checkTypes,extraRequire,missingReturn,unusedPrivateMembers,uselessCode} checked by tsc
*/
/** @type {?} */
const environment = (/** @type {?} */ ((typeof window !== 'undefined'
? window
: {})));
/**
* An angular-2-ified version of the Redux DevTools chrome extension.
*/
class DevToolsExtension {
/**
* @hidden
* @param {?} appRef
* @param {?} ngRedux
*/
constructor(appRef, ngRedux) {
this.appRef = appRef;
this.ngRedux = ngRedux;
/**
* A wrapper for the Chrome Extension Redux DevTools.
* Makes sure state changes triggered by the extension
* trigger Angular2's change detector.
*
* @argument options: dev tool options; same
* format as described here:
* [zalmoxisus/redux-devtools-extension/blob/master/docs/API/Arguments.md]
*/
this.enhancer = (options) => {
/** @type {?} */
let subscription;
if (!this.isEnabled()) {
return null;
}
// Make sure changes from dev tools update angular's view.
(/** @type {?} */ (this.getDevTools())).listen(({ type }) => {
if (type === 'START') {
subscription = this.ngRedux.subscribe(() => {
if (!NgZone.isInAngularZone()) {
this.appRef.tick();
}
});
}
else if (type === 'STOP') {
subscription();
}
});
return (/** @type {?} */ (this.getDevTools()))(options || {});
};
/**
* Returns true if the extension is installed and enabled.
*/
this.isEnabled = () => !!this.getDevTools();
/**
* Returns the redux devtools enhancer.
*/
this.getDevTools = () => environment &&
(environment.__REDUX_DEVTOOLS_EXTENSION__ || environment.devToolsExtension);
}
}
DevToolsExtension.decorators = [
{ type: Injectable }
];
/** @nocollapse */
DevToolsExtension.ctorParameters = () => [
{ type: ApplicationRef },
{ type: NgRedux }
];
/**
* @fileoverview added by tsickle
* @suppress {checkTypes,extraRequire,missingReturn,unusedPrivateMembers,uselessCode} checked by tsc
*/
/**
* Gets a deeply-nested property value from an object, given a 'path'
* of property names or array indices.
*
* @hidden
* @param {?} v
* @param {?} pathElems
* @return {?}
*/
function getIn(v, pathElems) {
if (!v) {
return v;
}
// If this is an ImmutableJS structure, use existing getIn function
if ('function' === typeof v.getIn) {
return v.getIn(pathElems);
}
const [firstElem, ...restElems] = pathElems;
if (undefined === v[firstElem]) {
return undefined;
}
if (restElems.length === 0) {
return v[firstElem];
}
return getIn(v[firstElem], restElems);
}
/**
* @fileoverview added by tsickle
* @suppress {checkTypes,extraRequire,missingReturn,unusedPrivateMembers,uselessCode} checked by tsc
*/
/**
* Sets a deeply-nested property value from an object, given a 'path'
* of property names or array indices. Path elements are created if
* not there already. Does not mutate the given object.
*
* @hidden
* @type {?}
*/
const setIn = (obj, [firstElem, ...restElems], value) => 'function' === typeof (obj[firstElem] || {}).setIn
? Object.assign({}, obj, { [firstElem]: obj[firstElem].setIn(restElems, value) }) : Object.assign({}, obj, { [firstElem]: restElems.length === 0
? value
: setIn(obj[firstElem] || {}, restElems, value) });
/**
* @fileoverview added by tsickle
* @suppress {checkTypes,extraRequire,missingReturn,unusedPrivateMembers,uselessCode} checked by tsc
*/
/** @type {?} */
let reducerMap = {};
/** @type {?} */
const composeReducers = (...reducers) => (state, action) => reducers.reduce((subState, reducer) => reducer(subState, action), state);
/**
* @param {?} rootReducer Call this on your root reducer to enable SubStore
* functionality for pre-configured stores (e.g. using NgRedux.provideStore()).
* NgRedux.configureStore
* does it for you under the hood.
* @return {?}
*/
function enableFractalReducers(rootReducer) {
reducerMap = {};
return composeReducers(rootFractalReducer, rootReducer);
}
/**
* @hidden
* @param {?} basePath
* @param {?} localReducer
* @return {?}
*/
function registerFractalReducer(basePath, localReducer) {
/** @type {?} */
const existingFractalReducer = reducerMap[JSON.stringify(basePath)];
if (existingFractalReducer && existingFractalReducer !== localReducer) {
throw new Error(`attempt to overwrite fractal reducer for basePath ${basePath}`);
}
reducerMap[JSON.stringify(basePath)] = localReducer;
}
/**
* @hidden
* @param {?} basePath
* @param {?} nextLocalReducer
* @return {?}
*/
function replaceLocalReducer(basePath, nextLocalReducer) {
reducerMap[JSON.stringify(basePath)] = nextLocalReducer;
}
/**
* @param {?=} state
* @param {?=} action
* @return {?}
*/
function rootFractalReducer(state = {}, action) {
/** @type {?} */
const fractalKey = action['@angular-redux::fractalkey'];
/** @type {?} */
const fractalPath = fractalKey ? JSON.parse(fractalKey) : [];
/** @type {?} */
const localReducer = reducerMap[fractalKey || ''];
return fractalKey && localReducer
? setIn(state, fractalPath, localReducer(getIn(state, fractalPath), action))
: state;
}
/**
* @fileoverview added by tsickle
* @suppress {checkTypes,extraRequire,missingReturn,unusedPrivateMembers,uselessCode} checked by tsc
*/
/**
* OPTIONS_KEY: this is per-class (static) and holds the config from the
* \@SubStore decorator.
* @type {?}
*/
const OPTIONS_KEY = '@angular-redux::substore::class::options';
/**
* INSTANCE_SUBSTORE_KEY, INSTANCE_SELECTIONS_KEY: these are per-instance
* (non-static) and holds references to the substores/selected observables
* to be used by an instance of a decorated class. I'm not using
* reflect-metadata here because I want
*
* 1. different instances to have different substores in the case where
* `basePathMethodName` is dynamic.
* 2. the instance substore to be garbage collected when the instance is no
* longer reachable.
* This is therefore an own-property on the actual instance of the decorated
* class.
* @type {?}
*/
const INSTANCE_SUBSTORE_KEY = '@angular-redux::substore::instance::store';
/** @type {?} */
const INSTANCE_SELECTIONS_KEY = '@angular-redux::substore::instance::selections';
/**
* Used to detect when the base path changes - this allows components to
* dynamically adjust their selections if necessary.
* @type {?}
*/
const INSTANCE_BASE_PATH_KEY = '@angular-redux::substore::instance::basepath';
/** @type {?} */
const getClassOptions = (decoratedInstance) => decoratedInstance.constructor[OPTIONS_KEY];
/**
* @hidden
* @type {?}
*/
const setClassOptions = (decoratedClassConstructor, options) => {
decoratedClassConstructor[OPTIONS_KEY] = options;
};
// I want the store to be saved on the actual instance so
// 1. different instances can have distinct substores if necessary
// 2. the substore/selections will be marked for garbage collection when the
// instance is destroyed.
/** @type {?} */
const setInstanceStore = (decoratedInstance, store) => (decoratedInstance[INSTANCE_SUBSTORE_KEY] = store);
/** @type {?} */
const getInstanceStore = (decoratedInstance) => decoratedInstance[INSTANCE_SUBSTORE_KEY];
/** @type {?} */
const getInstanceSelectionMap = (decoratedInstance) => {
/** @type {?} */
const map$$1 = decoratedInstance[INSTANCE_SELECTIONS_KEY] || {};
decoratedInstance[INSTANCE_SELECTIONS_KEY] = map$$1;
return map$$1;
};
/** @type {?} */
const hasBasePathChanged = (decoratedInstance, basePath) => decoratedInstance[INSTANCE_BASE_PATH_KEY] !== (basePath || []).toString();
/** @type {?} */
const setInstanceBasePath = (decoratedInstance, basePath) => {
decoratedInstance[INSTANCE_BASE_PATH_KEY] = (basePath || []).toString();
};
/** @type {?} */
const clearInstanceState = (decoratedInstance) => {
decoratedInstance[INSTANCE_SELECTIONS_KEY] = null;
decoratedInstance[INSTANCE_SUBSTORE_KEY] = null;
decoratedInstance[INSTANCE_BASE_PATH_KEY] = null;
};
/**
* Gets the store associated with a decorated instance (e.g. a
* component or service)
* @hidden
* @type {?}
*/
const getBaseStore = (decoratedInstance) => {
// The root store hasn't been set up yet.
if (!NgRedux.instance) {
return undefined;
}
/** @type {?} */
const options = getClassOptions(decoratedInstance);
// This is not decorated with `@WithSubStore`. Return the root store.
if (!options) {
return NgRedux.instance;
}
// Dynamic base path support:
/** @type {?} */
const basePath = decoratedInstance[options.basePathMethodName]();
if (hasBasePathChanged(decoratedInstance, basePath)) {
clearInstanceState(decoratedInstance);
setInstanceBasePath(decoratedInstance, basePath);
}
if (!basePath) {
return NgRedux.instance;
}
/** @type {?} */
const store = getInstanceStore(decoratedInstance);
if (!store) {
setInstanceStore(decoratedInstance, NgRedux.instance.configureSubStore(basePath, options.localReducer));
}
return getInstanceStore(decoratedInstance);
};
/**
* Creates an Observable from the given selection parameters,
* rooted at decoratedInstance's store, and caches it on the
* instance for future use.
* @hidden
* @type {?}
*/
const getInstanceSelection = (decoratedInstance, key, selector, transformer, comparator) => {
/** @type {?} */
const store = getBaseStore(decoratedInstance);
if (store) {
/** @type {?} */
const selections = getInstanceSelectionMap(decoratedInstance);
selections[key] =
selections[key] ||
(!transformer
? store.select(selector, comparator)
: store.select(selector).pipe(obs$ => transformer(obs$, decoratedInstance), distinctUntilChanged(comparator)));
return selections[key];
}
return undefined;
};
/**
* @fileoverview added by tsickle
* @suppress {checkTypes,extraRequire,missingReturn,unusedPrivateMembers,uselessCode} checked by tsc
*/
/**
* Auto-dispatches the return value of the decorated function.
*
* Decorate a function creator method with \@dispatch and its return
* value will automatically be passed to ngRedux.dispatch() for you.
* @return {?}
*/
function dispatch() {
return function decorate(target, key, descriptor) {
/** @type {?} */
let originalMethod;
/** @type {?} */
const wrapped = function (...args) {
/** @type {?} */
const result = originalMethod.apply(this, args);
if (result !== undefined) {
/** @type {?} */
const store = getBaseStore(this) || NgRedux.instance;
if (store) {
store.dispatch(result);
}
}
return result;
};
descriptor = descriptor || Object.getOwnPropertyDescriptor(target, key);
if (descriptor === undefined) {
/** @type {?} */
const dispatchDescriptor = {
get: () => wrapped,
set: setMethod => (originalMethod = setMethod),
};
Object.defineProperty(target, key, dispatchDescriptor);
return dispatchDescriptor;
}
else {
originalMethod = descriptor.value;
descriptor.value = wrapped;
return descriptor;
}
};
}
/**
* @fileoverview added by tsickle
* @suppress {checkTypes,extraRequire,missingReturn,unusedPrivateMembers,uselessCode} checked by tsc
*/
/**
* Selects an observable from the store, and attaches it to the decorated
* property.
*
* ```ts
* import { select } from '\@angular-redux/store';
*
* class SomeClass {
* \@select(['foo','bar']) foo$: Observable<string>
* }
* ```
*
* @template T
* @param {?=} selector
* A selector function, property name string, or property name path
* (array of strings/array indices) that locates the store data to be
* selected
*
* @param {?=} comparator Function used to determine if this selector has changed.
* @return {?}
*/
function select(selector, comparator) {
return (target, key) => {
/** @type {?} */
const adjustedSelector = selector
? selector
: String(key).lastIndexOf('$') === String(key).length - 1
? String(key).substring(0, String(key).length - 1)
: key;
decorate(adjustedSelector, undefined, comparator)(target, key);
};
}
/**
* Selects an observable using the given path selector, and runs it through the
* given transformer function. A transformer function takes the store
* observable as an input and returns a derived observable from it. That derived
* observable is run through distinctUntilChanges with the given optional
* comparator and attached to the store property.
*
* Think of a Transformer as a FunctionSelector that operates on observables
* instead of values.
*
* ```ts
* import { select$ } from 'angular-redux/store';
*
* export const debounceAndTriple = obs$ => obs$
* .debounce(300)
* .map(x => 3 * x);
*
* class Foo {
* \@select$(['foo', 'bar'], debounceAndTriple)
* readonly debouncedFooBar$: Observable<number>;
* }
* ```
* @template T
* @param {?} selector
* @param {?} transformer
* @param {?=} comparator
* @return {?}
*/
function select$(selector, transformer, comparator) {
return decorate(selector, transformer, comparator);
}
/**
* @param {?} selector
* @param {?=} transformer
* @param {?=} comparator
* @return {?}
*/
function decorate(selector, transformer, comparator) {
return function decorator(target, key) {
/**
* @this {?}
* @return {?}
*/
function getter() {
return getInstanceSelection(this, key, selector, transformer, comparator);
}
// Replace decorated property with a getter that returns the observable.
if (delete target[key]) {
Object.defineProperty(target, key, {
get: getter,
enumerable: true,
configurable: true,
});
}
};
}
/**
* @fileoverview added by tsickle
* @suppress {checkTypes,extraRequire,missingReturn,unusedPrivateMembers,uselessCode} checked by tsc
*/
/**
* Modifies the behaviour of any `\@select`, `\@select$`, or `\@dispatch`
* decorators to operate on a substore defined by the IFractalStoreOptions.
*
* See:
* https://github.com/angular-redux/platform/blob/master/packages/store/articles/fractal-store.md
* for more information about SubStores.
* @param {?} __0
* @return {?}
*/
function WithSubStore({ basePathMethodName, localReducer, }) {
return function decorate(constructor) {
setClassOptions(constructor, {
basePathMethodName,
localReducer,
});
};
}
/**
* @fileoverview added by tsickle
* @suppress {checkTypes,extraRequire,missingReturn,unusedPrivateMembers,uselessCode} checked by tsc
*/
/**
* @hidden
* @type {?}
*/
const assert = (condition, message) => {
if (!condition) {
throw new Error(message);
}
};
/**
* @fileoverview added by tsickle
* @suppress {checkTypes,extraRequire,missingReturn,unusedPrivateMembers,uselessCode} checked by tsc
*/
/**
* @hidden
* @type {?}
*/
const sniffSelectorType = (selector) => !selector
? 'nil'
: Array.isArray(selector)
? 'path'
: 'function' === typeof selector
? 'function'
: 'property';
/**
* @hidden
* @type {?}
*/
const resolver = (selector) => ({
property: (state) => state ? state[(/** @type {?} */ (selector))] : undefined,
path: (state) => getIn(state, (/** @type {?} */ (selector))),
function: (/** @type {?} */ (selector)),
nil: (state) => state,
});
/**
* @hidden
* @type {?}
*/
const resolveToFunctionSelector = (selector) => resolver(selector)[sniffSelectorType(selector)];
/**
* @fileoverview added by tsickle
* @suppress {checkTypes,extraRequire,missingReturn,unusedPrivateMembers,uselessCode} checked by tsc
*/
/**
* @hidden
* @template State
*/
class SubStore {
/**
* @param {?} rootStore
* @param {?} basePath
* @param {?} localReducer
*/
constructor(rootStore, basePath, localReducer) {
this.rootStore = rootStore;
this.basePath = basePath;
this.dispatch = action => this.rootStore.dispatch(Object.assign({}, ((/** @type {?} */ (action))), { '@angular-redux::fractalkey': JSON.stringify(this.basePath) }));
this.getState = () => getIn(this.rootStore.getState(), this.basePath);
this.configureSubStore = (basePath, localReducer) => new SubStore(this.rootStore, [...this.basePath, ...basePath], localReducer);
this.select = (selector, comparator) => this.rootStore.select(this.basePath).pipe(map(resolveToFunctionSelector(selector)), distinctUntilChanged(comparator));
this.subscribe = (listener) => {
/** @type {?} */
const subscription = this.select().subscribe(listener);
return () => subscription.unsubscribe();
};
this.replaceReducer = (nextLocalReducer) => replaceLocalReducer(this.basePath, nextLocalReducer);
registerFractalReducer(basePath, localReducer);
}
}
/**
* @fileoverview added by tsickle
* @suppress {checkTypes,extraRequire,missingReturn,unusedPrivateMembers,uselessCode} checked by tsc
*/
/**
* @hidden
* @template RootState
*/
class RootStore extends NgRedux {
/**
* @param {?} ngZone
*/
constructor(ngZone) {
super();
this.ngZone = ngZone;
this.store = undefined;
this.configureStore = (rootReducer, initState, middleware = [], enhancers = []) => {
assert(!this.store, 'Store already configured!');
// Variable-arity compose in typescript FTW.
this.setStore(compose(applyMiddleware(...middleware), ...enhancers)(createStore)(enableFractalReducers(rootReducer), initState));
};
this.provideStore = (store) => {
assert(!this.store, 'Store already configured!');
this.setStore(store);
};
this.getState = () => (/** @type {?} */ (this.store)).getState();
this.subscribe = (listener) => (/** @type {?} */ (this.store)).subscribe(listener);
this.replaceReducer = (nextReducer) => {
(/** @type {?} */ (this.store)).replaceReducer(nextReducer);
};
this.dispatch = (action) => {
assert(!!this.store, 'Dispatch failed: did you forget to configure your store? ' +
'https://github.com/angular-redux/platform/blob/master/packages/store/' +
'README.md#quick-start');
if (!NgZone.isInAngularZone()) {
return this.ngZone.run(() => (/** @type {?} */ (this.store)).dispatch(action));
}
else {
return (/** @type {?} */ (this.store)).dispatch(action);
}
};
this.select = (selector, comparator) => this.store$.pipe(distinctUntilChanged(), map(resolveToFunctionSelector(selector)), distinctUntilChanged(comparator));
this.configureSubStore = (basePath, localReducer) => new SubStore(this, basePath, localReducer);
this.storeToObservable = (store) => new Observable((observer) => {
observer.next(store.getState());
/** @type {?} */
const unsubscribeFromRedux = store.subscribe(() => observer.next(store.getState()));
return () => {
unsubscribeFromRedux();
observer.complete();
};
});
NgRedux.instance = this;
this.store$ = (/** @type {?} */ (new BehaviorSubject(undefined).pipe(filter(n => n !== undefined), switchMap(observableStore => (/** @type {?} */ (observableStore))))));
}
/**
* @private
* @param {?} store
* @return {?}
*/
setStore(store) {
this.store = store;
/** @type {?} */
const storeServable = this.storeToObservable(store);
this.store$.next((/** @type {?} */ (storeServable)));
}
}
/**
* @fileoverview added by tsickle
* @suppress {checkTypes,extraRequire,missingReturn,unusedPrivateMembers,uselessCode} checked by tsc
*/
/**
* @hidden
* @param {?} ngZone
* @return {?}
*/
function _ngReduxFactory(ngZone) {
return new RootStore(ngZone);
}
class NgReduxModule {
}
NgReduxModule.decorators = [
{ type: NgModule, args: [{
providers: [
DevToolsExtension,
{ provide: NgRedux, useFactory: _ngReduxFactory, deps: [NgZone] },
],
},] }
];
/**
* @fileoverview added by tsickle
* @suppress {checkTypes,extraRequire,missingReturn,unusedPrivateMembers,uselessCode} checked by tsc
*/
/**
* @fileoverview added by tsickle
* @suppress {checkTypes,extraRequire,missingReturn,unusedPrivateMembers,uselessCode} checked by tsc
*/
export { NgRedux, NgReduxModule, DevToolsExtension, enableFractalReducers, select, select$, dispatch, WithSubStore, RootStore as ɵb, _ngReduxFactory as ɵa };
//# sourceMappingURL=angular-redux-store.js.map