(
+ type: T,
+ config?: { _as: 'props' } | Creator
+): Creator {
+ if (typeof config === 'function') {
+ return defineType(type, (...args: unknown[]) => ({
+ ...config(...args),
+ type,
+ }));
+ }
+ const as = config ? config._as : 'empty';
+ switch (as) {
+ case 'empty':
+ return defineType(type, () => ({ type }));
+ case 'props':
+ return defineType(type, (props: unknown) => ({
+ ...(props as object),
+ type,
+ }));
+ default:
+ throw new Error('Unexpected config.');
+ }
+}
+
+export function props(): { _as: 'props'; _p: P } {
+ return { _as: 'props', _p: undefined! };
+}
+
+export function union<
+ C extends { [key: string]: ActionCreator }
+>(creators: C): ReturnType {
+ return undefined!;
+}
+
+function defineType(type: string, creator: Creator): Creator {
+ return Object.defineProperty(creator, 'type', {
+ value: type,
+ writable: false,
+ });
+}
diff --git a/modules/store/src/index.ts b/modules/store/src/index.ts
index ce02914c93..6db51ad83a 100644
--- a/modules/store/src/index.ts
+++ b/modules/store/src/index.ts
@@ -1,12 +1,15 @@
export {
Action,
+ ActionCreator,
ActionReducer,
ActionReducerMap,
ActionReducerFactory,
+ Creator,
MetaReducer,
Selector,
SelectorWithProps,
} from './models';
+export { createAction, props, union } from './action_creator';
export { Store, select } from './store';
export { combineReducers, compose, createReducerFactory } from './utils';
export { ActionsSubject, INIT } from './actions_subject';
@@ -43,6 +46,7 @@ export {
_FEATURE_REDUCERS,
FEATURE_REDUCERS,
_FEATURE_REDUCERS_TOKEN,
+ USER_PROVIDED_META_REDUCERS,
} from './tokens';
export {
StoreModule,
diff --git a/modules/store/src/meta-reducers/action_serialization_reducer.ts b/modules/store/src/meta-reducers/action_serialization_reducer.ts
new file mode 100644
index 0000000000..d01452b41c
--- /dev/null
+++ b/modules/store/src/meta-reducers/action_serialization_reducer.ts
@@ -0,0 +1,13 @@
+import { ActionReducer } from '../models';
+import { getUnserializable, throwIfUnserializable } from './utils';
+
+export function actionSerializationCheckMetaReducer(
+ reducer: ActionReducer
+): ActionReducer {
+ return function(state, action) {
+ const unserializable = getUnserializable(action);
+ throwIfUnserializable(unserializable, 'action');
+
+ return reducer(state, action);
+ };
+}
diff --git a/modules/store/src/meta-reducers/immutability_reducer.ts b/modules/store/src/meta-reducers/immutability_reducer.ts
new file mode 100644
index 0000000000..44616268b7
--- /dev/null
+++ b/modules/store/src/meta-reducers/immutability_reducer.ts
@@ -0,0 +1,32 @@
+import { ActionReducer } from '../models';
+import { isFunction, hasOwnProperty, isObjectLike } from './utils';
+
+export function immutabilityCheckMetaReducer(
+ reducer: ActionReducer
+): ActionReducer {
+ return function(state, action) {
+ const nextState = reducer(state, freeze(action));
+ return freeze(nextState);
+ };
+}
+
+function freeze(target: any) {
+ Object.freeze(target);
+
+ const targetIsFunction = isFunction(target);
+
+ Object.getOwnPropertyNames(target).forEach(prop => {
+ const propValue = target[prop];
+ if (
+ hasOwnProperty(target, prop) && targetIsFunction
+ ? prop !== 'caller' && prop !== 'callee' && prop !== 'arguments'
+ : true &&
+ (isObjectLike(propValue) || isFunction(propValue)) &&
+ !Object.isFrozen(propValue)
+ ) {
+ freeze(propValue);
+ }
+ });
+
+ return target;
+}
diff --git a/modules/store/src/meta-reducers/index.ts b/modules/store/src/meta-reducers/index.ts
new file mode 100644
index 0000000000..425789d973
--- /dev/null
+++ b/modules/store/src/meta-reducers/index.ts
@@ -0,0 +1,7 @@
+export {
+ stateSerializationCheckMetaReducer,
+} from './state_serialization_reducer';
+export {
+ actionSerializationCheckMetaReducer,
+} from './action_serialization_reducer';
+export { immutabilityCheckMetaReducer } from './immutability_reducer';
diff --git a/modules/store/src/meta-reducers/state_serialization_reducer.ts b/modules/store/src/meta-reducers/state_serialization_reducer.ts
new file mode 100644
index 0000000000..4b8fe2b2ce
--- /dev/null
+++ b/modules/store/src/meta-reducers/state_serialization_reducer.ts
@@ -0,0 +1,15 @@
+import { ActionReducer } from '../models';
+import { getUnserializable, throwIfUnserializable } from './utils';
+
+export function stateSerializationCheckMetaReducer(
+ reducer: ActionReducer
+): ActionReducer {
+ return function(state, action) {
+ const nextState = reducer(state, action);
+
+ const unserializable = getUnserializable(nextState);
+ throwIfUnserializable(unserializable, 'state');
+
+ return nextState;
+ };
+}
diff --git a/modules/store/src/meta-reducers/utils.ts b/modules/store/src/meta-reducers/utils.ts
new file mode 100644
index 0000000000..b3e1af2735
--- /dev/null
+++ b/modules/store/src/meta-reducers/utils.ts
@@ -0,0 +1,111 @@
+export function getUnserializable(
+ target?: any,
+ path: string[] = []
+): false | { path: string[]; value: any } {
+ // Guard against undefined and null, e.g. a reducer that returns undefined
+ if ((isUndefined(target) || isNull(target)) && path.length === 0) {
+ return {
+ path: ['root'],
+ value: target,
+ };
+ }
+
+ const keys = Object.keys(target);
+ return keys.reduce((result, key) => {
+ if (result) {
+ return result;
+ }
+
+ const value = (target as any)[key];
+
+ if (
+ isUndefined(value) ||
+ isNull(value) ||
+ isNumber(value) ||
+ isBoolean(value) ||
+ isString(value) ||
+ isArray(value)
+ ) {
+ return false;
+ }
+
+ if (isPlainObject(value)) {
+ return getUnserializable(value, [...path, key]);
+ }
+
+ return {
+ path: [...path, key],
+ value,
+ };
+ }, false);
+}
+
+export function throwIfUnserializable(
+ unserializable: false | { path: string[]; value: any },
+ context: 'state' | 'action'
+) {
+ if (unserializable === false) {
+ return;
+ }
+
+ const unserializablePath = unserializable.path.join('.');
+ const error: any = new Error(
+ `Detected unserializable ${context} at "${unserializablePath}"`
+ );
+ error.value = unserializable.value;
+ error.unserializablePath = unserializablePath;
+ throw error;
+}
+
+/**
+ * Object Utilities
+ */
+
+export function isUndefined(target: any): target is undefined {
+ return target === undefined;
+}
+
+export function isNull(target: any): target is null {
+ return target === null;
+}
+
+export function isArray(target: any): target is Array {
+ return Array.isArray(target);
+}
+
+export function isString(target: any): target is string {
+ return typeof target === 'string';
+}
+
+export function isBoolean(target: any): target is boolean {
+ return typeof target === 'boolean';
+}
+
+export function isNumber(target: any): target is number {
+ return typeof target === 'number';
+}
+
+export function isObjectLike(target: any): target is object {
+ return typeof target === 'object' && target !== null;
+}
+
+export function isObject(target: any): target is object {
+ return isObjectLike(target) && !isArray(target);
+}
+
+export function isPlainObject(target: any): target is object {
+ if (!isObject(target)) {
+ return false;
+ }
+
+ const targetPrototype = Object.getPrototypeOf(target);
+ return targetPrototype === Object.prototype || targetPrototype === null;
+}
+
+export function isFunction(target: any): target is Function {
+ return typeof target === 'function';
+}
+
+export function hasOwnProperty(target: object, propertyName: string): boolean {
+ return Object.prototype.hasOwnProperty.call(target, propertyName);
+}
diff --git a/modules/store/src/models.ts b/modules/store/src/models.ts
index 34413a0139..43e13f104b 100644
--- a/modules/store/src/models.ts
+++ b/modules/store/src/models.ts
@@ -2,6 +2,11 @@ export interface Action {
type: string;
}
+// declare to make it property-renaming safe
+export declare interface TypedAction extends Action {
+ readonly type: T;
+}
+
export type TypeId = () => T;
export type InitialState = Partial | TypeId> | void;
@@ -21,7 +26,7 @@ export interface ActionReducerFactory {
): ActionReducer;
}
-export type MetaReducer = (
+export type MetaReducer = (
reducer: ActionReducer
) => ActionReducer;
@@ -39,3 +44,21 @@ export type SelectorWithProps = (
state: State,
props: Props
) => Result;
+
+export type Creator = (...args: any[]) => object;
+
+export type ActionCreator = C &
+ TypedAction;
+
+export type FunctionWithParametersType = (
+ ...args: P
+) => R;
+
+export type ParametersType = T extends (...args: infer U) => unknown
+ ? U
+ : never;
+export interface RuntimeChecks {
+ strictStateSerializability: boolean;
+ strictActionSerializability: boolean;
+ strictImmutability: boolean;
+}
diff --git a/modules/store/src/runtime_checks.ts b/modules/store/src/runtime_checks.ts
new file mode 100644
index 0000000000..674b9abed2
--- /dev/null
+++ b/modules/store/src/runtime_checks.ts
@@ -0,0 +1,95 @@
+import { isDevMode, Provider } from '@angular/core';
+import {
+ stateSerializationCheckMetaReducer,
+ actionSerializationCheckMetaReducer,
+ immutabilityCheckMetaReducer,
+} from './meta-reducers';
+import { RuntimeChecks, MetaReducer } from './models';
+import {
+ _USER_RUNTIME_CHECKS,
+ _ACTIVE_RUNTIME_CHECKS,
+ META_REDUCERS,
+} from './tokens';
+
+export function createActiveRuntimeChecks(
+ runtimeChecks?: Partial
+): RuntimeChecks {
+ if (isDevMode()) {
+ if (runtimeChecks === undefined) {
+ console.warn(
+ '@ngrx/store: runtime checks are currently opt-in but will be the default in the next major version, see https://ngrx.io/guide/migration/v8 for more information.'
+ );
+ }
+ return {
+ strictStateSerializability: false,
+ strictActionSerializability: false,
+ strictImmutability: false,
+ ...runtimeChecks,
+ };
+ }
+
+ return {
+ strictStateSerializability: false,
+ strictActionSerializability: false,
+ strictImmutability: false,
+ };
+}
+
+export function createStateSerializationCheckMetaReducer({
+ strictStateSerializability,
+}: RuntimeChecks): MetaReducer {
+ return reducer =>
+ strictStateSerializability
+ ? stateSerializationCheckMetaReducer(reducer)
+ : reducer;
+}
+
+export function createActionSerializationCheckMetaReducer({
+ strictActionSerializability,
+}: RuntimeChecks): MetaReducer {
+ return reducer =>
+ strictActionSerializability
+ ? actionSerializationCheckMetaReducer(reducer)
+ : reducer;
+}
+
+export function createImmutabilityCheckMetaReducer({
+ strictImmutability,
+}: RuntimeChecks): MetaReducer {
+ return reducer =>
+ strictImmutability ? immutabilityCheckMetaReducer(reducer) : reducer;
+}
+
+export function provideRuntimeChecks(
+ runtimeChecks?: Partial
+): Provider[] {
+ return [
+ {
+ provide: _USER_RUNTIME_CHECKS,
+ useValue: runtimeChecks,
+ },
+ {
+ provide: _ACTIVE_RUNTIME_CHECKS,
+ deps: [_USER_RUNTIME_CHECKS],
+ useFactory: createActiveRuntimeChecks,
+ },
+ {
+ provide: META_REDUCERS,
+ multi: true,
+ deps: [_ACTIVE_RUNTIME_CHECKS],
+ useFactory: createStateSerializationCheckMetaReducer,
+ },
+ {
+ provide: META_REDUCERS,
+ multi: true,
+ deps: [_ACTIVE_RUNTIME_CHECKS],
+ useFactory: createActionSerializationCheckMetaReducer,
+ },
+ {
+ provide: META_REDUCERS,
+ multi: true,
+ deps: [_ACTIVE_RUNTIME_CHECKS],
+ useFactory: createImmutabilityCheckMetaReducer,
+ },
+ ];
+}
diff --git a/modules/store/src/state.ts b/modules/store/src/state.ts
index c0a7fc9b50..0eeba2dd25 100644
--- a/modules/store/src/state.ts
+++ b/modules/store/src/state.ts
@@ -47,9 +47,12 @@ export class State extends BehaviorSubject implements OnDestroy {
)
);
- this.stateSubscription = stateAndAction$.subscribe(({ state, action }) => {
- this.next(state);
- scannedActions.next(action);
+ this.stateSubscription = stateAndAction$.subscribe({
+ next: ({ state, action }) => {
+ this.next(state);
+ scannedActions.next(action);
+ },
+ error: err => this.error(err),
});
}
diff --git a/modules/store/src/store_module.ts b/modules/store/src/store_module.ts
index 407f78e409..121de3516a 100644
--- a/modules/store/src/store_module.ts
+++ b/modules/store/src/store_module.ts
@@ -14,6 +14,7 @@ import {
StoreFeature,
InitialState,
MetaReducer,
+ RuntimeChecks,
} from './models';
import { compose, combineReducers, createReducerFactory } from './utils';
import {
@@ -31,6 +32,8 @@ import {
_FEATURE_REDUCERS_TOKEN,
_STORE_FEATURES,
_FEATURE_CONFIGS,
+ USER_PROVIDED_META_REDUCERS,
+ _RESOLVED_META_REDUCERS,
} from './tokens';
import { ACTIONS_SUBJECT_PROVIDERS, ActionsSubject } from './actions_subject';
import {
@@ -44,6 +47,7 @@ import {
} from './scanned_actions_subject';
import { STATE_PROVIDERS } from './state';
import { STORE_PROVIDERS, Store } from './store';
+import { provideRuntimeChecks } from './runtime_checks';
@NgModule({})
export class StoreRootModule {
@@ -82,23 +86,28 @@ export class StoreFeatureModule implements OnDestroy {
}
}
-export type StoreConfig = {
+export interface StoreConfig {
initialState?: InitialState;
reducerFactory?: ActionReducerFactory;
metaReducers?: MetaReducer[];
-};
+}
+
+export interface RootStoreConfig
+ extends StoreConfig {
+ runtimeChecks?: Partial;
+}
@NgModule({})
export class StoreModule {
static forRoot(
reducers: ActionReducerMap | InjectionToken>,
- config?: StoreConfig
+ config?: RootStoreConfig
): ModuleWithProviders;
static forRoot(
reducers:
| ActionReducerMap
| InjectionToken>,
- config: StoreConfig = {}
+ config: RootStoreConfig = {}
): ModuleWithProviders {
return {
ngModule: StoreRootModule,
@@ -121,9 +130,14 @@ export class StoreModule {
useFactory: _createStoreReducers,
},
{
- provide: META_REDUCERS,
+ provide: USER_PROVIDED_META_REDUCERS,
useValue: config.metaReducers ? config.metaReducers : [],
},
+ {
+ provide: _RESOLVED_META_REDUCERS,
+ deps: [META_REDUCERS, USER_PROVIDED_META_REDUCERS],
+ useFactory: _concatMetaReducers,
+ },
{
provide: _REDUCER_FACTORY,
useValue: config.reducerFactory
@@ -132,7 +146,7 @@ export class StoreModule {
},
{
provide: REDUCER_FACTORY,
- deps: [_REDUCER_FACTORY, META_REDUCERS],
+ deps: [_REDUCER_FACTORY, _RESOLVED_META_REDUCERS],
useFactory: createReducerFactory,
},
ACTIONS_SUBJECT_PROVIDERS,
@@ -140,6 +154,7 @@ export class StoreModule {
SCANNED_ACTIONS_SUBJECT_PROVIDERS,
STATE_PROVIDERS,
STORE_PROVIDERS,
+ provideRuntimeChecks(config.runtimeChecks),
],
};
}
@@ -219,8 +234,7 @@ export class StoreModule {
export function _createStoreReducers(
injector: Injector,
- reducers: ActionReducerMap,
- tokenReducers: ActionReducerMap
+ reducers: ActionReducerMap
) {
return reducers instanceof InjectionToken ? injector.get(reducers) : reducers;
}
@@ -248,10 +262,9 @@ export function _createFeatureStore(
export function _createFeatureReducers(
injector: Injector,
- reducerCollection: ActionReducerMap[],
- tokenReducerCollection: ActionReducerMap[]
+ reducerCollection: ActionReducerMap[]
) {
- const reducers = reducerCollection.map((reducer, index) => {
+ const reducers = reducerCollection.map(reducer => {
return reducer instanceof InjectionToken ? injector.get(reducer) : reducer;
});
@@ -265,3 +278,10 @@ export function _initialStateFactory(initialState: any): any {
return initialState;
}
+
+export function _concatMetaReducers(
+ metaReducers: MetaReducer[],
+ userProvidedMetaReducers: MetaReducer[]
+): MetaReducer[] {
+ return metaReducers.concat(userProvidedMetaReducers);
+}
diff --git a/modules/store/src/tokens.ts b/modules/store/src/tokens.ts
index ae5f43fe6d..5e45f54597 100644
--- a/modules/store/src/tokens.ts
+++ b/modules/store/src/tokens.ts
@@ -1,4 +1,5 @@
import { InjectionToken } from '@angular/core';
+import { RuntimeChecks, MetaReducer } from './models';
export const _INITIAL_STATE = new InjectionToken(
'@ngrx/store Internal Initial State'
@@ -16,7 +17,6 @@ export const INITIAL_REDUCERS = new InjectionToken(
export const _INITIAL_REDUCERS = new InjectionToken(
'@ngrx/store Internal Initial Reducers'
);
-export const META_REDUCERS = new InjectionToken('@ngrx/store Meta Reducers');
export const STORE_FEATURES = new InjectionToken('@ngrx/store Store Features');
export const _STORE_REDUCERS = new InjectionToken(
'@ngrx/store Internal Store Reducers'
@@ -39,3 +39,39 @@ export const _FEATURE_REDUCERS_TOKEN = new InjectionToken(
export const FEATURE_REDUCERS = new InjectionToken(
'@ngrx/store Feature Reducers'
);
+
+/**
+ * User-defined meta reducers from StoreModule.forRoot()
+ */
+export const USER_PROVIDED_META_REDUCERS = new InjectionToken(
+ '@ngrx/store User Provided Meta Reducers'
+);
+
+/**
+ * Meta reducers defined either internally by @ngrx/store or by library authors
+ */
+export const META_REDUCERS = new InjectionToken(
+ '@ngrx/store Meta Reducers'
+);
+
+/**
+ * Concats the user provided meta reducers and the meta reducers provided on the multi
+ * injection token
+ */
+export const _RESOLVED_META_REDUCERS = new InjectionToken(
+ '@ngrx/store Internal Resolved Meta Reducers'
+);
+
+/**
+ * Runtime checks defined by the user
+ */
+export const _USER_RUNTIME_CHECKS = new InjectionToken(
+ '@ngrx/store Internal User Runtime Checks Config'
+);
+
+/**
+ * Runtime checks currently in use
+ */
+export const _ACTIVE_RUNTIME_CHECKS = new InjectionToken(
+ '@ngrx/store Internal Runetime Checks'
+);
diff --git a/modules/store/testing/BUILD b/modules/store/testing/BUILD
index 7c740f0458..41694f6668 100644
--- a/modules/store/testing/BUILD
+++ b/modules/store/testing/BUILD
@@ -14,7 +14,7 @@ ng_module(
visibility = ["//visibility:public"],
deps = [
"//modules/store",
- "@angular//packages/core",
- "@rxjs",
+ "@npm//@angular/core",
+ "@npm//rxjs",
],
)
diff --git a/package.json b/package.json
index 51fb693a2a..6a85e8fa42 100644
--- a/package.json
+++ b/package.json
@@ -1,14 +1,16 @@
{
"name": "@ngrx/platform",
- "version": "7.2.0",
+ "version": "7.4.0",
"description": "monorepo for ngrx development",
"scripts": {
"precommit": "lint-staged",
- "build": "yarn bazel build ...",
+ "build": "bazel build //modules/...",
"deploy:builds": "ts-node ./build/deploy-build.ts",
"deploy:preview": "ts-node ./build/deploy-preview.ts",
+ "cleanup:previews": "ts-node ./build/cleanup-previews.ts",
"test:unit": "node ./tests.js",
"test": "nyc yarn run test:unit",
+ "test:bazel": "bazel test //modules/...",
"clean": "git clean -xdf",
"cli": "ng",
"coverage:html": "nyc report --reporter=html",
@@ -16,22 +18,26 @@
"example:start": "yarn run cli serve",
"example:start:aot": "yarn run cli serve --prod",
"example:test": "jest -c projects/example-app/jest.config.js --watch",
- "example:build":"yarn cli build --prod",
- "example:build:prod": "yarn example:build -- --base-href \"/platform/example-app/\"",
+ "example:cypress:open": "cypress open --project=projects/example-app-cypress",
+ "example:cypress:run": "cypress run --project=projects/example-app-cypress",
+ "example:cypress:ci": "npm-run-all --parallel --race example:server \"example:cypress:run -- --config=baseUrl=http://localhost:4000\"",
+ "example:build": "yarn cli build --prod",
+ "example:build:prod": "yarn example:build --no-progress --base-href \"/platform/example-app/\"",
+ "example:server": "node build/example-app-server",
"ci": "yarn run test && nyc report --reporter=text-lcov | coveralls",
"prettier": "prettier --write \"**/*.ts\"",
"watch:tests": "chokidar \"modules/**/*.ts\" --initial --command \"yarn run test:unit\"",
"postinstall": "opencollective postinstall",
+ "prepublish": "ngc -p angular-metadata.tsconfig.json",
"changelog": "conventional-changelog -p angular -i CHANGELOG.md -s -r 0",
- "preskylint": "yarn bazel build --noshow_progress @io_bazel//src/tools/skylark/java/com/google/devtools/skylark/skylint:Skylint",
- "skylint": "find . -type f -name \"*.bzl\" ! -path \"*/node_modules/*\" ! -path \"./dist/*\" | xargs $(bazel info bazel-bin)/external/io_bazel/src/tools/skylark/java/com/google/devtools/skylark/skylint/Skylint --disable-checks=deprecated-api",
- "prebuildifier": "yarn bazel build --noshow_progress @com_github_bazelbuild_buildtools//buildifier",
- "buildifier": "find . -type f \\( -name BUILD -or -name BUILD.bazel \\) ! -path \"*/node_modules/*\" | xargs $(bazel info bazel-bin)/external/com_github_bazelbuild_buildtools/buildifier/*/buildifier",
+ "bazel:format": "find . -type f \\( -name \"*.bzl\" -or -name WORKSPACE -or -name BUILD -or -name BUILD.bazel \\) ! -path \"*/node_modules/*\" ! -path \"./dist/*\" | xargs buildifier -v --warnings=attr-cfg,attr-license,attr-non-empty,attr-output-default,attr-single-file,constant-glob,ctx-actions,ctx-args,depset-iteration,depset-union,dict-concatenation,duplicated-name,filetype,git-repository,http-archive,integer-division,load,load-on-top,native-build,native-package,out-of-order-load,output-group,package-name,package-on-top,positional-args,redefined-variable,repository-name,same-origin-load,string-iteration,unsorted-dict-items,unused-variable",
+ "bazel:lint": "yarn bazel:format --lint=warn",
+ "bazel:lint-fix": "yarn bazel:format --lint=fix",
"copy:schematics": "ts-node ./build/copy-schematics-core.ts",
"build:stackblitz": "ts-node ./build/stackblitz.ts && git add ./stackblitz.html"
},
"engines": {
- "node": ">=10.9.0 <11.2.0",
+ "node": ">=10.9.0 <=11.12.0",
"npm": ">=5.3.0",
"yarn": ">=1.9.2 <2.0.0"
},
@@ -39,6 +45,9 @@
"*.{ts,json,md}": [
"prettier --write",
"git add"
+ ],
+ "*.{bazel}": [
+ "buildifier"
]
},
"keywords": [
@@ -69,7 +78,7 @@
},
"dependencies": {
"@angular/animations": "^7.0.1",
- "@angular/bazel": "^7.0.1",
+ "@angular/bazel": "^8.0.0-beta.10",
"@angular/cdk": "^7.0.1",
"@angular/cli": "^7.0.1",
"@angular/common": "^7.0.1",
@@ -83,6 +92,8 @@
"@angular/platform-browser-dynamic": "^7.0.1",
"@angular/platform-server": "^7.0.1",
"@angular/router": "^7.0.1",
+ "@applitools/eyes-cypress": "^3.4.12",
+ "@bazel/buildifier": "^0.22.0",
"@ngrx/db": "^2.2.0-beta.0",
"core-js": "^2.5.4",
"hammerjs": "^2.0.8",
@@ -92,7 +103,9 @@
},
"devDependencies": {
"@angular-devkit/build-angular": "^0.10.0",
- "@bazel/bazel": "^0.19.1",
+ "@bazel/bazel": "^0.24.0",
+ "@bazel/typescript": "^0.27.8",
+ "@cypress/webpack-preprocessor": "^4.0.3",
"@octokit/rest": "^15.17.0",
"@types/fs-extra": "^2.1.0",
"@types/glob": "^5.0.33",
@@ -112,11 +125,12 @@
"conventional-changelog-cli": "^1.3.21",
"coveralls": "^2.13.0",
"cpy-cli": "^1.0.1",
+ "cypress": "^3.1.5",
"deep-freeze": "^0.0.1",
- "deep-freeze-strict": "^1.1.1",
+ "express": "^4.16.4",
"fs-extra": "^2.1.2",
"glob": "^7.1.2",
- "husky": "^0.14.3",
+ "husky": "^1.2.0",
"jasmine": "^2.5.3",
"jasmine-core": "~2.5.2",
"jasmine-marbles": "^0.4.0",
@@ -132,6 +146,7 @@
"karma-jasmine-html-reporter": "^0.2.2",
"lint-staged": "^8.0.0",
"ncp": "^2.0.0",
+ "npm-run-all": "^4.1.5",
"nyc": "^10.1.2",
"ora": "^1.3.0",
"prettier": "^1.11.1",
@@ -141,9 +156,11 @@
"rimraf": "^2.5.4",
"rollup": "^0.50.0",
"sorcery": "^0.10.0",
+ "ts-loader": "^5.3.3",
"ts-node": "^5.0.1",
+ "ts-snippet": "^4.1.0",
"tsconfig-paths": "^3.1.3",
- "tsickle": "^0.32.1",
+ "tsickle": "^0.34.3",
"tslib": "^1.7.1",
"tslint": "^5.7.0",
"tsutils": "2.20.0",
@@ -154,5 +171,11 @@
"type": "opencollective",
"url": "https://opencollective.com/ngrx",
"logo": "https://opencollective.com/opencollective/logo.txt"
+ },
+ "resolutions": {
+ "listr": "^0.14.2",
+ "listr-update-renderer": "^0.5.0",
+ "listr-verbose-renderer": "^0.5.0",
+ "lodash": "^4.17.11"
}
}
diff --git a/projects/example-app-cypress/cypress.json b/projects/example-app-cypress/cypress.json
new file mode 100644
index 0000000000..e100722097
--- /dev/null
+++ b/projects/example-app-cypress/cypress.json
@@ -0,0 +1,12 @@
+{
+ "baseUrl": "http://localhost:4200",
+ "fixturesFolder": "fixtures",
+ "integrationFolder": "integration",
+ "pluginsFile": "plugins/index.js",
+ "supportFile": "support/index.js",
+ "screenshotsFolder": "screenshots",
+ "videosFolder": "videos",
+ "video": false,
+ "projectId": "w65h7e",
+ "eyesTimeout": 180000
+}
diff --git a/projects/example-app-cypress/integration/round-trip.spec.ts b/projects/example-app-cypress/integration/round-trip.spec.ts
new file mode 100644
index 0000000000..bdccc874c8
--- /dev/null
+++ b/projects/example-app-cypress/integration/round-trip.spec.ts
@@ -0,0 +1,89 @@
+context('Full round trip', () => {
+ before(() => {
+ (cy as any).eyesOpen({
+ appName: 'books_app',
+ testName: 'round-trip',
+ browser: { width: 800, height: 600 },
+ });
+ window.indexedDB.deleteDatabase('books_app');
+ cy.visit('/');
+ });
+
+ after(() => {
+ (cy as any).eyesClose();
+ });
+
+ it('shows a message when the credentials are wrong', () => {
+ cy.get('[placeholder=Username]').type('wronguser');
+ cy.get('[placeholder=Password]').type('supersafepassword');
+ cy.get('[type="submit"]').click();
+
+ (cy as any).eyesCheckWindow(
+ 'show a message when the credentials are wrong'
+ );
+ cy.contains('Invalid username or password').should('be.visible');
+ });
+
+ it('is possible to login', () => {
+ cy.get('[placeholder=Username]')
+ .clear()
+ .type('test');
+ cy.get('[type="submit"]').click();
+ });
+
+ it('is possible to search for books', () => {
+ cy.contains('My Collection');
+ cy.contains('menu').click();
+ cy.contains('Browse Books').click();
+
+ (cy as any).eyesCheckWindow('is possible to search for books');
+ cy.get('[placeholder="Search for a book"]').type('The Alchemist');
+ cy.get('bc-book-preview')
+ .its('length')
+ .should('be.gte', 1);
+ });
+
+ it('is possible to add books', () => {
+ cy.get('bc-book-preview')
+ .eq(2)
+ .click();
+
+ cy.contains('Add Book to Collection').click();
+ (cy as any).eyesCheckWindow('is possible to add books');
+ cy.contains('Add Book to Collection').should('not.exist');
+ });
+
+ it('is possible to remove books', () => {
+ cy.go('back');
+
+ cy.get('bc-book-preview')
+ .eq(4)
+ .click();
+
+ cy.contains('Add Book to Collection').click();
+ cy.contains('Remove Book from Collection').click();
+
+ (cy as any).eyesCheckWindow('is possible to remove books');
+ cy.contains('Remove Book from Collection').should('not.exist');
+ });
+
+ it('is possible to show the collection', () => {
+ cy.contains('menu').click();
+ cy.contains('My Collection').click();
+
+ (cy as any).eyesCheckWindow('is possible to show the collection');
+ cy.get('bc-book-preview')
+ .its('length')
+ .should('be', 1);
+ });
+
+ it('is possible to sign out', () => {
+ cy.contains('menu').click();
+ cy.contains('Sign Out').click();
+ cy.contains('OK').click();
+
+ (cy as any).eyesCheckWindow('is possible to sign out');
+ cy.get('[placeholder=Username]').should('exist');
+ cy.get('[placeholder=Password]').should('exist');
+ });
+});
diff --git a/projects/example-app-cypress/plugins/cy-ts-preprocessor.js b/projects/example-app-cypress/plugins/cy-ts-preprocessor.js
new file mode 100644
index 0000000000..02ab5016c1
--- /dev/null
+++ b/projects/example-app-cypress/plugins/cy-ts-preprocessor.js
@@ -0,0 +1,26 @@
+const wp = require('@cypress/webpack-preprocessor');
+
+const webpackOptions = {
+ resolve: {
+ extensions: ['.ts', '.js'],
+ },
+ module: {
+ rules: [
+ {
+ test: /\.ts$/,
+ exclude: [/node_modules/],
+ use: [
+ {
+ loader: 'ts-loader',
+ },
+ ],
+ },
+ ],
+ },
+};
+
+const options = {
+ webpackOptions,
+};
+
+module.exports = wp(options);
diff --git a/projects/example-app-cypress/plugins/index.js b/projects/example-app-cypress/plugins/index.js
new file mode 100644
index 0000000000..20d85e40ad
--- /dev/null
+++ b/projects/example-app-cypress/plugins/index.js
@@ -0,0 +1,6 @@
+const cypressTypeScriptPreprocessor = require('./cy-ts-preprocessor');
+
+module.exports = on =>
+ on('file:preprocessor', cypressTypeScriptPreprocessor);
+
+require('@applitools/eyes-cypress')(module);
diff --git a/projects/example-app-cypress/support/index.js b/projects/example-app-cypress/support/index.js
new file mode 100644
index 0000000000..5b36f0c1a8
--- /dev/null
+++ b/projects/example-app-cypress/support/index.js
@@ -0,0 +1,3 @@
+import'@applitools/eyes-cypress/commands';
+
+
diff --git a/projects/example-app-cypress/tsconfig.json b/projects/example-app-cypress/tsconfig.json
new file mode 100644
index 0000000000..2a6ea9b8db
--- /dev/null
+++ b/projects/example-app-cypress/tsconfig.json
@@ -0,0 +1,13 @@
+{
+ "compilerOptions": {
+ "strict": true,
+ "baseUrl": "../../node_modules",
+ "target": "es5",
+ "experimentalDecorators": true,
+ "skipLibCheck": true,
+ "noImplicitAny": false,
+ "lib": ["es6", "dom"],
+ "types": ["cypress"]
+ },
+ "include": ["**/*.ts"]
+}
diff --git a/projects/example-app-e2e/protractor.conf.js b/projects/example-app-e2e/protractor.conf.js
deleted file mode 100644
index 23cd9398c4..0000000000
--- a/projects/example-app-e2e/protractor.conf.js
+++ /dev/null
@@ -1,31 +0,0 @@
-// Protractor configuration file, see link for more information
-// https://github.com/angular/protractor/blob/master/lib/config.ts
-
-/*global jasmine */
-const { SpecReporter } = require('jasmine-spec-reporter');
-
-exports.config = {
- allScriptsTimeout: 11000,
- specs: [
- './src/**/*.e2e-spec.ts'
- ],
- capabilities: {
- 'browserName': 'chrome'
- },
- directConnect: true,
- baseUrl: 'http://localhost:4200/',
- framework: 'jasmine',
- jasmineNodeOpts: {
- showColors: true,
- defaultTimeoutInterval: 30000,
- print: function() {}
- },
- beforeLaunch: function() {
- require('ts-node').register({
- project: require('path').join(__dirname, './tsconfig.e2e.json')
- });
- },
- onPrepare() {
- jasmine.getEnv().addReporter(new SpecReporter({ spec: { displayStacktrace: true } }));
- }
-};
diff --git a/projects/example-app-e2e/src/app.e2e-spec.ts b/projects/example-app-e2e/src/app.e2e-spec.ts
deleted file mode 100644
index 7dddcd57ab..0000000000
--- a/projects/example-app-e2e/src/app.e2e-spec.ts
+++ /dev/null
@@ -1,14 +0,0 @@
-import { ExampleAppPage } from './app.po';
-
-describe('example-app App', function() {
- let page: ExampleAppPage;
-
- beforeEach(() => {
- page = new ExampleAppPage();
- });
-
- it('should display the app title in the menu', () => {
- page.navigateTo();
- expect(page.getAppDescription()).toContain('Book Collection');
- });
-});
diff --git a/projects/example-app-e2e/src/app.po.ts b/projects/example-app-e2e/src/app.po.ts
deleted file mode 100644
index e9462e994b..0000000000
--- a/projects/example-app-e2e/src/app.po.ts
+++ /dev/null
@@ -1,11 +0,0 @@
-import { browser, element, by } from 'protractor';
-
-export class ExampleAppPage {
- navigateTo() {
- return browser.get('/');
- }
-
- getAppDescription() {
- return element(by.css('mat-toolbar')).getText();
- }
-}
diff --git a/projects/example-app-e2e/tsconfig.e2e.json b/projects/example-app-e2e/tsconfig.e2e.json
deleted file mode 100644
index c331400bf8..0000000000
--- a/projects/example-app-e2e/tsconfig.e2e.json
+++ /dev/null
@@ -1,14 +0,0 @@
-{
- "compilerOptions": {
- "sourceMap": true,
- "declaration": false,
- "moduleResolution": "node",
- "emitDecoratorMetadata": true,
- "experimentalDecorators": true,
- "lib": ["es2016"],
- "outDir": "../dist/out-tsc-e2e",
- "module": "commonjs",
- "target": "es6",
- "types": ["jasmine", "node"]
- }
-}
diff --git a/projects/example-app/jest.config.js b/projects/example-app/jest.config.js
index bffd342d18..efa8588d41 100644
--- a/projects/example-app/jest.config.js
+++ b/projects/example-app/jest.config.js
@@ -1,38 +1,24 @@
module.exports = {
- "rootDir": ".",
- "setupTestFrameworkScriptFile": "/src/setup-jest.ts",
- "globals": {
- "ts-jest": {
- "tsConfigFile": "projects/example-app/tsconfig.spec.json"
+ rootDir: '.',
+ setupTestFrameworkScriptFile: '/src/setup-jest.ts',
+ globals: {
+ 'ts-jest': {
+ tsConfigFile: 'projects/example-app/tsconfig.spec.json',
},
- "__TRANSFORM_HTML__": true
+ __TRANSFORM_HTML__: true,
},
- "transform": {
- "^.+\\.(ts|js|html)$": "/../../node_modules/jest-preset-angular/preprocessor.js"
+ transform: {
+ '^.+\\.(ts|js|html)$':
+ '/../../node_modules/jest-preset-angular/preprocessor.js',
},
- "testMatch": [
- "/**/*.spec.ts"
- ],
- "moduleFileExtensions": [
- "ts",
- "js",
- "html",
- "json"
- ],
- "mapCoverage": true,
- "coveragePathIgnorePatterns": [
- "/node_modules/",
- "/modules/*.*/"
- ],
- "moduleNameMapper": {
- "^@ngrx/(?!db)(.*)": "/../../modules/$1",
- "^@example-app/(.*)": "/src/app/$1",
- "ngrx-store-freeze": "/../../projects/ngrx-store-freeze/"
+ testMatch: ['/**/*.spec.ts'],
+ moduleFileExtensions: ['ts', 'js', 'html', 'json'],
+ mapCoverage: true,
+ coveragePathIgnorePatterns: ['/node_modules/', '/modules/*.*/'],
+ moduleNameMapper: {
+ '^@ngrx/(?!db)(.*)': '/../../modules/$1',
+ '^@example-app/(.*)': '/src/app/$1',
},
- "transformIgnorePatterns": [
- "node_modules/(?!@ngrx)"
- ],
- "modulePathIgnorePatterns": [
- "dist"
- ]
+ transformIgnorePatterns: ['node_modules/(?!@ngrx)'],
+ modulePathIgnorePatterns: ['dist'],
};
diff --git a/projects/example-app/src/app/app.module.ts b/projects/example-app/src/app/app.module.ts
index a2f60a4bca..bcfd886ce6 100644
--- a/projects/example-app/src/app/app.module.ts
+++ b/projects/example-app/src/app/app.module.ts
@@ -64,7 +64,7 @@ import { AppRoutingModule } from '@example-app/app-routing.module';
* sets up the effects class to be initialized immediately when the
* application starts.
*
- * See: https://github.com/ngrx/platform/blob/master/docs/effects/api.md#forroot
+ * See: https://ngrx.io/guide/effects#registering-root-effects
*/
EffectsModule.forRoot([]),
diff --git a/projects/example-app/src/app/auth/actions/auth-api.actions.ts b/projects/example-app/src/app/auth/actions/auth-api.actions.ts
index 48c19b3696..b21260157c 100644
--- a/projects/example-app/src/app/auth/actions/auth-api.actions.ts
+++ b/projects/example-app/src/app/auth/actions/auth-api.actions.ts
@@ -1,26 +1,20 @@
-import { Action } from '@ngrx/store';
+import { props, createAction } from '@ngrx/store';
import { User } from '@example-app/auth/models/user';
-export enum AuthApiActionTypes {
- LoginSuccess = '[Auth/API] Login Success',
- LoginFailure = '[Auth/API] Login Failure',
- LoginRedirect = '[Auth/API] Login Redirect',
-}
+export const loginSuccess = createAction(
+ '[Auth/API] Login Success',
+ props<{ user: User }>()
+);
-export class LoginSuccess implements Action {
- readonly type = AuthApiActionTypes.LoginSuccess;
+export const loginFailure = createAction(
+ '[Auth/API] Login Failure',
+ props<{ error: any }>()
+);
- constructor(public payload: { user: User }) {}
-}
+export const loginRedirect = createAction('[Auth/API] Login Redirect');
-export class LoginFailure implements Action {
- readonly type = AuthApiActionTypes.LoginFailure;
-
- constructor(public payload: { error: any }) {}
-}
-
-export class LoginRedirect implements Action {
- readonly type = AuthApiActionTypes.LoginRedirect;
-}
-
-export type AuthApiActionsUnion = LoginSuccess | LoginFailure | LoginRedirect;
+// This is an alternative to union() type export. Work great when you need
+// to export only a single Action type.
+export type AuthApiActionsUnion = ReturnType<
+ typeof loginSuccess | typeof loginFailure | typeof loginRedirect
+>;
diff --git a/projects/example-app/src/app/auth/actions/auth.actions.ts b/projects/example-app/src/app/auth/actions/auth.actions.ts
index 5151d08f34..cd75a1ea76 100644
--- a/projects/example-app/src/app/auth/actions/auth.actions.ts
+++ b/projects/example-app/src/app/auth/actions/auth.actions.ts
@@ -1,24 +1,10 @@
-import { Action } from '@ngrx/store';
+import { createAction, union } from '@ngrx/store';
-export enum AuthActionTypes {
- Logout = '[Auth] Logout',
- LogoutConfirmation = '[Auth] Logout Confirmation',
- LogoutConfirmationDismiss = '[Auth] Logout Confirmation Dismiss',
-}
+export const logout = createAction('[Auth] Logout');
+export const logoutConfirmation = createAction('[Auth] Logout Confirmation');
+export const logoutConfirmationDismiss = createAction(
+ '[Auth] Logout Confirmation Dismiss'
+);
-export class Logout implements Action {
- readonly type = AuthActionTypes.Logout;
-}
-
-export class LogoutConfirmation implements Action {
- readonly type = AuthActionTypes.LogoutConfirmation;
-}
-
-export class LogoutConfirmationDismiss implements Action {
- readonly type = AuthActionTypes.LogoutConfirmationDismiss;
-}
-
-export type AuthActionsUnion =
- | Logout
- | LogoutConfirmation
- | LogoutConfirmationDismiss;
+const all = union({ logout, logoutConfirmation, logoutConfirmationDismiss });
+export type AuthActionsUnion = typeof all;
diff --git a/projects/example-app/src/app/auth/actions/login-page.actions.ts b/projects/example-app/src/app/auth/actions/login-page.actions.ts
index 2a70296d7a..f881552f50 100644
--- a/projects/example-app/src/app/auth/actions/login-page.actions.ts
+++ b/projects/example-app/src/app/auth/actions/login-page.actions.ts
@@ -1,14 +1,9 @@
-import { Action } from '@ngrx/store';
+import { createAction, props, union } from '@ngrx/store';
import { Credentials } from '@example-app/auth/models/user';
-export enum LoginPageActionTypes {
- Login = '[Login Page] Login',
-}
+export const login = createAction(
+ '[Login Page] Login',
+ props<{ credentials: Credentials }>()
+);
-export class Login implements Action {
- readonly type = LoginPageActionTypes.Login;
-
- constructor(public payload: { credentials: Credentials }) {}
-}
-
-export type LoginPageActionsUnion = Login;
+export type LoginPageActionsUnion = ReturnType;
diff --git a/projects/example-app/src/app/auth/containers/login-page.component.spec.ts b/projects/example-app/src/app/auth/containers/login-page.component.spec.ts
index 9da3433266..5ef167af53 100644
--- a/projects/example-app/src/app/auth/containers/login-page.component.spec.ts
+++ b/projects/example-app/src/app/auth/containers/login-page.component.spec.ts
@@ -57,7 +57,7 @@ describe('Login Page', () => {
it('should dispatch a login event on submit', () => {
const credentials: any = {};
- const action = new LoginPageActions.Login({ credentials });
+ const action = LoginPageActions.login({ credentials });
instance.onSubmit(credentials);
diff --git a/projects/example-app/src/app/auth/containers/login-page.component.ts b/projects/example-app/src/app/auth/containers/login-page.component.ts
index f56a2577f6..4eded3bf77 100644
--- a/projects/example-app/src/app/auth/containers/login-page.component.ts
+++ b/projects/example-app/src/app/auth/containers/login-page.component.ts
@@ -24,6 +24,6 @@ export class LoginPageComponent implements OnInit {
ngOnInit() {}
onSubmit(credentials: Credentials) {
- this.store.dispatch(new LoginPageActions.Login({ credentials }));
+ this.store.dispatch(LoginPageActions.login({ credentials }));
}
}
diff --git a/projects/example-app/src/app/auth/effects/auth.effects.spec.ts b/projects/example-app/src/app/auth/effects/auth.effects.spec.ts
index 55dd1bb1f4..79818f5675 100644
--- a/projects/example-app/src/app/auth/effects/auth.effects.spec.ts
+++ b/projects/example-app/src/app/auth/effects/auth.effects.spec.ts
@@ -54,11 +54,11 @@ describe('AuthEffects', () => {
});
describe('login$', () => {
- it('should return an auth.LoginSuccess action, with user information if login succeeds', () => {
+ it('should return an auth.loginSuccess action, with user information if login succeeds', () => {
const credentials: Credentials = { username: 'test', password: '' };
const user = { name: 'User' } as User;
- const action = new LoginPageActions.Login({ credentials });
- const completion = new AuthApiActions.LoginSuccess({ user });
+ const action = LoginPageActions.login({ credentials });
+ const completion = AuthApiActions.loginSuccess({ user });
actions$ = hot('-a---', { a: action });
const response = cold('-a|', { a: user });
@@ -68,10 +68,10 @@ describe('AuthEffects', () => {
expect(effects.login$).toBeObservable(expected);
});
- it('should return a new auth.LoginFailure if the login service throws', () => {
+ it('should return a new auth.loginFailure if the login service throws', () => {
const credentials: Credentials = { username: 'someOne', password: '' };
- const action = new LoginPageActions.Login({ credentials });
- const completion = new AuthApiActions.LoginFailure({
+ const action = LoginPageActions.login({ credentials });
+ const completion = AuthApiActions.loginFailure({
error: 'Invalid username or password',
});
const error = 'Invalid username or password';
@@ -88,7 +88,7 @@ describe('AuthEffects', () => {
describe('loginSuccess$', () => {
it('should dispatch a RouterNavigation action', (done: any) => {
const user = { name: 'User' } as User;
- const action = new AuthApiActions.LoginSuccess({ user });
+ const action = AuthApiActions.loginSuccess({ user });
actions$ = of(action);
@@ -100,8 +100,8 @@ describe('AuthEffects', () => {
});
describe('loginRedirect$', () => {
- it('should dispatch a RouterNavigation action when auth.LoginRedirect is dispatched', (done: any) => {
- const action = new AuthApiActions.LoginRedirect();
+ it('should dispatch a RouterNavigation action when auth.loginRedirect is dispatched', (done: any) => {
+ const action = AuthApiActions.loginRedirect();
actions$ = of(action);
@@ -111,8 +111,8 @@ describe('AuthEffects', () => {
});
});
- it('should dispatch a RouterNavigation action when auth.Logout is dispatched', (done: any) => {
- const action = new AuthActions.Logout();
+ it('should dispatch a RouterNavigation action when auth.logout is dispatched', (done: any) => {
+ const action = AuthActions.logout();
actions$ = of(action);
@@ -125,8 +125,8 @@ describe('AuthEffects', () => {
describe('logoutConfirmation$', () => {
it('should dispatch a Logout action if dialog closes with true result', () => {
- const action = new AuthActions.LogoutConfirmation();
- const completion = new AuthActions.Logout();
+ const action = AuthActions.logoutConfirmation();
+ const completion = AuthActions.logout();
actions$ = hot('-a', { a: action });
const expected = cold('-b', { b: completion });
@@ -139,8 +139,8 @@ describe('AuthEffects', () => {
});
it('should dispatch a LogoutConfirmationDismiss action if dialog closes with falsy result', () => {
- const action = new AuthActions.LogoutConfirmation();
- const completion = new AuthActions.LogoutConfirmationDismiss();
+ const action = AuthActions.logoutConfirmation();
+ const completion = AuthActions.logoutConfirmationDismiss();
actions$ = hot('-a', { a: action });
const expected = cold('-b', { b: completion });
diff --git a/projects/example-app/src/app/auth/effects/auth.effects.ts b/projects/example-app/src/app/auth/effects/auth.effects.ts
index 7db06fe730..1cdbad8e19 100644
--- a/projects/example-app/src/app/auth/effects/auth.effects.ts
+++ b/projects/example-app/src/app/auth/effects/auth.effects.ts
@@ -17,28 +17,25 @@ import { LogoutConfirmationDialogComponent } from '@example-app/auth/components/
export class AuthEffects {
@Effect()
login$ = this.actions$.pipe(
- ofType(LoginPageActions.LoginPageActionTypes.Login),
- map(action => action.payload.credentials),
+ ofType(LoginPageActions.login.type),
+ map(action => action.credentials),
exhaustMap((auth: Credentials) =>
this.authService.login(auth).pipe(
- map(user => new AuthApiActions.LoginSuccess({ user })),
- catchError(error => of(new AuthApiActions.LoginFailure({ error })))
+ map(user => AuthApiActions.loginSuccess({ user })),
+ catchError(error => of(AuthApiActions.loginFailure({ error })))
)
)
);
@Effect({ dispatch: false })
loginSuccess$ = this.actions$.pipe(
- ofType(AuthApiActions.AuthApiActionTypes.LoginSuccess),
+ ofType(AuthApiActions.loginSuccess.type),
tap(() => this.router.navigate(['/']))
);
@Effect({ dispatch: false })
loginRedirect$ = this.actions$.pipe(
- ofType(
- AuthApiActions.AuthApiActionTypes.LoginRedirect,
- AuthActions.AuthActionTypes.Logout
- ),
+ ofType(AuthApiActions.loginRedirect.type, AuthActions.logout.type),
tap(authed => {
this.router.navigate(['/login']);
})
@@ -46,7 +43,7 @@ export class AuthEffects {
@Effect()
logoutConfirmation$ = this.actions$.pipe(
- ofType(AuthActions.AuthActionTypes.LogoutConfirmation),
+ ofType(AuthActions.logoutConfirmation.type),
exhaustMap(() => {
const dialogRef = this.dialog.open<
LogoutConfirmationDialogComponent,
@@ -58,9 +55,7 @@ export class AuthEffects {
}),
map(
result =>
- result
- ? new AuthActions.Logout()
- : new AuthActions.LogoutConfirmationDismiss()
+ result ? AuthActions.logout() : AuthActions.logoutConfirmationDismiss()
)
);
diff --git a/projects/example-app/src/app/auth/reducers/auth.reducer.spec.ts b/projects/example-app/src/app/auth/reducers/auth.reducer.spec.ts
index 6b9b9846c7..1af5f3d1ad 100644
--- a/projects/example-app/src/app/auth/reducers/auth.reducer.spec.ts
+++ b/projects/example-app/src/app/auth/reducers/auth.reducer.spec.ts
@@ -25,7 +25,7 @@ describe('AuthReducer', () => {
describe('LOGIN_SUCCESS', () => {
it('should add a user set loggedIn to true in auth state', () => {
const user = { name: 'test' } as User;
- const createAction = new AuthApiActions.LoginSuccess({ user });
+ const createAction = AuthApiActions.loginSuccess({ user });
const expectedResult = {
user: { name: 'test' },
@@ -42,7 +42,7 @@ describe('AuthReducer', () => {
const initialState = {
user: { name: 'test' },
} as fromAuth.State;
- const createAction = new AuthActions.Logout();
+ const createAction = AuthActions.logout();
const expectedResult = fromAuth.initialState;
diff --git a/projects/example-app/src/app/auth/reducers/auth.reducer.ts b/projects/example-app/src/app/auth/reducers/auth.reducer.ts
index 797ab9f0af..461c730f55 100644
--- a/projects/example-app/src/app/auth/reducers/auth.reducer.ts
+++ b/projects/example-app/src/app/auth/reducers/auth.reducer.ts
@@ -14,14 +14,14 @@ export function reducer(
action: AuthApiActions.AuthApiActionsUnion | AuthActions.AuthActionsUnion
): State {
switch (action.type) {
- case AuthApiActions.AuthApiActionTypes.LoginSuccess: {
+ case AuthApiActions.loginSuccess.type: {
return {
...state,
- user: action.payload.user,
+ user: action.user,
};
}
- case AuthActions.AuthActionTypes.Logout: {
+ case AuthActions.logout.type: {
return initialState;
}
diff --git a/projects/example-app/src/app/auth/reducers/login-page.reducer.spec.ts b/projects/example-app/src/app/auth/reducers/login-page.reducer.spec.ts
index c81149bbcc..628edb73d0 100644
--- a/projects/example-app/src/app/auth/reducers/login-page.reducer.spec.ts
+++ b/projects/example-app/src/app/auth/reducers/login-page.reducer.spec.ts
@@ -19,7 +19,7 @@ describe('LoginPageReducer', () => {
describe('LOGIN', () => {
it('should make pending to true', () => {
const user = { username: 'test' } as Credentials;
- const createAction = new LoginPageActions.Login({ credentials: user });
+ const createAction = LoginPageActions.login({ credentials: user });
const expectedResult = {
error: null,
@@ -35,7 +35,7 @@ describe('LoginPageReducer', () => {
describe('LOGIN_SUCCESS', () => {
it('should have no error and no pending state', () => {
const user = { name: 'test' } as User;
- const createAction = new AuthApiActions.LoginSuccess({ user });
+ const createAction = AuthApiActions.loginSuccess({ user });
const expectedResult = {
error: null,
@@ -51,7 +51,7 @@ describe('LoginPageReducer', () => {
describe('LOGIN_FAILURE', () => {
it('should have an error and no pending state', () => {
const error = 'login failed';
- const createAction = new AuthApiActions.LoginFailure({ error });
+ const createAction = AuthApiActions.loginFailure({ error });
const expectedResult = {
error: error,
diff --git a/projects/example-app/src/app/auth/reducers/login-page.reducer.ts b/projects/example-app/src/app/auth/reducers/login-page.reducer.ts
index 8e84ab37f7..452d4b5167 100644
--- a/projects/example-app/src/app/auth/reducers/login-page.reducer.ts
+++ b/projects/example-app/src/app/auth/reducers/login-page.reducer.ts
@@ -17,7 +17,7 @@ export function reducer(
| LoginPageActions.LoginPageActionsUnion
): State {
switch (action.type) {
- case LoginPageActions.LoginPageActionTypes.Login: {
+ case LoginPageActions.login.type: {
return {
...state,
error: null,
@@ -25,7 +25,7 @@ export function reducer(
};
}
- case AuthApiActions.AuthApiActionTypes.LoginSuccess: {
+ case AuthApiActions.loginSuccess.type: {
return {
...state,
error: null,
@@ -33,10 +33,10 @@ export function reducer(
};
}
- case AuthApiActions.AuthApiActionTypes.LoginFailure: {
+ case AuthApiActions.loginFailure.type: {
return {
...state,
- error: action.payload.error,
+ error: action.error,
pending: false,
};
}
diff --git a/projects/example-app/src/app/auth/services/auth-guard.service.spec.ts b/projects/example-app/src/app/auth/services/auth-guard.service.spec.ts
index 6b505bf979..4dd60b26cb 100644
--- a/projects/example-app/src/app/auth/services/auth-guard.service.spec.ts
+++ b/projects/example-app/src/app/auth/services/auth-guard.service.spec.ts
@@ -1,27 +1,29 @@
-import { TestBed, inject } from '@angular/core/testing';
-import { StoreModule, Store, combineReducers } from '@ngrx/store';
+import { TestBed } from '@angular/core/testing';
+import { Store } from '@ngrx/store';
import { cold } from 'jasmine-marbles';
import { AuthGuard } from '@example-app/auth/services/auth-guard.service';
-import { AuthApiActions } from '@example-app/auth/actions';
import * as fromRoot from '@example-app/reducers';
import * as fromAuth from '@example-app/auth/reducers';
+import * as fromLoginPage from '@example-app/auth/reducers/login-page.reducer';
+import { provideMockStore, MockStore } from '@ngrx/store/testing';
describe('Auth Guard', () => {
let guard: AuthGuard;
- let store: Store;
+ let store: MockStore;
+ const initialState = {
+ auth: {
+ status: {
+ user: null,
+ },
+ },
+ } as fromAuth.State;
beforeEach(() => {
TestBed.configureTestingModule({
- imports: [
- StoreModule.forRoot({
- ...fromRoot.reducers,
- auth: combineReducers(fromAuth.reducers),
- }),
- ],
+ providers: [AuthGuard, provideMockStore({ initialState })],
});
store = TestBed.get(Store);
- spyOn(store, 'dispatch').and.callThrough();
guard = TestBed.get(AuthGuard);
});
@@ -32,9 +34,17 @@ describe('Auth Guard', () => {
});
it('should return true if the user state is logged in', () => {
- const user: any = {};
- const action = new AuthApiActions.LoginSuccess({ user });
- store.dispatch(action);
+ store.setState({
+ ...initialState,
+ auth: {
+ loginPage: {} as fromLoginPage.State,
+ status: {
+ user: {
+ name: 'John',
+ },
+ },
+ },
+ });
const expected = cold('(a|)', { a: true });
diff --git a/projects/example-app/src/app/auth/services/auth-guard.service.ts b/projects/example-app/src/app/auth/services/auth-guard.service.ts
index c0402b1670..0537d667bd 100644
--- a/projects/example-app/src/app/auth/services/auth-guard.service.ts
+++ b/projects/example-app/src/app/auth/services/auth-guard.service.ts
@@ -17,7 +17,7 @@ export class AuthGuard implements CanActivate {
select(fromAuth.getLoggedIn),
map(authed => {
if (!authed) {
- this.store.dispatch(new AuthApiActions.LoginRedirect());
+ this.store.dispatch(AuthApiActions.loginRedirect());
return false;
}
diff --git a/projects/example-app/src/app/books/actions/book.actions.ts b/projects/example-app/src/app/books/actions/book.actions.ts
index 9926fc62cf..24c04fc9a3 100644
--- a/projects/example-app/src/app/books/actions/book.actions.ts
+++ b/projects/example-app/src/app/books/actions/book.actions.ts
@@ -1,14 +1,9 @@
-import { Action } from '@ngrx/store';
+import { createAction, props } from '@ngrx/store';
import { Book } from '@example-app/books/models/book';
-export enum BookActionTypes {
- LoadBook = '[Book Exists Guard] Load Book',
-}
+export const loadBook = createAction(
+ '[Book Exists Guard] Load Book',
+ props<{ book: Book }>()
+);
-export class LoadBook implements Action {
- readonly type = BookActionTypes.LoadBook;
-
- constructor(public payload: Book) {}
-}
-
-export type BookActionsUnion = LoadBook;
+export type BookActionsUnion = ReturnType;
diff --git a/projects/example-app/src/app/books/actions/books-api.actions.ts b/projects/example-app/src/app/books/actions/books-api.actions.ts
index e3a438ad33..4f141a4ba7 100644
--- a/projects/example-app/src/app/books/actions/books-api.actions.ts
+++ b/projects/example-app/src/app/books/actions/books-api.actions.ts
@@ -1,32 +1,19 @@
-import { Action } from '@ngrx/store';
+import { createAction, union, props } from '@ngrx/store';
import { Book } from '@example-app/books/models/book';
-export enum BooksApiActionTypes {
- SearchSuccess = '[Books/API] Search Success',
- SearchFailure = '[Books/API] Search Failure',
-}
+export const searchSuccess = createAction(
+ '[Books/API] Search Success',
+ props<{ books: Book[] }>()
+);
-/**
- * Every action is comprised of at least a type and an optional
- * payload. Expressing actions as classes enables powerful
- * type checking in reducer functions.
- *
- * See Discriminated Unions: https://www.typescriptlang.org/docs/handbook/advanced-types.html#discriminated-unions
- */
-export class SearchSuccess implements Action {
- readonly type = BooksApiActionTypes.SearchSuccess;
-
- constructor(public payload: Book[]) {}
-}
-
-export class SearchFailure implements Action {
- readonly type = BooksApiActionTypes.SearchFailure;
-
- constructor(public payload: string) {}
-}
+export const searchFailure = createAction(
+ '[Books/API] Search Failure',
+ props<{ errorMsg: string }>()
+);
/**
* Export a type alias of all actions in this action group
* so that reducers can easily compose action types
*/
-export type BooksApiActionsUnion = SearchSuccess | SearchFailure;
+const all = union({ searchSuccess, searchFailure });
+export type BooksApiActionsUnion = typeof all;
diff --git a/projects/example-app/src/app/books/actions/collection-api.actions.ts b/projects/example-app/src/app/books/actions/collection-api.actions.ts
index 39412f5a1c..f5165abf6a 100644
--- a/projects/example-app/src/app/books/actions/collection-api.actions.ts
+++ b/projects/example-app/src/app/books/actions/collection-api.actions.ts
@@ -1,64 +1,51 @@
-import { Action } from '@ngrx/store';
+import { createAction, props, union } from '@ngrx/store';
import { Book } from '@example-app/books/models/book';
-export enum CollectionApiActionTypes {
- AddBookSuccess = '[Collection/API] Add Book Success',
- AddBookFailure = '[Collection/API] Add Book Failure',
- RemoveBookSuccess = '[Collection/API] Remove Book Success',
- RemoveBookFailure = '[Collection/API] Remove Book Failure',
- LoadBooksSuccess = '[Collection/API] Load Books Success',
- LoadBooksFailure = '[Collection/API] Load Books Failure',
-}
-
/**
* Add Book to Collection Actions
*/
-export class AddBookSuccess implements Action {
- readonly type = CollectionApiActionTypes.AddBookSuccess;
-
- constructor(public payload: Book) {}
-}
-
-export class AddBookFailure implements Action {
- readonly type = CollectionApiActionTypes.AddBookFailure;
+export const addBookSuccess = createAction(
+ '[Collection/API] Add Book Success',
+ props<{ book: Book }>()
+);
- constructor(public payload: Book) {}
-}
+export const addBookFailure = createAction(
+ '[Collection/API] Add Book Failure',
+ props<{ book: Book }>()
+);
/**
* Remove Book from Collection Actions
*/
-export class RemoveBookSuccess implements Action {
- readonly type = CollectionApiActionTypes.RemoveBookSuccess;
-
- constructor(public payload: Book) {}
-}
-
-export class RemoveBookFailure implements Action {
- readonly type = CollectionApiActionTypes.RemoveBookFailure;
+export const removeBookSuccess = createAction(
+ '[Collection/API] Remove Book Success',
+ props<{ book: Book }>()
+);
- constructor(public payload: Book) {}
-}
+export const removeBookFailure = createAction(
+ '[Collection/API] Remove Book Failure',
+ props<{ book: Book }>()
+);
/**
* Load Collection Actions
*/
-export class LoadBooksSuccess implements Action {
- readonly type = CollectionApiActionTypes.LoadBooksSuccess;
-
- constructor(public payload: Book[]) {}
-}
-
-export class LoadBooksFailure implements Action {
- readonly type = CollectionApiActionTypes.LoadBooksFailure;
-
- constructor(public payload: any) {}
-}
-
-export type CollectionApiActionsUnion =
- | AddBookSuccess
- | AddBookFailure
- | RemoveBookSuccess
- | RemoveBookFailure
- | LoadBooksSuccess
- | LoadBooksFailure;
+export const loadBooksSuccess = createAction(
+ '[Collection/API] Load Books Success',
+ props<{ books: Book[] }>()
+);
+
+export const loadBooksFailure = createAction(
+ '[Collection/API] Load Books Failure',
+ props<{ error: any }>()
+);
+
+const all = union({
+ addBookSuccess,
+ addBookFailure,
+ removeBookSuccess,
+ removeBookFailure,
+ loadBooksSuccess,
+ loadBooksFailure,
+});
+export type CollectionApiActionsUnion = typeof all;
diff --git a/projects/example-app/src/app/books/actions/collection-page.actions.ts b/projects/example-app/src/app/books/actions/collection-page.actions.ts
index 9a789799ea..7bfd6174ca 100644
--- a/projects/example-app/src/app/books/actions/collection-page.actions.ts
+++ b/projects/example-app/src/app/books/actions/collection-page.actions.ts
@@ -1,14 +1,8 @@
-import { Action } from '@ngrx/store';
-
-export enum CollectionPageActionTypes {
- LoadCollection = '[Collection Page] Load Collection',
-}
+import { createAction } from '@ngrx/store';
/**
* Load Collection Action
*/
-export class LoadCollection implements Action {
- readonly type = CollectionPageActionTypes.LoadCollection;
-}
+export const loadCollection = createAction('[Collection Page] Load Collection');
-export type CollectionPageActionsUnion = LoadCollection;
+export type CollectionPageActionsUnion = ReturnType;
diff --git a/projects/example-app/src/app/books/actions/find-book-page.actions.ts b/projects/example-app/src/app/books/actions/find-book-page.actions.ts
index a6387cdf7c..f1ab45d3ff 100644
--- a/projects/example-app/src/app/books/actions/find-book-page.actions.ts
+++ b/projects/example-app/src/app/books/actions/find-book-page.actions.ts
@@ -1,13 +1,8 @@
-import { Action } from '@ngrx/store';
+import { createAction, props } from '@ngrx/store';
-export enum FindBookPageActionTypes {
- SearchBooks = '[Find Book Page] Search Books',
-}
+export const searchBooks = createAction(
+ '[Find Book Page] Search Books',
+ props<{ query: string }>()
+);
-export class SearchBooks implements Action {
- readonly type = FindBookPageActionTypes.SearchBooks;
-
- constructor(public payload: string) {}
-}
-
-export type FindBookPageActionsUnion = SearchBooks;
+export type FindBookPageActionsUnion = ReturnType;
diff --git a/projects/example-app/src/app/books/actions/selected-book-page.actions.ts b/projects/example-app/src/app/books/actions/selected-book-page.actions.ts
index 1698a909df..353c38431b 100644
--- a/projects/example-app/src/app/books/actions/selected-book-page.actions.ts
+++ b/projects/example-app/src/app/books/actions/selected-book-page.actions.ts
@@ -1,27 +1,22 @@
-import { Action } from '@ngrx/store';
+import { createAction, union, props } from '@ngrx/store';
import { Book } from '@example-app/books/models/book';
-export enum SelectedBookPageActionTypes {
- AddBook = '[Selected Book Page] Add Book',
- RemoveBook = '[Selected Book Page] Remove Book',
-}
-
/**
* Add Book to Collection Action
*/
-export class AddBook implements Action {
- readonly type = SelectedBookPageActionTypes.AddBook;
-
- constructor(public payload: Book) {}
-}
+export const addBook = createAction(
+ '[Selected Book Page] Add Book',
+ props<{ book: Book }>()
+);
/**
* Remove Book from Collection Action
*/
-export class RemoveBook implements Action {
- readonly type = SelectedBookPageActionTypes.RemoveBook;
+export const removeBook = createAction(
+ '[Selected Book Page] Remove Book',
+ props<{ book: Book }>()
+);
- constructor(public payload: Book) {}
-}
+const all = union({ addBook, removeBook });
-export type SelectedBookPageActionsUnion = AddBook | RemoveBook;
+export type SelectedBookPageActionsUnion = typeof all;
diff --git a/projects/example-app/src/app/books/actions/view-book-page.actions.ts b/projects/example-app/src/app/books/actions/view-book-page.actions.ts
index 1d15fa2074..38a43b6626 100644
--- a/projects/example-app/src/app/books/actions/view-book-page.actions.ts
+++ b/projects/example-app/src/app/books/actions/view-book-page.actions.ts
@@ -1,13 +1,8 @@
-import { Action } from '@ngrx/store';
+import { createAction, props } from '@ngrx/store';
-export enum ViewBookPageActionTypes {
- SelectBook = '[View Book Page] Select Book',
-}
+export const selectBook = createAction(
+ '[View Book Page] Select Book',
+ props<{ id: string }>()
+);
-export class SelectBook implements Action {
- readonly type = ViewBookPageActionTypes.SelectBook;
-
- constructor(public payload: string) {}
-}
-
-export type ViewBookPageActionsUnion = SelectBook;
+export type ViewBookPageActionsUnion = ReturnType;
diff --git a/projects/example-app/src/app/books/containers/__snapshots__/selected-book-page.component.spec.ts.snap b/projects/example-app/src/app/books/containers/__snapshots__/selected-book-page.component.spec.ts.snap
index c726290936..945329d0be 100644
--- a/projects/example-app/src/app/books/containers/__snapshots__/selected-book-page.component.spec.ts.snap
+++ b/projects/example-app/src/app/books/containers/__snapshots__/selected-book-page.component.spec.ts.snap
@@ -8,6 +8,7 @@ exports[`Selected Book Page should compile 1`] = `
>
diff --git a/projects/example-app/src/app/books/containers/collection-page.component.spec.ts b/projects/example-app/src/app/books/containers/collection-page.component.spec.ts
index 1047f68381..9db1d0ab4a 100644
--- a/projects/example-app/src/app/books/containers/collection-page.component.spec.ts
+++ b/projects/example-app/src/app/books/containers/collection-page.component.spec.ts
@@ -52,7 +52,7 @@ describe('Collection Page', () => {
});
it('should dispatch a collection.Load on init', () => {
- const action = new CollectionPageActions.LoadCollection();
+ const action = CollectionPageActions.loadCollection();
fixture.detectChanges();
diff --git a/projects/example-app/src/app/books/containers/collection-page.component.ts b/projects/example-app/src/app/books/containers/collection-page.component.ts
index d238835f0a..f0ebfd39e7 100644
--- a/projects/example-app/src/app/books/containers/collection-page.component.ts
+++ b/projects/example-app/src/app/books/containers/collection-page.component.ts
@@ -39,6 +39,6 @@ export class CollectionPageComponent implements OnInit {
}
ngOnInit() {
- this.store.dispatch(new CollectionPageActions.LoadCollection());
+ this.store.dispatch(CollectionPageActions.loadCollection());
}
}
diff --git a/projects/example-app/src/app/books/containers/find-book-page.component.spec.ts b/projects/example-app/src/app/books/containers/find-book-page.component.spec.ts
index 126ccc4e99..76652658ec 100644
--- a/projects/example-app/src/app/books/containers/find-book-page.component.spec.ts
+++ b/projects/example-app/src/app/books/containers/find-book-page.component.spec.ts
@@ -63,7 +63,7 @@ describe('Find Book Page', () => {
it('should dispatch a book.Search action on search', () => {
const $event = 'book name';
- const action = new FindBookPageActions.SearchBooks($event);
+ const action = FindBookPageActions.searchBooks({ query: $event });
instance.search($event);
diff --git a/projects/example-app/src/app/books/containers/find-book-page.component.ts b/projects/example-app/src/app/books/containers/find-book-page.component.ts
index 72cc1ea52f..3e260678ed 100644
--- a/projects/example-app/src/app/books/containers/find-book-page.component.ts
+++ b/projects/example-app/src/app/books/containers/find-book-page.component.ts
@@ -39,6 +39,6 @@ export class FindBookPageComponent {
}
search(query: string) {
- this.store.dispatch(new FindBookPageActions.SearchBooks(query));
+ this.store.dispatch(FindBookPageActions.searchBooks({ query }));
}
}
diff --git a/projects/example-app/src/app/books/containers/selected-book-page.component.spec.ts b/projects/example-app/src/app/books/containers/selected-book-page.component.spec.ts
index db23d0738f..e33dfa6919 100644
--- a/projects/example-app/src/app/books/containers/selected-book-page.component.spec.ts
+++ b/projects/example-app/src/app/books/containers/selected-book-page.component.spec.ts
@@ -48,7 +48,7 @@ describe('Selected Book Page', () => {
it('should dispatch a collection.AddBook action when addToCollection is called', () => {
const $event: Book = generateMockBook();
- const action = new SelectedBookPageActions.AddBook($event);
+ const action = SelectedBookPageActions.addBook({ book: $event });
instance.addToCollection($event);
@@ -57,7 +57,7 @@ describe('Selected Book Page', () => {
it('should dispatch a collection.RemoveBook action on removeFromCollection', () => {
const $event: Book = generateMockBook();
- const action = new SelectedBookPageActions.RemoveBook($event);
+ const action = SelectedBookPageActions.removeBook({ book: $event });
instance.removeFromCollection($event);
diff --git a/projects/example-app/src/app/books/containers/selected-book-page.component.ts b/projects/example-app/src/app/books/containers/selected-book-page.component.ts
index fb7471ffaf..727f5e4d73 100644
--- a/projects/example-app/src/app/books/containers/selected-book-page.component.ts
+++ b/projects/example-app/src/app/books/containers/selected-book-page.component.ts
@@ -32,10 +32,10 @@ export class SelectedBookPageComponent {
}
addToCollection(book: Book) {
- this.store.dispatch(new SelectedBookPageActions.AddBook(book));
+ this.store.dispatch(SelectedBookPageActions.addBook({ book }));
}
removeFromCollection(book: Book) {
- this.store.dispatch(new SelectedBookPageActions.RemoveBook(book));
+ this.store.dispatch(SelectedBookPageActions.removeBook({ book }));
}
}
diff --git a/projects/example-app/src/app/books/containers/view-book-page.component.spec.ts b/projects/example-app/src/app/books/containers/view-book-page.component.spec.ts
index 4bd5118347..c66c5dba32 100644
--- a/projects/example-app/src/app/books/containers/view-book-page.component.spec.ts
+++ b/projects/example-app/src/app/books/containers/view-book-page.component.spec.ts
@@ -56,7 +56,7 @@ describe('View Book Page', () => {
});
it('should dispatch a book.Select action on init', () => {
- const action = new ViewBookPageActions.SelectBook('2');
+ const action = ViewBookPageActions.selectBook({ id: '2' });
params.next({ id: '2' });
fixture.detectChanges();
diff --git a/projects/example-app/src/app/books/containers/view-book-page.component.ts b/projects/example-app/src/app/books/containers/view-book-page.component.ts
index 5d50407e94..b1e13c353b 100644
--- a/projects/example-app/src/app/books/containers/view-book-page.component.ts
+++ b/projects/example-app/src/app/books/containers/view-book-page.component.ts
@@ -29,7 +29,7 @@ export class ViewBookPageComponent implements OnDestroy {
constructor(store: Store, route: ActivatedRoute) {
this.actionsSubscription = route.params
- .pipe(map(params => new ViewBookPageActions.SelectBook(params.id)))
+ .pipe(map(params => ViewBookPageActions.selectBook({ id: params.id })))
.subscribe(store);
}
diff --git a/projects/example-app/src/app/books/effects/book.effects.spec.ts b/projects/example-app/src/app/books/effects/book.effects.spec.ts
index 73b3a37fc3..9147011d6f 100644
--- a/projects/example-app/src/app/books/effects/book.effects.spec.ts
+++ b/projects/example-app/src/app/books/effects/book.effects.spec.ts
@@ -35,12 +35,12 @@ describe('BookEffects', () => {
});
describe('search$', () => {
- it('should return a new book.SearchComplete, with the books, on success, after the de-bounce', () => {
+ it('should return a book.SearchComplete, with the books, on success, after the de-bounce', () => {
const book1 = { id: '111', volumeInfo: {} } as Book;
const book2 = { id: '222', volumeInfo: {} } as Book;
const books = [book1, book2];
- const action = new FindBookPageActions.SearchBooks('query');
- const completion = new BooksApiActions.SearchSuccess(books);
+ const action = FindBookPageActions.searchBooks({ query: 'query' });
+ const completion = BooksApiActions.searchSuccess({ books });
actions$ = hot('-a---', { a: action });
const response = cold('-a|', { a: books });
@@ -55,11 +55,11 @@ describe('BookEffects', () => {
).toBeObservable(expected);
});
- it('should return a new book.SearchError if the books service throws', () => {
- const action = new FindBookPageActions.SearchBooks('query');
- const completion = new BooksApiActions.SearchFailure(
- 'Unexpected Error. Try again later.'
- );
+ it('should return a book.SearchError if the books service throws', () => {
+ const action = FindBookPageActions.searchBooks({ query: 'query' });
+ const completion = BooksApiActions.searchFailure({
+ errorMsg: 'Unexpected Error. Try again later.',
+ });
const error = 'Unexpected Error. Try again later.';
actions$ = hot('-a---', { a: action });
@@ -76,7 +76,7 @@ describe('BookEffects', () => {
});
it(`should not do anything if the query is an empty string`, () => {
- const action = new FindBookPageActions.SearchBooks('');
+ const action = FindBookPageActions.searchBooks({ query: '' });
actions$ = hot('-a---', { a: action });
const expected = cold('---');
diff --git a/projects/example-app/src/app/books/effects/book.effects.ts b/projects/example-app/src/app/books/effects/book.effects.ts
index bee500a9af..d1063c365a 100644
--- a/projects/example-app/src/app/books/effects/book.effects.ts
+++ b/projects/example-app/src/app/books/effects/book.effects.ts
@@ -36,23 +36,24 @@ export class BookEffects {
Action
> =>
this.actions$.pipe(
- ofType(FindBookPageActions.FindBookPageActionTypes.SearchBooks),
+ ofType(FindBookPageActions.searchBooks.type),
debounceTime(debounce, scheduler),
- map(action => action.payload),
- switchMap(query => {
+ switchMap(({ query }) => {
if (query === '') {
return empty;
}
const nextSearch$ = this.actions$.pipe(
- ofType(FindBookPageActions.FindBookPageActionTypes.SearchBooks),
+ ofType(FindBookPageActions.searchBooks.type),
skip(1)
);
return this.googleBooks.searchBooks(query).pipe(
takeUntil(nextSearch$),
- map((books: Book[]) => new BooksApiActions.SearchSuccess(books)),
- catchError(err => of(new BooksApiActions.SearchFailure(err)))
+ map((books: Book[]) => BooksApiActions.searchSuccess({ books })),
+ catchError(err =>
+ of(BooksApiActions.searchFailure({ errorMsg: err }))
+ )
);
})
);
diff --git a/projects/example-app/src/app/books/effects/collection.effects.spec.ts b/projects/example-app/src/app/books/effects/collection.effects.spec.ts
index 62bd9e4ca9..5f6b5fec90 100644
--- a/projects/example-app/src/app/books/effects/collection.effects.spec.ts
+++ b/projects/example-app/src/app/books/effects/collection.effects.spec.ts
@@ -52,11 +52,10 @@ describe('CollectionEffects', () => {
describe('loadCollection$', () => {
it('should return a collection.LoadSuccess, with the books, on success', () => {
- const action = new CollectionPageActions.LoadCollection();
- const completion = new CollectionApiActions.LoadBooksSuccess([
- book1,
- book2,
- ]);
+ const action = CollectionPageActions.loadCollection();
+ const completion = CollectionApiActions.loadBooksSuccess({
+ books: [book1, book2],
+ });
actions$ = hot('-a', { a: action });
const response = cold('-a-b|', { a: book1, b: book2 });
@@ -67,9 +66,9 @@ describe('CollectionEffects', () => {
});
it('should return a collection.LoadFail, if the query throws', () => {
- const action = new CollectionPageActions.LoadCollection();
+ const action = CollectionPageActions.loadCollection();
const error = 'Error!';
- const completion = new CollectionApiActions.LoadBooksFailure(error);
+ const completion = CollectionApiActions.loadBooksFailure({ error });
actions$ = hot('-a', { a: action });
const response = cold('-#', {}, error);
@@ -82,8 +81,8 @@ describe('CollectionEffects', () => {
describe('addBookToCollection$', () => {
it('should return a collection.AddBookSuccess, with the book, on success', () => {
- const action = new SelectedBookPageActions.AddBook(book1);
- const completion = new CollectionApiActions.AddBookSuccess(book1);
+ const action = SelectedBookPageActions.addBook({ book: book1 });
+ const completion = CollectionApiActions.addBookSuccess({ book: book1 });
actions$ = hot('-a', { a: action });
const response = cold('-b', { b: true });
@@ -95,8 +94,8 @@ describe('CollectionEffects', () => {
});
it('should return a collection.AddBookFail, with the book, when the db insert throws', () => {
- const action = new SelectedBookPageActions.AddBook(book1);
- const completion = new CollectionApiActions.AddBookFailure(book1);
+ const action = SelectedBookPageActions.addBook({ book: book1 });
+ const completion = CollectionApiActions.addBookFailure({ book: book1 });
const error = 'Error!';
actions$ = hot('-a', { a: action });
@@ -109,8 +108,10 @@ describe('CollectionEffects', () => {
describe('removeBookFromCollection$', () => {
it('should return a collection.RemoveBookSuccess, with the book, on success', () => {
- const action = new SelectedBookPageActions.RemoveBook(book1);
- const completion = new CollectionApiActions.RemoveBookSuccess(book1);
+ const action = SelectedBookPageActions.removeBook({ book: book1 });
+ const completion = CollectionApiActions.removeBookSuccess({
+ book: book1,
+ });
actions$ = hot('-a', { a: action });
const response = cold('-b', { b: true });
@@ -124,8 +125,10 @@ describe('CollectionEffects', () => {
});
it('should return a collection.RemoveBookFail, with the book, when the db insert throws', () => {
- const action = new SelectedBookPageActions.RemoveBook(book1);
- const completion = new CollectionApiActions.RemoveBookFailure(book1);
+ const action = SelectedBookPageActions.removeBook({ book: book1 });
+ const completion = CollectionApiActions.removeBookFailure({
+ book: book1,
+ });
const error = 'Error!';
actions$ = hot('-a', { a: action });
diff --git a/projects/example-app/src/app/books/effects/collection.effects.ts b/projects/example-app/src/app/books/effects/collection.effects.ts
index f38494e20c..4711f2ddd5 100644
--- a/projects/example-app/src/app/books/effects/collection.effects.ts
+++ b/projects/example-app/src/app/books/effects/collection.effects.ts
@@ -31,15 +31,15 @@ export class CollectionEffects {
@Effect()
loadCollection$: Observable = this.actions$.pipe(
- ofType(CollectionPageActions.CollectionPageActionTypes.LoadCollection),
+ ofType(CollectionPageActions.loadCollection.type),
switchMap(() =>
this.db.query('books').pipe(
toArray(),
- map(
- (books: Book[]) => new CollectionApiActions.LoadBooksSuccess(books)
+ map((books: Book[]) =>
+ CollectionApiActions.loadBooksSuccess({ books })
),
catchError(error =>
- of(new CollectionApiActions.LoadBooksFailure(error))
+ of(CollectionApiActions.loadBooksFailure({ error }))
)
)
)
@@ -47,24 +47,22 @@ export class CollectionEffects {
@Effect()
addBookToCollection$: Observable = this.actions$.pipe(
- ofType(SelectedBookPageActions.SelectedBookPageActionTypes.AddBook),
- map(action => action.payload),
- mergeMap(book =>
+ ofType(SelectedBookPageActions.addBook.type),
+ mergeMap(({ book }) =>
this.db.insert('books', [book]).pipe(
- map(() => new CollectionApiActions.AddBookSuccess(book)),
- catchError(() => of(new CollectionApiActions.AddBookFailure(book)))
+ map(() => CollectionApiActions.addBookSuccess({ book })),
+ catchError(() => of(CollectionApiActions.addBookFailure({ book })))
)
)
);
@Effect()
removeBookFromCollection$: Observable = this.actions$.pipe(
- ofType(SelectedBookPageActions.SelectedBookPageActionTypes.RemoveBook),
- map(action => action.payload),
- mergeMap(book =>
+ ofType(SelectedBookPageActions.removeBook.type),
+ mergeMap(({ book }) =>
this.db.executeWrite('books', 'delete', [book.id]).pipe(
- map(() => new CollectionApiActions.RemoveBookSuccess(book)),
- catchError(() => of(new CollectionApiActions.RemoveBookFailure(book)))
+ map(() => CollectionApiActions.removeBookSuccess({ book })),
+ catchError(() => of(CollectionApiActions.removeBookFailure({ book })))
)
)
);
diff --git a/projects/example-app/src/app/books/guards/book-exists.guard.ts b/projects/example-app/src/app/books/guards/book-exists.guard.ts
index d20856240f..6a33f24615 100644
--- a/projects/example-app/src/app/books/guards/book-exists.guard.ts
+++ b/projects/example-app/src/app/books/guards/book-exists.guard.ts
@@ -54,8 +54,8 @@ export class BookExistsGuard implements CanActivate {
*/
hasBookInApi(id: string): Observable {
return this.googleBooks.retrieveBook(id).pipe(
- map(bookEntity => new BookActions.LoadBook(bookEntity)),
- tap((action: BookActions.LoadBook) => this.store.dispatch(action)),
+ map(bookEntity => BookActions.loadBook({ book: bookEntity })),
+ tap(action => this.store.dispatch(action)),
map(book => !!book),
catchError(() => {
this.router.navigate(['/404']);
diff --git a/projects/example-app/src/app/books/reducers/__snapshots__/books.reducer.spec.ts.snap b/projects/example-app/src/app/books/reducers/__snapshots__/books.reducer.spec.ts.snap
index 3ebd4bd978..0582de529a 100644
--- a/projects/example-app/src/app/books/reducers/__snapshots__/books.reducer.spec.ts.snap
+++ b/projects/example-app/src/app/books/reducers/__snapshots__/books.reducer.spec.ts.snap
@@ -160,7 +160,7 @@ Object {
}
`;
-exports[`BooksReducer SEARCH_COMPLETE & LOAD_SUCCESS should add only new books when books already exist 1`] = `
+exports[`BooksReducer SEARCH_COMPLETE & LOAD_SUCCESS should add only books when books already exist 1`] = `
Object {
"entities": Object {
"1": Object {
@@ -230,7 +230,7 @@ Object {
}
`;
-exports[`BooksReducer SEARCH_COMPLETE & LOAD_SUCCESS should add only new books when books already exist 2`] = `
+exports[`BooksReducer SEARCH_COMPLETE & LOAD_SUCCESS should add only books when books already exist 2`] = `
Object {
"entities": Object {
"1": Object {
diff --git a/projects/example-app/src/app/books/reducers/books.reducer.spec.ts b/projects/example-app/src/app/books/reducers/books.reducer.spec.ts
index 13724e8c8e..fc6429e840 100644
--- a/projects/example-app/src/app/books/reducers/books.reducer.spec.ts
+++ b/projects/example-app/src/app/books/reducers/books.reducer.spec.ts
@@ -30,22 +30,29 @@ describe('BooksReducer', () => {
});
describe('SEARCH_COMPLETE & LOAD_SUCCESS', () => {
+ type BooksActions =
+ | typeof BooksApiActions.searchSuccess
+ | typeof CollectionApiActions.loadBooksSuccess;
function noExistingBooks(
- action: any,
+ action: BooksActions,
booksInitialState: any,
books: Book[]
) {
- const createAction = new action(books);
+ const createAction = action({ books });
const result = reducer(booksInitialState, createAction);
expect(result).toMatchSnapshot();
}
- function existingBooks(action: any, booksInitialState: any, books: Book[]) {
+ function existingBooks(
+ action: BooksActions,
+ booksInitialState: any,
+ books: Book[]
+ ) {
// should not replace existing books
const differentBook2 = { ...books[0], foo: 'bar' };
- const createAction = new action([books[1], differentBook2]);
+ const createAction = action({ books: [books[1], differentBook2] });
const expectedResult = {
ids: [...booksInitialState.ids, books[1].id],
@@ -62,24 +69,24 @@ describe('BooksReducer', () => {
}
it('should add all books in the payload when none exist', () => {
- noExistingBooks(BooksApiActions.SearchSuccess, initialState, [
+ noExistingBooks(BooksApiActions.searchSuccess, initialState, [
book1,
book2,
]);
- noExistingBooks(CollectionApiActions.LoadBooksSuccess, initialState, [
+ noExistingBooks(CollectionApiActions.loadBooksSuccess, initialState, [
book1,
book2,
]);
});
- it('should add only new books when books already exist', () => {
- existingBooks(BooksApiActions.SearchSuccess, initialState, [
+ it('should add only books when books already exist', () => {
+ existingBooks(BooksApiActions.searchSuccess, initialState, [
book2,
book3,
]);
- existingBooks(CollectionApiActions.LoadBooksSuccess, initialState, [
+ existingBooks(CollectionApiActions.loadBooksSuccess, initialState, [
book2,
book3,
]);
@@ -96,7 +103,7 @@ describe('BooksReducer', () => {
};
it('should add a single book, if the book does not exist', () => {
- const action = new BookActions.LoadBook(book1);
+ const action = BookActions.loadBook({ book: book1 });
const result = reducer(fromBooks.initialState, action);
@@ -104,7 +111,7 @@ describe('BooksReducer', () => {
});
it('should return the existing state if the book exists', () => {
- const action = new BookActions.LoadBook(book1);
+ const action = BookActions.loadBook({ book: book1 });
const result = reducer(expectedResult, action);
@@ -114,7 +121,7 @@ describe('BooksReducer', () => {
describe('SELECT', () => {
it('should set the selected book id on the state', () => {
- const action = new ViewBookPageActions.SelectBook(book1.id);
+ const action = ViewBookPageActions.selectBook({ id: book1.id });
const result = reducer(initialState, action);
diff --git a/projects/example-app/src/app/books/reducers/books.reducer.ts b/projects/example-app/src/app/books/reducers/books.reducer.ts
index 847702a719..ec73016799 100644
--- a/projects/example-app/src/app/books/reducers/books.reducer.ts
+++ b/projects/example-app/src/app/books/reducers/books.reducer.ts
@@ -49,8 +49,8 @@ export function reducer(
| CollectionApiActions.CollectionApiActionsUnion
): State {
switch (action.type) {
- case BooksApiActions.BooksApiActionTypes.SearchSuccess:
- case CollectionApiActions.CollectionApiActionTypes.LoadBooksSuccess: {
+ case BooksApiActions.searchSuccess.type:
+ case CollectionApiActions.loadBooksSuccess.type: {
/**
* The addMany function provided by the created adapter
* adds many records to the entity dictionary
@@ -58,10 +58,10 @@ export function reducer(
* the collection is to be sorted, the adapter will
* sort each record upon entry into the sorted array.
*/
- return adapter.addMany(action.payload, state);
+ return adapter.addMany(action.books, state);
}
- case BookActions.BookActionTypes.LoadBook: {
+ case BookActions.loadBook.type: {
/**
* The addOne function provided by the created adapter
* adds one record to the entity dictionary
@@ -69,13 +69,13 @@ export function reducer(
* exist already. If the collection is to be sorted, the adapter will
* insert the new record into the sorted array.
*/
- return adapter.addOne(action.payload, state);
+ return adapter.addOne(action.book, state);
}
- case ViewBookPageActions.ViewBookPageActionTypes.SelectBook: {
+ case ViewBookPageActions.selectBook.type: {
return {
...state,
- selectedBookId: action.payload,
+ selectedBookId: action.id,
};
}
diff --git a/projects/example-app/src/app/books/reducers/collection.reducer.ts b/projects/example-app/src/app/books/reducers/collection.reducer.ts
index 9c246f697e..32889e4f54 100644
--- a/projects/example-app/src/app/books/reducers/collection.reducer.ts
+++ b/projects/example-app/src/app/books/reducers/collection.reducer.ts
@@ -24,38 +24,38 @@ export function reducer(
| CollectionApiActions.CollectionApiActionsUnion
): State {
switch (action.type) {
- case CollectionPageActions.CollectionPageActionTypes.LoadCollection: {
+ case CollectionPageActions.loadCollection.type: {
return {
...state,
loading: true,
};
}
- case CollectionApiActions.CollectionApiActionTypes.LoadBooksSuccess: {
+ case CollectionApiActions.loadBooksSuccess.type: {
return {
loaded: true,
loading: false,
- ids: action.payload.map(book => book.id),
+ ids: action.books.map(book => book.id),
};
}
- case CollectionApiActions.CollectionApiActionTypes.AddBookSuccess:
- case CollectionApiActions.CollectionApiActionTypes.RemoveBookFailure: {
- if (state.ids.indexOf(action.payload.id) > -1) {
+ case CollectionApiActions.addBookSuccess.type:
+ case CollectionApiActions.removeBookFailure.type: {
+ if (state.ids.indexOf(action.book.id) > -1) {
return state;
}
return {
...state,
- ids: [...state.ids, action.payload.id],
+ ids: [...state.ids, action.book.id],
};
}
- case CollectionApiActions.CollectionApiActionTypes.RemoveBookSuccess:
- case CollectionApiActions.CollectionApiActionTypes.AddBookFailure: {
+ case CollectionApiActions.removeBookSuccess.type:
+ case CollectionApiActions.addBookFailure.type: {
return {
...state,
- ids: state.ids.filter(id => id !== action.payload.id),
+ ids: state.ids.filter(id => id !== action.book.id),
};
}
diff --git a/projects/example-app/src/app/books/reducers/index.ts b/projects/example-app/src/app/books/reducers/index.ts
index 97e25c7178..73146eab03 100644
--- a/projects/example-app/src/app/books/reducers/index.ts
+++ b/projects/example-app/src/app/books/reducers/index.ts
@@ -156,6 +156,6 @@ export const isSelectedBookInCollection = createSelector(
getCollectionBookIds,
getSelectedBookId,
(ids, selected) => {
- return selected && ids.indexOf(selected) > -1;
+ return !!selected && ids.indexOf(selected) > -1;
}
);
diff --git a/projects/example-app/src/app/books/reducers/search.reducer.ts b/projects/example-app/src/app/books/reducers/search.reducer.ts
index 4869fdbbc7..6081e245bd 100644
--- a/projects/example-app/src/app/books/reducers/search.reducer.ts
+++ b/projects/example-app/src/app/books/reducers/search.reducer.ts
@@ -24,8 +24,8 @@ export function reducer(
| FindBookPageActions.FindBookPageActionsUnion
): State {
switch (action.type) {
- case FindBookPageActions.FindBookPageActionTypes.SearchBooks: {
- const query = action.payload;
+ case FindBookPageActions.searchBooks.type: {
+ const query = action.query;
if (query === '') {
return {
@@ -44,20 +44,20 @@ export function reducer(
};
}
- case BooksApiActions.BooksApiActionTypes.SearchSuccess: {
+ case BooksApiActions.searchSuccess.type: {
return {
- ids: action.payload.map(book => book.id),
+ ids: action.books.map(book => book.id),
loading: false,
error: '',
query: state.query,
};
}
- case BooksApiActions.BooksApiActionTypes.SearchFailure: {
+ case BooksApiActions.searchFailure.type: {
return {
...state,
loading: false,
- error: action.payload,
+ error: action.errorMsg,
};
}
diff --git a/projects/example-app/src/app/core/actions/layout.actions.ts b/projects/example-app/src/app/core/actions/layout.actions.ts
index c5f055b1b3..1dd87dc536 100644
--- a/projects/example-app/src/app/core/actions/layout.actions.ts
+++ b/projects/example-app/src/app/core/actions/layout.actions.ts
@@ -1,16 +1,7 @@
-import { Action } from '@ngrx/store';
+import { createAction, union } from '@ngrx/store';
-export enum LayoutActionTypes {
- OpenSidenav = '[Layout] Open Sidenav',
- CloseSidenav = '[Layout] Close Sidenav',
-}
+export const openSidenav = createAction('[Layout] Open Sidenav');
+export const closeSidenav = createAction('[Layout] Close Sidenav');
-export class OpenSidenav implements Action {
- readonly type = LayoutActionTypes.OpenSidenav;
-}
-
-export class CloseSidenav implements Action {
- readonly type = LayoutActionTypes.CloseSidenav;
-}
-
-export type LayoutActionsUnion = OpenSidenav | CloseSidenav;
+const all = union({ openSidenav, closeSidenav });
+export type LayoutActionsUnion = typeof all;
diff --git a/projects/example-app/src/app/core/containers/app.component.ts b/projects/example-app/src/app/core/containers/app.component.ts
index fa2acfd019..1797ce9fdb 100644
--- a/projects/example-app/src/app/core/containers/app.component.ts
+++ b/projects/example-app/src/app/core/containers/app.component.ts
@@ -38,7 +38,7 @@ export class AppComponent {
showSidenav$: Observable;
loggedIn$: Observable;
- constructor(private store: Store) {
+ constructor(private store: Store) {
/**
* Selectors can be applied with the `select` operator which passes the state
* tree to the provided selector
@@ -54,16 +54,16 @@ export class AppComponent {
* updates and user interaction through the life of our
* application.
*/
- this.store.dispatch(new LayoutActions.CloseSidenav());
+ this.store.dispatch(LayoutActions.closeSidenav());
}
openSidenav() {
- this.store.dispatch(new LayoutActions.OpenSidenav());
+ this.store.dispatch(LayoutActions.openSidenav());
}
logout() {
this.closeSidenav();
- this.store.dispatch(new AuthActions.LogoutConfirmation());
+ this.store.dispatch(AuthActions.logoutConfirmation());
}
}
diff --git a/projects/example-app/src/app/core/reducers/layout.reducer.ts b/projects/example-app/src/app/core/reducers/layout.reducer.ts
index 3ea44fd0dc..b128a041bd 100644
--- a/projects/example-app/src/app/core/reducers/layout.reducer.ts
+++ b/projects/example-app/src/app/core/reducers/layout.reducer.ts
@@ -1,6 +1,5 @@
-import {
- LayoutActions
-} from '@example-app/core/actions';
+import { Action } from '@ngrx/store';
+import { LayoutActions } from '@example-app/core/actions';
export interface State {
showSidenav: boolean;
@@ -10,17 +9,16 @@ const initialState: State = {
showSidenav: false,
};
-export function reducer(
- state: State = initialState,
- action: LayoutActions.LayoutActionsUnion
-): State {
- switch (action.type) {
- case LayoutActions.LayoutActionTypes.CloseSidenav:
+export function reducer(state: State = initialState, action: Action): State {
+ const specificAction = action as LayoutActions.LayoutActionsUnion;
+
+ switch (specificAction.type) {
+ case LayoutActions.closeSidenav.type:
return {
showSidenav: false,
};
- case LayoutActions.LayoutActionTypes.OpenSidenav:
+ case LayoutActions.openSidenav.type:
return {
showSidenav: true,
};
diff --git a/projects/example-app/src/app/core/services/google-books.service.spec.ts b/projects/example-app/src/app/core/services/google-books.service.spec.ts
index a74c36f0c0..3a10724b09 100644
--- a/projects/example-app/src/app/core/services/google-books.service.spec.ts
+++ b/projects/example-app/src/app/core/services/google-books.service.spec.ts
@@ -38,7 +38,7 @@ describe('Service: GoogleBooks', () => {
expect(service.searchBooks(queryTitle)).toBeObservable(expected);
expect(http.get).toHaveBeenCalledWith(
- `https://www.googleapis.com/books/v1/volumes?q=${queryTitle}`
+ `https://www.googleapis.com/books/v1/volumes?orderBy=newest&q=${queryTitle}`
);
});
diff --git a/projects/example-app/src/app/core/services/google-books.service.ts b/projects/example-app/src/app/core/services/google-books.service.ts
index 3d5423ebad..5643744ee8 100644
--- a/projects/example-app/src/app/core/services/google-books.service.ts
+++ b/projects/example-app/src/app/core/services/google-books.service.ts
@@ -15,7 +15,7 @@ export class GoogleBooksService {
searchBooks(queryTitle: string): Observable {
return this.http
- .get<{ items: Book[] }>(`${this.API_PATH}?q=${queryTitle}`)
+ .get<{ items: Book[] }>(`${this.API_PATH}?orderBy=newest&q=${queryTitle}`)
.pipe(map(books => books.items || []));
}
diff --git a/projects/example-app/src/app/reducers/index.ts b/projects/example-app/src/app/reducers/index.ts
index 3afb1a641a..8f8256f696 100644
--- a/projects/example-app/src/app/reducers/index.ts
+++ b/projects/example-app/src/app/reducers/index.ts
@@ -8,13 +8,6 @@ import {
import { environment } from '../../environments/environment';
import * as fromRouter from '@ngrx/router-store';
-/**
- * storeFreeze prevents state from being mutated. When mutation occurs, an
- * exception will be thrown. This is useful during development mode to
- * ensure that none of the reducers accidentally mutates the state.
- */
-import { storeFreeze } from 'ngrx-store-freeze';
-
/**
* Every reducer module's default export is the reducer function itself. In
* addition, each module should export a type or interface that describes
@@ -45,7 +38,7 @@ export const reducers: ActionReducerMap = {
// console.log all actions
export function logger(reducer: ActionReducer): ActionReducer {
- return (state: State, action: any): any => {
+ return (state, action) => {
const result = reducer(state, action);
console.groupCollapsed(action.type);
console.log('prev state', state);
@@ -63,7 +56,7 @@ export function logger(reducer: ActionReducer): ActionReducer {
* that will be composed to form the root meta-reducer.
*/
export const metaReducers: MetaReducer[] = !environment.production
- ? [logger, storeFreeze]
+ ? [logger]
: [];
/**
diff --git a/projects/example-app/tsconfig.app.json b/projects/example-app/tsconfig.app.json
index af2f470570..6673d1c4e8 100644
--- a/projects/example-app/tsconfig.app.json
+++ b/projects/example-app/tsconfig.app.json
@@ -1,5 +1,7 @@
{
"compilerOptions": {
+ "strict": true,
+ "strictPropertyInitialization": false,
"sourceMap": true,
"declaration": false,
"moduleResolution": "node",
@@ -18,8 +20,7 @@
"@ngrx/router-store": ["../../modules/router-store"],
"@ngrx/entity": ["../../modules/entity"],
"@ngrx/schematics": ["../../modules/schematics"],
- "@example-app/*": ["./src/app/*"],
- "ngrx-store-freeze": ["../../projects/ngrx-store-freeze"]
+ "@example-app/*": ["./src/app/*"]
}
},
"exclude": [
diff --git a/projects/ngrx-store-freeze/README.md b/projects/ngrx-store-freeze/README.md
deleted file mode 100755
index 168569ff14..0000000000
--- a/projects/ngrx-store-freeze/README.md
+++ /dev/null
@@ -1,56 +0,0 @@
-## ngrx-store-freeze
-
-[![npm version](https://badge.fury.io/js/ngrx-store-freeze.svg)](https://badge.fury.io/js/ngrx-store-freeze)
-[![CircleCI](https://circleci.com/gh/brandonroberts/ngrx-store-freeze/tree/master.svg?style=svg&circle-token=6ba0f6b74d2186f7896a58377b8607346c07cee6)](https://circleci.com/gh/brandonroberts/ngrx-store-freeze/tree/master)
-
-ngrx-store-freeze is a meta-reducer that prevents state from being mutated
-
-- Recursively freezes the **current state**, the dispatched **action payload** if provided and the **new state**.
-- When mutation occurs, an exception will be thrown.
-- Should be used **only in development** to ensure that the state remains immutable.
-
-### Installation
-
-```sh
-npm i --save-dev ngrx-store-freeze
-```
-
-OR
-
-```sh
-yarn add ngrx-store-freeze --dev
-```
-
-### Setup
-
-```ts
-import { StoreModule, MetaReducer, ActionReducerMap } from '@ngrx/store';
-import { storeFreeze } from 'ngrx-store-freeze';
-import { environment } from '../environments/environment'; // Angular CLI environment
-
-export interface State {
- // reducer interfaces
-}
-
-export const reducers: ActionReducerMap = {
- // reducers
-};
-
-export const metaReducers: MetaReducer[] = !environment.production
- ? [storeFreeze]
- : [];
-
-@NgModule({
- imports: [StoreModule.forRoot(reducers, { metaReducers })],
-})
-export class AppModule {}
-```
-
-## Additional Documentation
-
-- [Usage with `@ngrx/router-store`](./docs/docs.md#router-store-compatibility)
-
-## Credits
-
-[redux-freeze](https://github.com/buunguyen/redux-freeze) - Redux middleware that prevents state from being mutated
-[Attila Egyed](https://github.com/tsm91) - The original maintainer of this project
diff --git a/projects/ngrx-store-freeze/index.ts b/projects/ngrx-store-freeze/index.ts
deleted file mode 100644
index cba1843545..0000000000
--- a/projects/ngrx-store-freeze/index.ts
+++ /dev/null
@@ -1 +0,0 @@
-export * from './src/index';
diff --git a/projects/ngrx-store-freeze/package.json b/projects/ngrx-store-freeze/package.json
deleted file mode 100755
index fb825c5331..0000000000
--- a/projects/ngrx-store-freeze/package.json
+++ /dev/null
@@ -1,4 +0,0 @@
-{
- "name": "ngrx-store-freeze",
- "version": "0.2.4"
-}
diff --git a/projects/ngrx-store-freeze/src/index.ts b/projects/ngrx-store-freeze/src/index.ts
deleted file mode 100755
index b53cad56c5..0000000000
--- a/projects/ngrx-store-freeze/src/index.ts
+++ /dev/null
@@ -1,30 +0,0 @@
-declare var require: any;
-import { ActionReducer, Action } from '@ngrx/store';
-const deepFreeze = require('deep-freeze-strict');
-
-/**
- * Meta-reducer that prevents state from being mutated anywhere in the app.
- */
-export function storeFreeze(
- reducer: ActionReducer
-): ActionReducer;
-export function storeFreeze(
- reducer: ActionReducer
-): ActionReducer {
- return function freeze(state, action): any {
- state = state || {};
-
- deepFreeze(state);
-
- // guard against trying to freeze null or undefined types
- if (action.payload) {
- deepFreeze(action.payload);
- }
-
- const nextState = reducer(state, action);
-
- deepFreeze(nextState);
-
- return nextState;
- };
-}
diff --git a/projects/ngrx.io/README.md b/projects/ngrx.io/README.md
index 295e83e1ac..99c33f837d 100644
--- a/projects/ngrx.io/README.md
+++ b/projects/ngrx.io/README.md
@@ -42,6 +42,10 @@ Here are the most important tasks you might need to use:
- `yarn build-ie-polyfills` - generates a js file of polyfills that can be loaded in Internet Explorer.
+## Developing on Windows
+
+It is necessary to run `yarn setup` and `yarn boilerplate:add` using Administrator rights as Linux-specific symlinks are used.
+
## Using ServiceWorker locally
Running `yarn start` (even when explicitly targeting production mode) does not set up the
diff --git a/projects/ngrx.io/content/guide/effects/testing.md b/projects/ngrx.io/content/guide/effects/testing.md
index 85c4dcad33..20ed86d2fa 100644
--- a/projects/ngrx.io/content/guide/effects/testing.md
+++ b/projects/ngrx.io/content/guide/effects/testing.md
@@ -12,8 +12,7 @@ Usage:
import { TestBed } from '@angular/core/testing';
import { provideMockActions } from '@ngrx/effects/testing';
-import { ReplaySubject } from 'rxjs/ReplaySubject';
-import { hot, cold } from 'jasmine-marbles';
+import { cold, hot } from 'jasmine-marbles';
import { Observable } from 'rxjs';
import { MyEffects } from './my-effects';
@@ -48,14 +47,44 @@ describe('My Effects', () => {
expect(effects.someSource$).toBeObservable(expected);
});
+});
+
- it('should work also', () => {
- actions = new ReplaySubject(1);
+It is also possible to use `ReplaySubject` as an alternative for marble tests:
+
+
+import { TestBed } from '@angular/core/testing';
+import { provideMockActions } from '@ngrx/effects/testing';
+import { ReplaySubject } from 'rxjs';
+
+import { MyEffects } from './my-effects';
+import * as MyActions from '../actions/my-actions';
+
+describe('My Effects', () => {
+ let effects: MyEffects;
+ let actions: ReplaySubject<any>;
+
+ beforeEach(() => {
+ TestBed.configureTestingModule({
+ imports: [
+ // any modules needed
+ ],
+ providers: [
+ MyEffects,
+ provideMockActions(() => actions),
+ // other providers
+ ],
+ });
- actions.next(SomeAction);
+ effects = TestBed.get(MyEffects);
+ });
+
+ it('should work', () => {
+ actions = new ReplaySubject(1);
+ actions.next(new MyActions.ExampleAction());
effects.someSource$.subscribe(result => {
- expect(result).toEqual(AnotherAction);
+ expect(result).toEqual(new MyActions.ExampleActionSuccess());
});
});
});
diff --git a/projects/ngrx.io/content/guide/entity/index.md b/projects/ngrx.io/content/guide/entity/index.md
index 550a25750c..60a1c87797 100644
--- a/projects/ngrx.io/content/guide/entity/index.md
+++ b/projects/ngrx.io/content/guide/entity/index.md
@@ -11,3 +11,13 @@ Entity provides an API to manipulate and query entity collections.
## Installation
Detailed installation instructions can be found on the [Installation](guide/entity/install) page.
+
+## Entity and class instances
+
+Entity promotes the use of plain JavaScript objects when managing collections. *ES6 class instances will be transformed into plain JavaScript objects when entities are managed in a collection*. This provides you with some assurances when managing these entities:
+
+1. Guarantee that the data structures contained in state don't themselves contain logic, reducing the chance that they'll mutate themselves.
+2. State will always be serializable allowing you to store and rehydrate from browser storage mechanisms like local storage.
+3. State can be inspected via the Redux Devtools.
+
+This is one of the [core principles](docs#core-principles) of NgRx. The [Redux docs](https://redux.js.org/faq/organizingstate#can-i-put-functions-promises-or-other-non-serializable-items-in-my-store-state) also offers some more insight into this constraint.
diff --git a/projects/ngrx.io/content/guide/entity/interfaces.md b/projects/ngrx.io/content/guide/entity/interfaces.md
index cce9f27604..f360df7e3a 100644
--- a/projects/ngrx.io/content/guide/entity/interfaces.md
+++ b/projects/ngrx.io/content/guide/entity/interfaces.md
@@ -14,7 +14,7 @@ interface EntityState<V> {
- `ids`: An array of all the primary ids in the collection
- `entities`: A dictionary of entities in the collection indexed by the primary id
-Extend this interface to provided any additional properties for the entity state.
+Extend this interface to provide any additional properties for the entity state.
Usage:
diff --git a/projects/ngrx.io/content/guide/router-store/configuration.md b/projects/ngrx.io/content/guide/router-store/configuration.md
index 3064875484..c9050c2d97 100644
--- a/projects/ngrx.io/content/guide/router-store/configuration.md
+++ b/projects/ngrx.io/content/guide/router-store/configuration.md
@@ -17,8 +17,6 @@ interface StoreRouterConfig {
During each navigation cycle, a `RouterNavigationAction` is dispatched with a snapshot of the state in its payload, the `RouterStateSnapshot`. The `RouterStateSnapshot` is a large complex structure, containing many pieces of information about the current state and what's rendered by the router. This can cause performance
issues when used with the Store Devtools. In most cases, you may only need a piece of information from the `RouterStateSnapshot`. In order to pare down the `RouterStateSnapshot` provided during navigation, you provide a custom serializer for the snapshot to only return what you need to be added to the payload and store.
-Additionally, the router state snapshot is a mutable object, which can cause issues when developing with [store freeze](https://github.com/brandonroberts/ngrx-store-freeze) to prevent direct state mutations. This can be avoided by using a custom serializer.
-
Your custom serializer should implement the abstract class `RouterStateSerializer` and return a snapshot which should have an interface extending `BaseRouterStoreState`.
You then provide the serializer through the config.
diff --git a/projects/ngrx.io/content/guide/schematics/action.md b/projects/ngrx.io/content/guide/schematics/action.md
index 3a1c4d808e..25351d67fd 100644
--- a/projects/ngrx.io/content/guide/schematics/action.md
+++ b/projects/ngrx.io/content/guide/schematics/action.md
@@ -40,6 +40,13 @@ Group the action file within an `actions` folder.
- Type: `boolean`
- Default: `false`
+Specifies if api success and failure actions should be generated.
+
+- `--api`
+ - Alias: `-a`
+ - Type: `boolean`
+ - Default: `false`
+
Generate a spec file alongside the action file.
- `--spec`
diff --git a/projects/ngrx.io/content/guide/schematics/container.md b/projects/ngrx.io/content/guide/schematics/container.md
index d3d9d2268e..f7220afcd1 100644
--- a/projects/ngrx.io/content/guide/schematics/container.md
+++ b/projects/ngrx.io/content/guide/schematics/container.md
@@ -42,3 +42,13 @@ Generate a `UsersPage` container component with your reducers imported and the `
```sh
ng generate container UsersPage --state reducers/index.ts --stateInterface MyState
```
+
+If you want to generate a container with an scss file, add `@ngrx/schematics:container` to the `schematics` in your `angular.json`.
+
+```json
+"schematics": {
+ "@ngrx/schematics:container": {
+ "styleext": "scss"
+ }
+}
+```
diff --git a/projects/ngrx.io/content/guide/schematics/effect.md b/projects/ngrx.io/content/guide/schematics/effect.md
index 3b62d951ee..0ceedf7c8b 100644
--- a/projects/ngrx.io/content/guide/schematics/effect.md
+++ b/projects/ngrx.io/content/guide/schematics/effect.md
@@ -51,6 +51,13 @@ When used with the `--module` option, it registers an effect within the `Angular
- Type: `boolean`
- Default: `false`
+Specifies if effect has api success and failure actions wired up.
+
+- `--api`
+ - Alias: `-a`
+ - Type: `boolean`
+ - Default: `false`
+
Generate a spec file alongside the action file.
- `--spec`
diff --git a/projects/ngrx.io/content/guide/schematics/feature.md b/projects/ngrx.io/content/guide/schematics/feature.md
index bb0e50dff6..cf0f618b60 100644
--- a/projects/ngrx.io/content/guide/schematics/feature.md
+++ b/projects/ngrx.io/content/guide/schematics/feature.md
@@ -51,6 +51,13 @@ Provide the path to a `reducers` file containing a state interface and a object
- Alias: `-r`
- Type: `string`
+Specifies if api success and failure `actions`, `reducer`, and `effects` should be generated as part of this feature.
+
+- `--api`
+ - Alias: `-a`
+ - Type: `boolean`
+ - Default: `false`
+
Generate spec files associated with the feature files.
- `--spec`
diff --git a/projects/ngrx.io/content/guide/schematics/reducer.md b/projects/ngrx.io/content/guide/schematics/reducer.md
index 6f8385277f..3f40776241 100644
--- a/projects/ngrx.io/content/guide/schematics/reducer.md
+++ b/projects/ngrx.io/content/guide/schematics/reducer.md
@@ -52,6 +52,13 @@ Provide the path to a `reducers` file containing a state interface and an object
- Alias: `-r`
- Type: `string`
+Specifies if api success and failure actions should be added to the reducer.
+
+- `--api`
+ - Alias: `-a`
+ - Type: `boolean`
+ - Default: `false`
+
Generate a spec file alongside the reducer file.
- `--spec`
diff --git a/projects/ngrx.io/content/guide/store/testing.md b/projects/ngrx.io/content/guide/store/testing.md
index 71d37a99bc..13e24f3f9c 100644
--- a/projects/ngrx.io/content/guide/store/testing.md
+++ b/projects/ngrx.io/content/guide/store/testing.md
@@ -1,6 +1,65 @@
# Testing
-### Providing Store for testing
+### Using a Mock Store
+
+The `provideMockStore()` function registers providers that allow you to mock out the `Store` for testing functionality that has a dependency on `Store` without setting up reducers.
+You can write tests validating behaviors corresponding to the specific state snapshot easily.
+
+
+
+**Note:** All dispatched actions don't affect to the state, but you can see them in the `Actions` stream.
+
+
+
+Usage:
+
+
+import { TestBed } from '@angular/core/testing';
+import { Store } from '@ngrx/store';
+import { provideMockStore } from '@ngrx/store/testing';
+import { cold } from 'jasmine-marbles';
+
+import { AuthGuard } from '../guards/auth.guard';
+import * as AuthActions from '../actions/auth-actions';
+
+describe('Auth Guard', () => {
+ let guard: AuthGuard;
+ let store: MockStore<{ loggedIn: boolean }>;
+ const initialState = { loggedIn: false };
+
+ beforeEach(() => {
+ TestBed.configureTestingModule({
+ imports: [
+ // any modules needed
+ ],
+ providers: [
+ AuthGuard,
+ provideMockStore({ initialState }),
+ // other providers
+ ],
+ });
+
+ guard = TestBed.get(AuthGuard);
+ store = TestBed.get(Store);
+ });
+
+ it('should return false if the user state is not logged in', () => {
+ const expected = cold('(a|)', { a: false });
+
+ expect(guard.canActivate()).toBeObservable(expected);
+ });
+
+ it('should return true if the user state is logged in', () => {
+ store.setState({ loggedIn: true });
+
+ const expected = cold('(a|)', { a: true });
+
+ expect(guard.canActivate()).toBeObservable(expected);
+ });
+});
+
+
+### Using Store for Integration Testing
Use the `StoreModule.forRoot` in your `TestBed` configuration when testing components or services that inject `Store`.
diff --git a/projects/ngrx.io/content/marketing/events.html b/projects/ngrx.io/content/marketing/events.html
index 5f1756cec3..436c358dc7 100755
--- a/projects/ngrx.io/content/marketing/events.html
+++ b/projects/ngrx.io/content/marketing/events.html
@@ -2,73 +2,5 @@
Events
-Upcoming Events presenting about NgRx:
-
-
-
- Event |
- Location |
- Date |
-
-
-
-
- ngAtlanta |
- Atlanta, Georgia |
- January 9 - 12, 2019 |
-
-
- ng-India |
- Gurgaon, India |
- February 23, 2019 |
-
-
- ng-conf |
- Salt Lake City, Utah |
- May 1 - 3, 2019 |
-
-
-
-Past Events:
-
-
-
- Event |
- Location |
- Date |
-
-
-
-
- NgColombia |
- MedellÃn, Colombia |
- September 6 - 7th, 2018 |
-
-
- DevFestATL |
- Atlanta, GA |
- September 22nd, 2018 |
-
-
- Framework Summit |
- Park City, UT |
- September 22nd, 2018 |
-
-
- DevFestATL |
- Atlanta, GA |
- October 2 - 3rd, 2018 |
-
-
- AngularMix - Use the discount code "RYAN" or "ROBERTS" to receive $50 off your registration! |
- Orlando, FL |
- October 10 - 12th, 2018 |
-
-
- AngularConnect |
- Excel, London |
- November 6 - 7th, 2018 |
-
-
-
+
diff --git a/projects/ngrx.io/content/marketing/events.json b/projects/ngrx.io/content/marketing/events.json
new file mode 100644
index 0000000000..74e3b6c033
--- /dev/null
+++ b/projects/ngrx.io/content/marketing/events.json
@@ -0,0 +1,115 @@
+[
+ {
+ "name": "NgColombia",
+ "url": "http://www.ngcolombia.com",
+ "location": "MedellÃn, Colombia",
+ "startDate": "2018-09-06",
+ "endDate": "2018-09-07"
+ },
+ {
+ "name": "DevFestATL",
+ "url": "http://devfestatl.com",
+ "location": "Atlanta, Georgia",
+ "endDate": "2018-09-22"
+ },
+ {
+ "name": "Framework Summit",
+ "url": "http://frameworksummit.com",
+ "location": "Park City, Utah",
+ "endDate": "2018-09-22"
+ },
+ {
+ "name": "DevFestATL",
+ "url": "http://devfestatl.com",
+ "location": "Atlanta, Georgia",
+ "startDate": "2018-10-02",
+ "endDate": "2018-10-03"
+ },
+ {
+ "name": "AngularMix",
+ "url": "http://www.angularmix.com",
+ "location": "Orlando, Florida",
+ "startDate": "2018-10-10",
+ "endDate": "2018-10-12"
+ },
+ {
+ "name": "AngularConnect",
+ "url": "https://www.angularconnect.com",
+ "location": "Excel, London",
+ "startDate": "2018-11-06",
+ "endDate": "2018-11-07"
+ },
+ {
+ "name": "ngAtlanta",
+ "url": "http://ng-atl.org/",
+ "location": "Atlanta, Georgia",
+ "startDate": "2019-01-09",
+ "endDate": "2019-01-12"
+ },
+ {
+ "name": "ng-India",
+ "url": "https://www.ng-ind.com",
+ "location": "Gurgaon, India",
+ "endDate": "2019-02-23"
+ },
+ {
+ "name": "Open Source 101",
+ "url": "https://opensource101.com/",
+ "location": "Columbia, South Carolina",
+ "endDate": "2019-04-18"
+ },
+ {
+ "name": "NG-Conf",
+ "url": "https://www.ng-conf.org",
+ "location": "Salt Lake City, Utah",
+ "startDate": "2019-05-01",
+ "endDate": "2019-05-03"
+ },
+ {
+ "name": "ngVikings",
+ "url": "https://ngvikings.org/",
+ "location": "Copenhagen, Denmark",
+ "startDate": "2019-05-26",
+ "endDate": "2019-05-28"
+ },
+ {
+ "name": "REFACTR.TECH",
+ "url": "https://refactr.tech/",
+ "location": "Atlanta, Georgia",
+ "startDate": "2019-06-05",
+ "endDate": "2019-06-07"
+ },
+ {
+ "name": "AngularUP",
+ "url": "https://angular-up.com/",
+ "location": "Tel Aviv, Israel",
+ "endDate": "2019-06-12"
+ },
+ {
+ "name": "NG-MY",
+ "url": "https://ng-my.org/",
+ "location": "Kuala Lumpur, Malaysia",
+ "startDate": "2019-07-06",
+ "endDate": "2019-07-07"
+ },
+ {
+ "name": "ngDenver",
+ "url": "http://angulardenver.com/",
+ "location": "Denver, Colorado",
+ "startDate": "2019-08-01",
+ "endDate": "2019-08-02"
+ },
+ {
+ "name": "AngularConnect",
+ "url": "https://www.angularconnect.com/",
+ "location": "London, United Kingdom",
+ "startDate": "2019-09-19",
+ "endDate": "2019-09-20"
+ },
+ {
+ "name": "NG-Rome",
+ "url": "https://ngrome.io/",
+ "location": "Rome, Italy",
+ "endDate": "2019-10-07"
+ }
+]
diff --git a/projects/ngrx.io/content/marketing/resources.json b/projects/ngrx.io/content/marketing/resources.json
index 1fbd94651d..c246127fa0 100644
--- a/projects/ngrx.io/content/marketing/resources.json
+++ b/projects/ngrx.io/content/marketing/resources.json
@@ -14,7 +14,7 @@
"ngrx-best-practices-for-enterprise-angular-applications": {
"title": "NgRx — Best Practices for Enterprise Angular Applications",
"desc": "Enterprise NgRx pattern for organizing store into modules and sub modules",
- "url": "https://itnext.io/ngrx-best-practices-for-enterprise-angular-applications-6f00bcdf36d7",
+ "url": "https://wesleygrimes.com/angular/2018/05/30/ngrx-best-practices-for-enterprise-angular-applications",
"rev": true
}
}
@@ -69,6 +69,12 @@
"url": "https://github.com/avatsaev/angular-contacts-app-example",
"rev": true
},
+ "angular-checklist": {
+ "title": "Angular Checklist",
+ "desc": "Curated list of common mistakes made when developing Angular applications",
+ "url": "https://angular-checklist.io",
+ "rev": true
+ },
"online-training": {
"title":
"Online Training website using ASP.Net Core 2.0 & Angular 4",
@@ -87,6 +93,20 @@
"Use a Pizza Provider of your choice and add/edit orders in real time with friends/colleagues",
"url": "https://github.com/maxime1992/pizza-sync",
"rev": true
+ },
+ "goose-weather": {
+ "title": "Goose Weather",
+ "desc":
+ "Angular App that uses NOAA and the OpenWeatherMapAPI to create a weather forecast",
+ "url": "https://github.com/andrewevans02/goose-weather",
+ "rev": true
+ },
+ "mdb-angular-boilerplate": {
+ "title": "MDB Angular Boilerplate",
+ "desc":
+ "Angular CRUD application starter with NgRx state management and Firebase backend",
+ "url": "https://github.com/mdbootstrap/Angular-Bootstrap-Boilerplate",
+ "rev": true
}
}
},
diff --git a/projects/ngrx.io/src/404-body.html b/projects/ngrx.io/src/404-body.html
index 3e6cf878fb..7794bc9c08 100644
--- a/projects/ngrx.io/src/404-body.html
+++ b/projects/ngrx.io/src/404-body.html
@@ -42,7 +42,7 @@ Resource Not Found