rxbox
Version:
state container for Angular
242 lines • 8.82 kB
JavaScript
import { Observable, BehaviorSubject } from 'rxjs';
import { distinctUntilChanged } from 'rxjs/operators';
import { cloneDeep, get, isEqual, assign } from 'lodash-es';
import { stringifyDate } from './stringifyDate';
import { createUUID } from './uuid';
const appState = {};
const store = new BehaviorSubject(appState);
const slotInStorage = '__rxbox';
export class RXBox {
constructor() {
this.lastChanges = null;
this.store = store;
this.saveToLocalStorage = false;
this.saveToSessionStorage = false;
// hold the history of the app state
// only work when debug is set to true
this.history = [];
this.subscribers = [];
// Observable that watch for any change in the store
this.changes = store.asObservable().pipe(distinctUntilChanged());
// ************************** API **************************
// change this for true when you want to push to history
this.debug = false;
if (RXBox.isWasRun && typeof window !== 'undefined') {
throw 'You can only create one instance of RXBox in your app';
}
RXBox.isWasRun = true;
if (typeof window !== 'undefined') {
((window) => {
window.RXBox = {
state: this.store,
history: this.history,
subscribers: this.subscribers
};
})(window);
}
}
static preventFunctionsInKey(obj) {
for (let i in obj) {
if (obj[i] !== null && typeof obj[i] === 'object') {
RXBox.preventFunctionsInKey(obj[i]);
}
if (typeof obj[i] === 'function') {
throw {
msg: "RXBox error -> can't store function inside RXBox store",
object: obj,
key: obj[i]
};
}
}
}
// push old state to history
pushHistory() {
// prevent save more then one version in the history when not
// running in debug mode
if (!this.debug && this.history.length) {
this.history.shift();
}
const state = this.getState();
this.history.push(state);
}
// show the history of the state
getHistory() {
return this.history;
}
// remove all state history
clearHistory() {
this.history = [];
}
// watch for key change in store (you can also use nested key
// like "key1.key2.key3")
/**
* @deprecated Since version 0.5.6. Will be deleted in future version
*/
watch(key, subscriberName, passByReference) {
return new Observable(observer => {
const uuid = createUUID();
this.subscribers[uuid] = {
name: subscriberName,
key
};
const sub = this.changes.subscribe(state => {
// watch for all change (no key specified)
if (typeof key === 'undefined') {
if (passByReference) {
observer.next(state);
}
else {
observer.next(cloneDeep(state));
}
return;
}
// if we inside this catch meaning that the key to watch
// is not inside the last change so we can return
// without response to the subscribers
const isKeyInLastChange = get(this.lastChanges, key);
if (typeof isKeyInLastChange === 'undefined')
return;
const newValue = get(state, key);
const oldState = this.history[this.history.length - 1];
const oldValue = get(oldState, key);
const equals = isEqual(newValue, oldValue);
if (!equals) {
const val = get(state, key);
if (passByReference) {
observer.next(val);
}
else {
observer.next(cloneDeep(val));
}
}
});
observer.add(() => {
sub.unsubscribe();
delete this.subscribers[uuid];
});
});
}
// watch for key change in store (you can also use nested key
// like "key1.key2.key3")
select(key, subscriberName, passByReference) {
return new Observable(observer => {
const uuid = createUUID();
this.subscribers[uuid] = {
name: subscriberName,
key
};
// get all keys (no key specified)
let skip = false;
const STATE = this.store.getValue();
let value;
if (typeof key === 'undefined') {
if (typeof STATE !== 'undefined') {
if (passByReference) {
observer.next(STATE);
}
else {
observer.next(cloneDeep(STATE));
}
skip = true;
}
}
else { // get specific key
value = get(STATE, key);
if (typeof value !== 'undefined') {
if (passByReference) {
observer.next(value);
}
else {
observer.next(cloneDeep(value));
}
skip = true;
}
}
const sub = this.changes.subscribe(state => {
// watch for all change (no key specified)
if (typeof key === 'undefined') {
if (skip && isEqual(STATE, state)) {
skip = false;
return;
}
if (passByReference) {
observer.next(state);
}
else {
observer.next(cloneDeep(state));
}
skip = false;
return;
}
// is not inside the last change so we can return
// without response to the subscribers
const isKeyInLastChange = get(this.lastChanges, key);
if (typeof isKeyInLastChange === 'undefined')
return;
const newValue = get(state, key);
const oldState = this.history[this.history.length - 1];
const oldValue = get(oldState, key);
const equals = isEqual(newValue, oldValue);
if (!equals) {
if (skip && isEqual(newValue, value)) {
skip = false;
return;
}
const val = get(state, key);
if (passByReference) {
observer.next(val);
}
else {
observer.next(cloneDeep(val));
}
skip = false;
}
});
observer.add(() => {
sub.unsubscribe();
delete this.subscribers[uuid];
});
});
}
// return current state
getState(passByReference) {
if (passByReference)
return this.store.value;
return cloneDeep(this.store.value);
}
// remove current state
clearState() {
this.store.next({});
}
// merge new keys to the current state
assignState(stateChanges) {
if (this.debug)
RXBox.preventFunctionsInKey(stateChanges);
const newState = assign({}, this.getState(), stateChanges);
this.pushHistory();
this.lastChanges = stateChanges;
this.store.next(newState);
if (this.saveToLocalStorage) {
localStorage.setItem(slotInStorage, stringifyDate.stringify(this.getState()));
}
if (this.saveToSessionStorage) {
sessionStorage.setItem(slotInStorage, stringifyDate.stringify(this.getState()));
}
}
assignStateAsync(stateChanges) {
return new Promise(resolve => {
setTimeout(() => {
this.assignState(stateChanges);
resolve();
});
});
}
getStoreFromSessionStorage() {
return stringifyDate.parse(sessionStorage.getItem(slotInStorage));
}
getStoreFromLocalStorage() {
return stringifyDate.parse(localStorage.getItem(slotInStorage));
}
}
RXBox.isWasRun = false;
//# sourceMappingURL=index.js.map