fastlion-amis
Version:
一种MIS页面生成工具
642 lines (560 loc) • 17 kB
text/typescript
import {
types,
getEnv,
flow,
getRoot,
detach,
destroy,
isAlive,
Instance
} from 'mobx-state-tree';
import debounce from 'lodash/debounce';
import { ServiceStore } from './service';
import { FormItemStore, IFormItemStore, SFormItemStore } from './formItem';
import { Api, ApiObject, fetchOptions, Payload } from '../types';
import { ServerError } from '../utils/errors';
import {
getVariable,
setVariable,
deleteVariable,
cloneObject,
createObject,
difference,
guid,
isEmpty,
mapObject,
keyToPath,
isMobile
} from '../utils/helper';
import isEqual from 'lodash/isEqual';
import flatten from 'lodash/flatten';
import { getStoreById, removeStore } from './manager';
import { filter } from '../utils/tpl';
import { normalizeApiResponseData } from '../utils/api';
import { Divider } from 'antd';
export const FormStore = ServiceStore.named('FormStore')
.props({
inited: false,
validated: false,
submited: false,
submiting: false,
savedData: types.frozen(),
// items: types.optional(types.array(types.late(() => FormItemStore)), []),
canAccessSuperData: true,
persistData: types.optional(types.union(types.string, types.boolean), ''),
restError: types.optional(types.array(types.string), []) // 没有映射到表达项上的 errors
})
.views(self => {
function getItems() {
const formItems: Array<IFormItemStore> = [];
// 查找孩子节点中是 formItem 的表单项
const pool = self.children.concat();
while (pool.length) {
const current = pool.shift()!;
if (current.storeType === FormItemStore.name) {
formItems.push(current);
} else {
pool.push(...current.children);
}
}
return formItems;
}
return {
get loading() {
return self.saving || self.fetching;
},
get items() {
return getItems();
},
get errors() {
let errors: {
[propName: string]: Array<string>;
} = {};
getItems().forEach(item => {
if (!item.valid) {
errors[item.name] = Array.isArray(errors[item.name])
? errors[item.name].concat(item.errors)
: item.errors.concat();
}
});
return errors;
},
getValueByName(
name: string,
canAccessSuperData = self.canAccessSuperData
) {
return getVariable(self.data, name, canAccessSuperData);
},
getPristineValueByName(name: string) {
return getVariable(self.pristine, name);
},
getItemById(id: string) {
return getItems().find(item => item.itemId === id);
},
getItemByName(name: string) {
return getItems().find(item => item.name === name);
},
getItemsByName(name: string) {
return getItems().filter(item => item.name === name);
},
get valid() {
return (
getItems().every(item => item.valid) &&
(!self.restError || !self.restError.length)
);
},
get validating() {
return getItems().some(item => item.validating);
},
get isPristine() {
return isEqual(self.pristine, self.data);
},
get modified() {
if (self.savedData) {
return self.savedData !== self.data;
}
return !this.isPristine;
},
get persistKey() {
return `${location.pathname}/${self.path}/${typeof self.persistData === 'string'
? filter(self.persistData, self.data)
: self.persistData
}`;
}
};
})
.actions(self => {
function setValues(values: object, tag?: object, replace?: boolean) {
self.updateData(values, tag, replace);
// 如果数据域中有数据变化,就都reset一下,去掉之前残留的验证消息
self.items.forEach(item => item.reset());
// 同步 options
syncOptions();
}
function setValueByName(
name: string,
value: any,
isPristine: boolean = false,
force: boolean = false
) {
// 没有变化就不跑了。
const origin = getVariable(self.data, name, false);
const prev = self.data;
const data = cloneObject(self.data);
if (value !== origin) {
if (prev.__prev) {
// 基于之前的 __prev 改
const prevData = cloneObject(prev.__prev);
setVariable(prevData, name, origin);
Object.defineProperty(data, '__prev', {
value: prevData,
enumerable: false,
configurable: false,
writable: false
});
} else {
Object.defineProperty(data, '__prev', {
value: { ...prev },
enumerable: false,
configurable: false,
writable: false
});
}
} else if (!force) {
return;
}
setVariable(data, name, value);
if (isPristine) {
const pristine = cloneObject(self.pristine);
setVariable(pristine, name, value);
self.pristine = pristine;
}
if (!data.__pristine) {
Object.defineProperty(data, '__pristine', {
value: self.pristine,
enumerable: false,
configurable: false,
writable: false
});
}
self.data = data;
// 同步 options
syncOptions();
}
function deleteValueByName(name: string) {
const prev = self.data;
const data = cloneObject(self.data);
if (prev.__prev) {
// 基于之前的 __prev 改
const prevData = cloneObject(prev.__prev);
setVariable(prevData, name, getVariable(prev, name));
Object.defineProperty(data, '__prev', {
value: prevData,
enumerable: false,
configurable: false,
writable: false
});
} else {
Object.defineProperty(data, '__prev', {
value: { ...prev },
enumerable: false,
configurable: false,
writable: false
});
}
deleteVariable(data, name);
self.data = data;
}
function trimValues() {
let data = mapObject(self.data, (item: any) =>
typeof item === 'string' ? item.trim() : item
);
self.updateData(data);
}
const syncOptions = debounce(
() => self.items.forEach(item => item.syncOptions(undefined, self.data)),
250,
{
trailing: true,
leading: false
}
);
function setRestError(errors: string[]) {
self.restError.replace(errors);
}
function addRestError(msg: string | Array<string>) {
const msgs: Array<string> = Array.isArray(msg) ? msg : [msg];
msgs.forEach(msg => {
self.restError.push(msg);
});
}
function clearRestError() {
setRestError([]);
}
const saveRemote: (
api: Api,
data?: object,
options?: fetchOptions
) => Promise<any> = flow(function* saveRemote(
api: Api,
data: object,
options: fetchOptions = {}
) {
clearRestError();
try {
options = {
method: 'post', // 默认走 post
...options
};
if (options && options.beforeSend) {
let ret = options.beforeSend(data);
if (ret && ret.then) {
ret = yield ret;
}
if (ret === false) {
return;
}
}
self.markSaving(true);
const json: Payload = yield getEnv(self).fetcher(api, data, options);
// 失败也同样修改数据,如果有数据的话。
if (!isEmpty(json.data) || json.ok) {
self.updatedAt = Date.now();
setValues(
normalizeApiResponseData(json.data),
json.ok
? {
__saved: Date.now()
}
: undefined,
!!(api as ApiObject).replaceData
);
}
if (!json.ok) {
// 验证错误
if (json.status === 422 && json.errors) {
handleRemoteError(json.errors);
self.updateMessage(
json.msg ??
self.__(options && options.errorMessage) ??
self.__('Form.validateFailed'),
true
);
} else {
self.updateMessage(
json.msg ?? self.__(options && options.errorMessage),
true
);
}
throw new ServerError(self.msg, json);
} else {
updateSavedData();
if (options && options.onSuccess) {
const ret = options.onSuccess(json);
if (ret && ret.then) {
yield ret;
}
}
self.markSaving(false);
self.updateMessage(
json.msg ?? self.__(options && options.successMessage)
);
self.msg &&
getEnv(self).notify(
'success',
self.msg,
json.msgTimeout !== undefined
? {
closeButton: true,
timeout: json.msgTimeout
}
: undefined
);
return json.data;
}
} catch (e) {
self.markSaving(false);
if (!isAlive(self) || self.disposed) {
return;
}
if (e.type === 'ServerError') {
const result = (e as ServerError).response;
getEnv(self).notify(
'error',
e.message,
result.msgTimeout !== undefined
? {
closeButton: true,
timeout: result.msgTimeout
}
: undefined
);
} else {
getEnv(self).notify('error', e.message);
}
throw e;
}
});
function handleRemoteError(errors: { [propName: string]: string }) {
Object.keys(errors).forEach((key: string) => {
const item = self.getItemById(key);
const items = self.getItemsByName(key);
if (item) {
item.setError(errors[key]);
delete errors[key];
} else if (items.length) {
// 通过 name 直接找到的
items.forEach(item => item.setError(errors[key], 'remote'));
delete errors[key];
} else {
// 尝试通过path寻找
const items = getItemsByPath(key);
if (Array.isArray(items) && items.length) {
items.forEach(item => item.setError(`${errors[key]}`));
delete errors[key];
}
}
});
// 没有映射上的error信息加在msg后显示出来
!isEmpty(errors) &&
setRestError(Object.keys(errors).map(key => String(errors[key])));
}
const getItemsByPath = (key: string) => {
const paths = keyToPath(key);
const len = paths.length;
return paths.reduce(
(stores: any[], path, idx) => {
if (Array.isArray(stores) && stores.every(s => s.getItemsByName)) {
const items = flatten(
stores.map(s => s.getItemsByName(path))
).filter(i => i);
const subStores = items
.map(item => item?.getSubStore?.())
.filter(i => i);
return subStores.length && idx < len - 1 ? subStores : items;
}
return null;
},
[self]
);
};
const submit: (
fn?: (values: object) => Promise<any>,
hooks?: Array<() => Promise<any>>,
failedMessage?: string,
hideValidateFailedDetail?: boolean
) => Promise<any> = flow(function* submit(
fn: any,
hooks?: Array<() => Promise<any>>,
failedMessage?: string,
hideValidateFailedDetail?: boolean
) {
self.submited = true;
self.submiting = true;
try {
let valid = yield validate(hooks);
// 如果不是valid,而且有包含不是remote的报错的表单项时,不可提交
if (
(!valid &&
self.items.some(item =>
item.errorData.some(e => e.tag !== 'remote')
)) ||
self.restError.length
) {
let msg = failedMessage ?? self.__('Form.validateFailed');
const env = getEnv(self);
// 同时也列出所有表单项报错,方便在很长的表单中知道是哪个字段的问题
// 支持在env中配hideValidateFailedDetail来隐藏所有表单项报错
self.items.forEach(item => {
item.errorData.forEach(errorData => {
msg = `${msg}\n${errorData.msg}`;
});
});
// 移动端还没有做自动定位到必填
msg && env.notify('warning', msg);
// msg && !isMobile() && env.notify('warning', msg);
throw new Error(msg);
}
if (fn) {
const diff = difference(self.data, self.pristine);
const result = yield fn(
createObject(
createObject(self.data.__super, {
diff: diff,
__diff: diff,
pristine: self.pristine
}),
self.data
)
);
return result ?? self.data;
}
return self.data;
} finally {
self.submiting = false;
}
});
const validate: (
hooks?: Array<() => Promise<any>>,
forceValidate?: boolean
) => Promise<boolean> = flow(function* validate(
hooks?: Array<() => Promise<any>>,
forceValidate?: boolean
) {
self.validated = true;
const items = self.items.concat();
for (let i = 0, len = items.length; i < len; i++) {
let item = items[i] as IFormItemStore;
// 验证过,或者是 unique 的表单项,或者强制验证,或者有远端校验api
if (
!item.validated ||
item.unique ||
forceValidate ||
!!item.validateApi
) {
yield item.validate(self.data);
}
}
if (hooks && hooks.length) {
for (let i = 0, len = hooks.length; i < len; i++) {
yield hooks[i]();
}
}
return self.valid;
});
const validateFields: (fields: Array<string>) => Promise<boolean> = flow(
function* validateFields(fields: Array<string>) {
const items = self.items.concat();
let result: Array<boolean> = [];
for (let i = 0, len = items.length; i < len; i++) {
let item = items[i] as IFormItemStore;
if (~fields.indexOf(item.name)) {
result.push(yield item.validate(self.data));
}
}
return result.every(item => item);
}
);
function clearErrors() {
const items = self.items.concat();
items.forEach(item => item.reset());
}
function reset(cb?: (data: any) => void, resetData: boolean = true) {
if (resetData) {
self.data = self.pristine;
}
// 值可能变了,重新验证一次。
self.validated = false;
self.submited = false;
self.items.forEach(item => item.reset());
cb && cb(self.data);
}
function clear(cb?: (data: any) => void) {
const toClear: any = {};
self.items.forEach(item => {
if (item.name && item.type !== 'hidden') {
setVariable(toClear, item.name, item.resetValue);
}
});
setValues(toClear);
self.validated = false;
self.submited = false;
self.items.forEach(item => item.reset());
cb && cb(self.data);
}
function setCanAccessSuperData(value: boolean = true) {
self.canAccessSuperData = value;
}
function setInited(value: boolean) {
self.inited = value;
}
function setPersistData(value = '') {
self.persistData = value;
}
const setLocalPersistData = () => {
localStorage.setItem(self.persistKey, JSON.stringify(self.data));
};
function getLocalPersistData() {
let data = localStorage.getItem(self.persistKey);
if (data) {
self.updateData(JSON.parse(data));
}
}
function clearLocalPersistData() {
localStorage.removeItem(self.persistKey);
}
function updateSavedData() {
self.savedData = self.data;
}
return {
setInited,
setValues,
setValueByName,
trimValues,
submit,
validate,
validateFields,
clearErrors,
saveRemote,
reset,
syncOptions,
setCanAccessSuperData,
deleteValueByName,
getLocalPersistData,
setLocalPersistData,
clearLocalPersistData,
setPersistData,
clear,
updateSavedData,
handleRemoteError,
getItemsByPath,
setRestError,
addRestError,
clearRestError,
beforeDestroy() {
syncOptions.cancel();
}
};
});
export type IFormStore = Instance<typeof FormStore>;
export { IFormItemStore };