Package Exports
- ng-state
This package does not declare an exports field, so the exports above have been automatically detected and optimized by JSPM instead. If any package subpath is missing, it is recommended to post an issue to the original package (ng-state) to support the "exports" field. If that is not possible, create a JSPM override to customize the exports field for this package.
Readme
ng-state
RxJS and ImmutableJs powered nested state management for Angular 2 applications inspired by @ngrx/store.
ng-state is a controlled nested state container designed to help write performant, consistent applications on top of Angular 2. Core tenets:
- State is a single immutable data structure
- Each component gets its own peace of nested state
- State accessed with
actions
variable under component or theStore
, an observable of state and an observer of global state
These core principles enable building components that can use the OnPush
change detection strategy
giving you intelligent, performant change detection
throughout your application.
Main differences from other RxJs store based state managements solutions
- Developers do not need to rememebr long nested paths to access store
- Decoples / Hides paths to state from components
- Uses Redux like pure functions - actions to interact with state
- Less boilerplate
Installation
Install ng-state from npm:
npm install ng-state --save
Examples
- Official ng-state/example-app is an officially maintained example application showcasing possibilities of @ng-state
Main idea
In order to work with peace of state, current state path (statePath) and current lits item index (stateIndex) is passed down to child components and are received in state actions. Or absolute pats are set in state actions. (see explanation image at the bottom)
Configuration
In your app's main module, register store with initial state by using StoreModule.provideStore(initialState)
( where initialState is imported function returing plain object ) function to provide them to Angular's injector:
import { NgModule } from '@angular/core'
import { StoreModule } from 'ng-state';
@NgModule({
imports: [
BrowserModule,
StoreModule.provideStore(initialState)
]
})
export class AppModule {}
export function initialState() {
return {
todos: []
};
}
Then create actions for each component state by decorating class with @InjectStore decorator and HasStore inheritance. This action will receive only that peace of nested state wich is provided as first parameter.
import * as Immutable from 'immutable';
import { HasStore, InjectStore } from "../../react-state/decorators/inject-store.decorator";
import { Store } from "../../react-state/store/store";
import { TodoModel } from "./todo.model";
@InjectStore('todos')
export class TodosStateActions implements HasStore {
store: Store<any>;
addTodo(item: TodoModel) {
this.store.update(state => {
state.push(Immutable.fromJS(item));
})
}
deleteTodo(index: number) {
this.store.update(state => {
state.delete(index);
}, false);
}
get todos() {
return this.store.map((state) => {
return state.toArray();
});
}
}
Be aware that from version 1.2.5 simple getters are converted to properties to get better performance by reducing calls to functions.
InjectStore first parameter is path:
- if added between single quotes '' it counts as absolute path
- if added in array [], final path will be merrged with path passed from parent ([statePath]="statePath"):
['b'] -> ['a', 'b']
- if state is part of the list, ${stateIndex} param should be passed from the parent component and new path will look like:
['b', '${stateIndex}'] -> ['a', 'b', 0]
- stateIndex param can be used in absolute path as well:
'a/b/${statePath}' -> ['a', 'b', 0]
- stateIndex can be an array of indexes so state path can have multiple ${stateIndex}:
['${stateIndex}', 'some_other_path', '${stateIndex}']
- there can be usecases when actions can be shared because of identical states keeping in different locations. In this case there can be anonymus function passed as a first parameter:
@InjectStore((currentPath: string[]) => {
return currentPath.indexOf('search') >= 0
? ['entities', '${stateIndex}']
: ['${stateIndex}'];
})
InjectStore second parameter is initial state:
this is optional parameter and can add default state for that path
export const FooInitialState = {
loading: false,
entities: [],
};
Now you can inject state actions by marking component with @ComponentState decorator and inheriting from IComponentState interface.
Notice that statePath and stateIndex parameters are passed from todos
to todo-description
in order to use relative path in todo-description
state actions.
@ComponentState(TodosStateActions)
@Component({
selector: 'todos',
templateUrl: './todos.component.html',
changeDetection: ChangeDetectionStrategy.OnPush
})
export class TodosComponent implements IComponentStateActions<TodosStateActions> {
actions: TodosStateActions;
}
<tr *ngFor="let todo of actions.todos | async; let i = index;">
<th scope="row">{{ i + 1 }}</th>
<td>{{ todo.name }}</td>
<td>
<todo-description [statePath]="statePath" [stateIndex]="i"></todo-description>
</td>
<td><button (click)="deleteItem(i)">X</button></td>
</tr>
statePath and stateIndex properties are created in decorator and injected into Angular component to avoid boilerplate @Input's.
@ComponentState may take state actions object or anonymous function to select an object for creating instance:
@ComponentState(TodosStateActions)
OR
@ComponentState((component: TodosComponent) => {
return component.isFromCollection
? A_StateActions
: B_StateActions;
})
You can also inject the Store
service into your components and services. Use store.select
to
select slice(s) of state:
import { Store } from 'ng-state';
interface AppState {
counter: number;
}
@Component({
selector: 'my-app',
template: `
<div>todos: {{ (todos | async)?.getIn([0]) }}</div>
`
})
class MyAppComponent {
todos: Observable<number>;
constructor(private store: Store<AppState>){
this.todos = store.select(['todos']);
}
}
also you can avoid having async pipe by subscribing to state change. But then you will be responsible for subscription management. Hence it is recommended to leave this for Angular.
@Component({
selector: 'my-app',
template: `
<div>todos: {{ todos.getIn([0]) }}</div>
`
})
class MyAppComponent {
todos: Observable<number>;
counterSubscription: Rx.Subscription;
constructor(private store: Store<AppState>) implements OnDestroy{
this.counterSubscription = store.select(['todos'])
.subscribe(state => {
this.todos = state;
});
}
ngOnDestroy(){
this.counterSubscription.unsubscribe();
}
}
Dispatcher
There are cases when states of not related components, which has different places in state tree, should change e.g: when list item is selected filter should collapse. This is where dispatcher kicks in. Dispatcher is design to send and receive messages between components.
/* Child A */
export class UpdateMessage extends Message {
constructor(payload?: any) {
super('MessageName', payload);
}
}
dispatcher.subscribe(UpdateMessage, (payload: any) => {
this.actions.update....
});
/* Child B */
dispatcher.publish(new UpdateMessage('payload'));
Or, by using overload, even more simpler
/* Child A */
dispatcher.subscribe('UPDATE_MESSAGE', (payload: any) => {
this.actions.update....
});
/* Child B */
dispatcher.publish('UPDATE_MESSAGE', 'payload');
Time travel
@ng-state allows you to time travel. To enable this you have to add StateHistoryComponent to your app file
<state-history></state-history>
and from console run window.state.showHistory()
. While you in the time travel mode history is not collected. To exit mode run window.state.hideHistory()
command from the console.
You can also view current state in window.state.CURRENT_STATE
and whole history in window.state.HISTORY
. This allows you to debug or write your own time travel component if necessary.
History collecting can be disabled by passing false
to StoreModule.provideStore
second parameter.
By default 100 history steps are stored in memory but it can be modified by passing third parameter to StoreModule.provideStore
.
Here is basic flow with code side-by-side explained:
Contributing
Please read contributing guidelines here.