mobx-roof
Version:
Simple React data management by mobx.
149 lines (145 loc) • 4.41 kB
JavaScript
import { extendObservable, autorun, transaction, toJS, runInAction, action, isObservableArray } from 'mobx';
import { mapValues, each, isRegExp, toObservableObj } from '../common/utils';
import globalMiddleware from './globalMiddleware';
let count = 0;
export default class MobxModel {
static uuid = 0
constructor(initData = {}, middleware, autorunMap = {}, constants = {}) {
if (
this.constructor !== MobxModel &&
this.constructor.uuid === Object.getPrototypeOf(this.constructor).uuid
) {
throw new Error('[MobxModel] Can not immediately extend from MobxModel.');
}
this._actionStates = {};
this._middleware = middleware || globalMiddleware;
this._id = count ++;
Object.keys(initData).forEach((key) => {
if (constants[key] !== undefined) {
throw new Error(`[MobxModel] data key "${key}" is defined in constants`);
}
});
// check keys
this._dataKeys = Object.keys(initData).concat(Object.keys(constants));
this._checkDataKeys();
// add constants
const _constants = mapValues(constants, (value) => {
return {
enumerable: true,
configurable: true,
writable: false,
value,
};
});
// add observable keys
Object.defineProperties(this, _constants);
extendObservable(this, toObservableObj(initData));
// exec init before autorun
transaction(() => this.init(initData));
// add auto run key
each(autorunMap, (autorunFn) => {
autorun(autorunFn, this);
});
}
init(initData) {
this.set({
...initData,
});
}
getID() {
return this._id;
}
get dataKeys() {
return this._dataKeys;
}
set middleware(middleware) {
this._middleware = middleware;
}
get middleware() {
return this._middleware;
}
getActionState(actionName) {
if (!this[actionName]) throw new Error('[MobxModel] Undefined action: ', actionName);
if (!this._actionStates[actionName]) {
extendObservable(this._actionStates, { [actionName]: { loading: false, error: null } });
}
return this._actionStates[actionName];
}
toJS(key) {
function parse(val) {
if (val instanceof MobxModel) {
return val.toJS();
}
if (Array.isArray(val) || isObservableArray(val)) {
return val.map(item => parse(item));
} else if (isRegExp(val)) {
return val;
} else if (val && typeof val === 'object') {
return mapValues(val, (item) => parse(item));
}
return toJS(val);
}
if (key) return parse(this[key]);
return this._dataKeys.reduce((json, key) => {
json[key] = parse(this[key]);
return json;
}, {});
}
toJSON(key) {
return this.toJS(key);
}
stringify() {
return JSON.stringify(this.toJS());
}
each(fn) {
this._dataKeys.map((key) => {
fn(this[key], key, this);
});
}
toString() {
return this.constructor.name;
}
_checkDataKeys() {
this._dataKeys.forEach((dataKey) => {
if (this[dataKey]) {
throw new Error(`[MobxModel] Data key "${dataKey}" is defined in prototype methods.`);
}
});
}
set(key, val) {
if (typeof key === 'string') {
this[key] = val;
return this;
}
runInAction(() => mapValues(key, item => item, this));
return this;
}
_setActionState(actionName, val) {
this._actionStates[actionName] = val;
}
}
export function toMobxActions(actions) {
return mapValues(actions, (actionFn, actionName) => {
return function (...actionArgs) {
const actionContext = this;
// Ensure actionState
this.getActionState(actionName);
// 1. add loading state and save the pre error
this._setActionState(actionName, { loading: true, error: this._actionStates[actionName].error });
// 2. exec action with hooks
return this.middleware.execAction({ actionFn: action(actionFn), actionName, actionArgs, actionContext })
.then((payload) => {
// 3. loaded success
this._setActionState(actionName, { loading: false, error: null });
return payload;
}).catch((error) => {
// 4. loaded error
this._setActionState(actionName, { loading: false, error });
throw error;
});
};
});
}
export function isMobxModelClass(target) {
return target === MobxModel || target.prototype instanceof MobxModel;
}