UNPKG

statefull

Version:

Subscribe to state changes.

231 lines (179 loc) 5.08 kB
function isObject(val) { return val != null && typeof val === "object" && Array.isArray(val) === false; } class State { state = null; handlers = []; toHandlers = {}; constructor(initState) { this.state = initState; this.getSnapshot = this.getSnapshot.bind(this); this.setState = this.setState.bind(this); this.getState = this.getState.bind(this); this.setValue = this.setValue.bind(this); this.getValue = this.getValue.bind(this); this.set = this.set.bind(this); this.get = this.get.bind(this); this._set = this._set.bind(this); this.on = this.on.bind(this); this.off = this.off.bind(this); this._getValue = this._getValue.bind(this); this.subscribe = this.subscribe.bind(this); this.subscribeTo = this.subscribeTo.bind(this); this.unsubscribe = this.unsubscribe.bind(this); this.unsubscribeFrom = this.unsubscribeFrom.bind(this); this.notify = this.notify.bind(this); } subscribe(handlerOrPath, handler) { if (typeof handlerOrPath === "string") { this.subscribeTo(handlerOrPath, handler); } else { this.handlers.push(handlerOrPath); } } _getValue(path, state) { const keyPath = path.split("."); let value = state for (let i = 0; i < keyPath.length; ++i) { const key = keyPath[i]; if(value === null || value === undefined || Array.isArray(value)) { return undefined; } value = value[key]; } return value; } getValue(path) { return this._getValue(path, this.getSnapshot()); } /* * @desc Set a value to the state (by mutation). * @param {Array<string>} keys * @param {any} value * @param {object} state * */ _set(keys, value, obj) { const lastKey = keys.length === 1; const [key, ...keyRest] = keys; const isNumber = (key) => Number.isInteger(Number(key)); const isArray = (v) => Array.isArray(v); if (lastKey) { if (isNumber(key)) { obj[key] = value; return obj } if (isArray(obj)) { return { [key]: value } } obj[key] = value; return obj; } const v = obj[key] if (isNumber(key)) { if (Array.isArray(obj)) { obj[Number(key)] = this._set(keyRest, value, v) return obj } } if (!isNumber(key)) { if (Array.isArray(v)) { return { ...obj, [key]: this._set(keyRest, value, v) } } if (!isObject(v)) { return { ...obj, [key]: this._set(keyRest, value, {}) } } if (isObject(v)) { return { ... obj, [key]: this._set(keyRest, value, v) } } } return obj; } set(pathOrValue, value) { return this.setState(pathOrValue, value); } /* * @param {string | undefined} path * */ get(path) { if (typeof path === "string") { return this.getValue(path) } return this.getState(); } /* * @param {string} path * @param {any} value * */ setValue(path, value) { return this._set(path.split("."), value, this.getState()); } subscribeTo(path, handler) { if (path in this.toHandlers) { this.toHandlers[path].push(handler); } else { this.toHandlers[path] = [handler]; } } on(path, handler) { this.subscribeTo(path, handler); } off(path, handler) { this.unsubscribeFrom(path, handler); } unsubscribeFrom(path, handler) { if (path in this.toHandlers) { this.toHandlers[path] = this.toHandlers[path].filter( (h) => h !== handler ); if (this.toHandlers[path].length === 0) { delete this.toHandlers[path]; } } } unsubscribe(handlerOrPath, handler) { if (typeof handlerOrPath === "string") { return this.unsubscribeFrom(handlerOrPath, handler) } if (typeof handlerOrPath === "function") { this.handlers = this.handlers.filter((h) => h !== handlerOrPath); } } getSnapshot() { return JSON.parse(JSON.stringify(this.state)); } getState(path) { if (typeof path === "string") { return this.getValue(path); } return this.getSnapshot(); } notify(newState, oldState) { this.handlers.forEach((h) => { if (!(typeof h === "function")) { return; } h(newState); }); Object.entries(this.toHandlers).forEach(([path, handlers]) => { const oldValue = this._getValue(path, oldState); const newValue = this._getValue(path, newState); if (newValue !== oldValue) { handlers.forEach((h) => h(newValue, newState)); } }); } setState(valueOrFunctionOrPath, value) { let newState = null; let oldState = this.getSnapshot(); if (typeof valueOrFunctionOrPath === "function") { newState = valueOrFunctionOrPath(oldState); } else if (typeof valueOrFunctionOrPath === "string") { newState = this.setValue(valueOrFunctionOrPath, value); } else { newState = valueOrFunctionOrPath; } this.state = newState; this.notify(newState, oldState); } } export default State;