Skip to content
This repository has been archived by the owner on Jan 10, 2018. It is now read-only.

Commit

Permalink
feat(Store): Enable fractal state management
Browse files Browse the repository at this point in the history
  • Loading branch information
MikeRyanDev committed Feb 27, 2017
1 parent 30544e9 commit 684d04b
Show file tree
Hide file tree
Showing 32 changed files with 1,399 additions and 970 deletions.
3 changes: 3 additions & 0 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"typescript.tsdk": "./node_modules/typescript/lib"
}
8 changes: 4 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ RxJS powered state management for Angular applications, inspired by Redux
on top of Angular. Core tenets:
- State is a single immutable data structure
- Actions describe state changes
- Pure functions called reducers take the previous state and the next action to compute the new state
- Pure functions called reducers take previous slices of state and the next action to compute the new state
- State accessed with the `Store`, an observable of state and an observer of actions

These core principles enable building components that can use the `OnPush` change detection strategy
Expand Down Expand Up @@ -57,7 +57,7 @@ export const INCREMENT = 'INCREMENT';
export const DECREMENT = 'DECREMENT';
export const RESET = 'RESET';

export const counterReducer: ActionReducer<number> = (state: number = 0, action: Action) => {
export function counterReducer(state: number = 0, action: Action): number {
switch (action.type) {
case INCREMENT:
return state + 1;
Expand All @@ -74,7 +74,7 @@ export const counterReducer: ActionReducer<number> = (state: number = 0, action:
}
```

In your app's main module, import those reducers and use the `StoreModule.provideStore(reducers)`
In your app's main module, import those reducers and use the `StoreModule.forRoot(reducers)`
function to provide them to Angular's injector:

```ts
Expand All @@ -85,7 +85,7 @@ import { counterReducer } from './counter';
@NgModule({
imports: [
BrowserModule,
StoreModule.provideStore({ counter: counterReducer })
StoreModule.forRoot({ counter: counterReducer })
]
})
export class AppModule {}
Expand Down
Empty file added docs/action-reducers.md
Empty file.
Empty file added docs/actions.md
Empty file.
Empty file added docs/store-module.md
Empty file.
Empty file added docs/store.md
Empty file.
14 changes: 8 additions & 6 deletions index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
export * from './src/dispatcher';
export * from './src/ng2';
export * from './src/reducer';
export * from './src/state';
export * from './src/store';
export * from './src/utils';
import * as __private_export__ from './src/private_export';

export { Action, ActionReducer, ActionReducerMap, ActionReducerFactory } from './src/models';
export { StoreModule } from './src/store_module';
export { Store } from './src/store';
export { combineReducers, compose } from './src/utils';
export { __private_export__ };

51 changes: 27 additions & 24 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
"scripts": {
"karma": "karma start --single-run",
"test:unit": "node tests.js",
"test:unit:coverage": "nyc npm run test:unit",
"test:ngc": "ngc -p ./spec/ngc/tsconfig.ngc.json",
"test": "npm run test:unit && npm run test:ngc",
"clean:pre": "rimraf release",
Expand Down Expand Up @@ -35,38 +36,40 @@
},
"homepage": "https://github.com/ngrx/store#readme",
"peerDependencies": {
"@angular/core": "^2.1.0",
"@angular/core": "4.0.0-rc.1",
"@ngrx/core": "^1.1.0",
"rxjs": "^5.0.0-beta.12"
"rxjs": "^5.0.0"
},
"devDependencies": {
"@angular/common": "^2.1.0",
"@angular/compiler": "^2.1.0",
"@angular/compiler-cli": "^2.1.0",
"@angular/core": "^2.1.0",
"@angular/platform-browser": "^2.1.0",
"@angular/platform-browser-dynamic": "^2.1.0",
"@angular/platform-server": "^2.1.0",
"@angular/common": "4.0.0-rc.1",
"@angular/compiler": "4.0.0-rc.1",
"@angular/compiler-cli": "4.0.0-rc.1",
"@angular/core": "4.0.0-rc.1",
"@angular/http": "4.0.0-rc.1",
"@angular/platform-browser": "4.0.0-rc.1",
"@angular/platform-browser-dynamic": "4.0.0-rc.1",
"@angular/platform-server": "4.0.0-rc.1",
"@ngrx/core": "^1.2.0",
"@types/jasmine": "^2.2.33",
"@types/node": "^6.0.38",
"awesome-typescript-loader": "^2.2.1",
"@types/jasmine": "^2.5.42",
"@types/node": "^7.0.5",
"awesome-typescript-loader": "^3.0.4-rc.2",
"core-js": "^2.4.1",
"cpy-cli": "^1.0.1",
"istanbul-instrumenter-loader": "^0.2.0",
"jasmine": "^2.5.2",
"nyc": "^8.3.2",
"istanbul-instrumenter-loader": "^2.0.0",
"jasmine": "^2.5.3",
"jasmine-marbles": "^0.0.2",
"nyc": "^10.1.2",
"rimraf": "^2.5.4",
"rollup": "^0.34.13",
"rxjs": "^5.0.0-beta.11",
"ts-loader": "^0.8.2",
"ts-node": "^1.6.1",
"tslint": "^3.15.1",
"tslint-loader": "^2.1.5",
"typescript": "^2.0.2",
"rollup": "^0.41.4",
"rxjs": "^5.1.0",
"ts-loader": "^2.0.0",
"ts-node": "^2.1.0",
"tslint": "^4.4.2",
"tslint-loader": "^3.3.0",
"typescript": "^2.1.6",
"uglifyjs": "^2.4.10",
"webpack": "^2.1.0-beta.21",
"zone.js": "^0.6.17"
"webpack": "^2.2.1",
"zone.js": "^0.7.6"
},
"nyc": {
"extension": [
Expand Down
29 changes: 9 additions & 20 deletions spec/edge.spec.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
import {ReflectiveInjector} from '@angular/core';
import {Observable} from 'rxjs/Observable';
import {todos, todoCount} from './fixtures/edge_todos';

import {Store, StoreModule, Dispatcher, State, Action, combineReducers} from '../';
import { Observable } from 'rxjs/Observable';
import { todos, todoCount } from './fixtures/edge_todos';
import { createInjector } from './helpers/injector';
import { Store, StoreModule } from '../';


interface TestAppSchema {
Expand All @@ -14,41 +13,32 @@ interface TestAppSchema {
interface Todo { }

interface TodoAppSchema {
visibilityFilter: string;
todoCount: number;
todos: Todo[];
}



describe('ngRx Store', () => {

describe('basic store actions', function() {

let injector: ReflectiveInjector;
let store: Store<TestAppSchema>;
let dispatcher: Dispatcher;
describe('basic store actions', () => {
let store: Store<TodoAppSchema>;

beforeEach(() => {

injector = ReflectiveInjector.resolveAndCreate([
StoreModule.provideStore({ todos, todoCount }).providers
]);
const injector = createInjector(StoreModule.forRoot<TodoAppSchema>({ todos, todoCount }));

store = injector.get(Store);
dispatcher = injector.get(Dispatcher);
});

it('should provide an Observable Store', () => {
expect(store).toBeDefined();
});

it('should handle re-entrancy', (done) => {

let todosNextCount = 0;
let todosCountNextCount = 0;

store.select('todos').subscribe((todos: any[]) => {
todosNextCount++
todosNextCount++;
store.dispatch({ type: 'SET_COUNT', payload: todos.length })
});

Expand All @@ -65,7 +55,6 @@ describe('ngRx Store', () => {
expect(todosCountNextCount).toBe(2);
done();
}, 10);

});
});
});
16 changes: 11 additions & 5 deletions spec/fixtures/todos.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,9 @@
export interface TodoItem {
id: number;
completed: boolean;
text: string;
}

export const ADD_TODO = 'ADD_TODO';
export const COMPLETE_TODO = 'COMPLETE_TODO';
export const SET_VISIBILITY_FILTER = 'SET_VISIBILITY_FILTER';
Expand All @@ -20,7 +26,7 @@ export function visibilityFilter(state = VisibilityFilters.SHOW_ALL, {type, payl
}
};

export function todos(state = [], {type, payload}) {
export function todos(state: TodoItem[] = [], {type, payload}): TodoItem[] {
switch (type) {
case ADD_TODO:
return [
Expand All @@ -32,11 +38,11 @@ export function todos(state = [], {type, payload}) {
}
];
case COMPLETE_ALL_TODOS:
return state.map(todo => Object.assign({}, todo, {completed: true}));
return state.map(todo => ({ ...todo, completed: true }));
case COMPLETE_TODO:
return state.map(todo => {
return todo.id === payload.id ? Object.assign({}, todo, {completed: true}) : todo;
});
return state.map(todo =>
todo.id === payload.id ? { ...todo, completed: true } : todo
);
default:
return state;
}
Expand Down
18 changes: 18 additions & 0 deletions spec/helpers/injector.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { ReflectiveInjector, ModuleWithProviders } from '@angular/core';


export function createInjector({ ngModule, providers }: ModuleWithProviders): ReflectiveInjector {
const injector = ReflectiveInjector.resolveAndCreate([ ...(providers || []), ngModule ]);

injector.get(ngModule);

return injector;
}

export function createChildInjector(parent: ReflectiveInjector, { ngModule, providers }: ModuleWithProviders): ReflectiveInjector {
const injector = parent.resolveAndCreateChild([ ...(providers || []), ngModule ]);

injector.get(ngModule);

return injector;
}
92 changes: 43 additions & 49 deletions spec/integration.spec.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
import {Observable} from 'rxjs/Observable';
import {ReflectiveInjector} from '@angular/core';
import 'rxjs/add/observable/combineLatest';

import {Store, StoreModule, Action, combineReducers, INITIAL_REDUCER, INITIAL_STATE} from '../';
import {counterReducer, INCREMENT, DECREMENT, RESET} from './fixtures/counter';
import {todos, visibilityFilter, VisibilityFilters, SET_VISIBILITY_FILTER, ADD_TODO, COMPLETE_TODO, COMPLETE_ALL_TODOS} from './fixtures/todos';
import 'rxjs/add/operator/first';
import { Observable } from 'rxjs/Observable';
import { TestBed } from '@angular/core/testing';
import { Store, StoreModule, Action, combineReducers } from '../';
import { ReducerManager, INITIAL_STATE, State } from '../src/private_export';
import { counterReducer, INCREMENT, DECREMENT, RESET } from './fixtures/counter';
import { todos, visibilityFilter, VisibilityFilters, SET_VISIBILITY_FILTER, ADD_TODO, COMPLETE_TODO, COMPLETE_ALL_TODOS } from './fixtures/todos';

interface Todo {
id: number;
Expand All @@ -20,89 +21,82 @@ interface TodoAppSchema {
describe('ngRx Integration spec', () => {

describe('todo integration spec', function() {

let injector: ReflectiveInjector;
let store: Store<TodoAppSchema>;
let currentState: TodoAppSchema;
let state: State<TodoAppSchema>;

const rootReducer = combineReducers({ todos, visibilityFilter });
const initialValue = { todos: [], visibilityFilter: VisibilityFilters.SHOW_ALL };
const initialState = { todos: [], visibilityFilter: VisibilityFilters.SHOW_ALL };
const reducers = { todos, visibilityFilter };

injector = ReflectiveInjector.resolveAndCreate([
StoreModule.provideStore(rootReducer, initialValue).providers
]);
beforeEach(() => {
spyOn(reducers, 'todos').and.callThrough();

store = injector.get(Store);
TestBed.configureTestingModule({
imports: [
StoreModule.forRoot(reducers, { initialState }),
]
});

store.subscribe(state => {
currentState = state;
store = TestBed.get(Store);
state = TestBed.get(State);
});

it('should successfully instantiate', () => {
expect(store).toBeDefined();
});

it('should combine reducers automatically if a key/value map is provided', () => {
const reducers = { test: function(){} };
spyOn(reducers, 'test');
const action = { type: 'Test Action' };
const reducer = ReflectiveInjector.resolveAndCreate([ StoreModule.provideStore(reducers).providers ]).get(INITIAL_REDUCER);

expect(reducer).toBeDefined();
expect(typeof reducer === 'function').toBe(true);
const reducer$: ReducerManager = TestBed.get(ReducerManager);

reducer(undefined, action);

expect(reducers.test).toHaveBeenCalledWith(undefined, action);
});
reducer$.first().subscribe(reducer => {
expect(reducer).toBeDefined();
expect(typeof reducer === 'function').toBe(true);

it('should probe the reducer to resolve the initial state if no initial state is provided', () => {
const reducer = () => 2;
const initialState = ReflectiveInjector.resolveAndCreate([ StoreModule.provideStore(reducer).providers ]).get(INITIAL_STATE);
reducer({ todos: [] }, action);

expect(initialState).toBe(2);
expect(reducers.todos).toHaveBeenCalledWith([], action);
});
});

it('should use a provided initial state', () => {
const reducer = () => 2;
const initialState = ReflectiveInjector.resolveAndCreate([ StoreModule.provideStore(reducer, 3).providers ]).get(INITIAL_STATE);
const resolvedInitialState = TestBed.get(INITIAL_STATE);

expect(initialState).toBe(3);
expect(resolvedInitialState).toEqual(initialState);
});

it('should start with no todos and showing all filter', () => {
expect(currentState.todos.length).toEqual(0);
expect(currentState.visibilityFilter).toEqual(VisibilityFilters.SHOW_ALL);
expect(state.value.todos.length).toEqual(0);
expect(state.value.visibilityFilter).toEqual(VisibilityFilters.SHOW_ALL);
});

it('should add a todo', () => {
store.dispatch({ type: ADD_TODO, payload: { text: 'first todo' } });
expect(currentState.todos.length).toEqual(1);

expect(currentState.todos[0].text).toEqual('first todo');
expect(currentState.todos[0].completed).toEqual(false);
expect(currentState.todos[0].id).toEqual(1);
expect(state.value.todos.length).toEqual(1);
expect(state.value.todos[0].text).toEqual('first todo');
expect(state.value.todos[0].completed).toEqual(false);
});

it('should add another todo', () => {
store.dispatch({ type: ADD_TODO, payload: { text: 'first todo' } });
store.dispatch({ type: ADD_TODO, payload: { text: 'second todo' } });
expect(currentState.todos.length).toEqual(2);

expect(currentState.todos[1].text).toEqual('second todo');
expect(currentState.todos[1].completed).toEqual(false);
expect(currentState.todos[1].id).toEqual(2);
expect(state.value.todos.length).toEqual(2);
expect(state.value.todos[1].text).toEqual('second todo');
expect(state.value.todos[1].completed).toEqual(false);
});

it('should complete the first todo', () => {
store.dispatch({ type: COMPLETE_TODO, payload: { id: 1 } });
expect(currentState.todos.length).toEqual(2);
store.dispatch({ type: ADD_TODO, payload: { text: 'first todo' } });
store.dispatch({ type: COMPLETE_TODO, payload: { id: state.value.todos[0].id } });

expect(currentState.todos[0].text).toEqual('first todo');
expect(currentState.todos[0].completed).toEqual(true);
expect(currentState.todos[0].id).toEqual(1);
expect(state.value.todos[0].completed).toEqual(true);
});

it('should use visibilityFilter to filter todos', () => {
store.dispatch({ type: ADD_TODO, payload: { text: 'first todo' } });
store.dispatch({ type: ADD_TODO, payload: { text: 'second todo' } });
store.dispatch({ type: COMPLETE_TODO, payload: { id: state.value.todos[0].id } });

const filterVisibleTodos = (visibilityFilter, todos) => {
let predicate;
Expand Down
Loading

0 comments on commit 684d04b

Please sign in to comment.