Contribute to help us improve!
Are there edge cases or problems that we didn't consider? Is there a technical pitfall that we should add? Did we miss a comma in a sentence?
If you have any input for us, we would love to hear from you and appreciate every contribution. Our goal is to learn from projects for projects such that nobody has to reinvent the wheel.
Let's collect our experiences together to make room to explore the novel!
To contribute click on Contribute to this page on the toolbar.
Abstract Class Store
The following solution presents a base class for implementing stores which handle state and its transitions. Working with the base class achieves:
-
common API across all stores
-
logging (when activated in the constructor)
-
state transitions are asynchronous by design - sequential order problems are avoided
@Injectable()
export class ModalStore extends Store<ModalState> {
constructor() {
super({ isOpen: false }, !environment.production);
}
closeDialog() {
this.dispatchAction('Close Dialog', (currentState) => ({...currentState, isOpen: false}));
}
openDialog() {
this.dispatchAction('Open Dialog', (currentState) => ({...currentState, isOpen: true}));
}
}
import { OnDestroy } from '@angular/core';
import { BehaviorSubject } from 'rxjs/BehaviorSubject';
import { Observable } from 'rxjs/Observable';
import { intersection, difference } from 'lodash';
import { map, distinctUntilChanged, observeOn } from 'rxjs/operators';
import { Subject } from 'rxjs/Subject';
import { queue } from 'rxjs/scheduler/queue';
import { Subscription } from 'rxjs/Subscription';
interface Action<T> {
name: string;
actionFn: (state: T) => T;
}
/** Base class for implementing stores. */
export abstract class Store<T> implements OnDestroy {
private actionSubscription: Subscription;
private actionSource: Subject<Action<T>>;
private stateSource: BehaviorSubject<T>;
state$: Observable<T>;
/**
* Initializes a store with initial state and logging.
* @param initialState Initial state
* @param logChanges When true state transitions are logged to the console.
*/
constructor(initialState: T, public logChanges = false) {
this.stateSource = new BehaviorSubject<T>(initialState);
this.state$ = this.stateSource.asObservable();
this.actionSource = new Subject<Action<T>>();
this.actionSubscription = this.actionSource.pipe(observeOn(queue)).subscribe(action => {
const currentState = this.stateSource.getValue();
const nextState = action.actionFn(currentState);
if (this.logChanges) {
this.log(action.name, currentState, nextState);
}
this.stateSource.next(nextState);
});
}
/**
* Selects a property from the stores state.
* Will do distinctUntilChanged() and map() with the given selector.
* @param selector Selector function which selects the needed property from the state.
* @returns Observable of return type from selector function.
*/
select<TX>(selector: (state: T) => TX): Observable<TX> {
return this.state$.pipe(
map(selector),
distinctUntilChanged()
);
}
protected dispatchAction(name: string, action: (state: T) => T) {
this.actionSource.next({ name, actionFn: action });
}
private log(actionName: string, before: T, after: T) {
const result: { [key: string]: { from: any, to: any} } = {};
const sameProbs = intersection(Object.keys(after), Object.keys(before));
const newProbs = difference(Object.keys(after), Object.keys(before));
for (const prop of newProbs) {
result[prop] = { from: undefined, to: (<any>after)[prop] };
}
for (const prop of sameProbs) {
if ((<any>before)[prop] !== (<any>after)[prop]) {
result[prop] = { from: (<any>before)[prop], to: (<any>after)[prop] };
}
}
console.log(this.constructor.name, actionName, result);
}
ngOnDestroy() {
this.actionSubscription.unsubscribe();
}
}