amis
Version:
一种MIS页面生成工具
553 lines (474 loc) • 20.4 kB
text/typescript
import {types, getParent, SnapshotIn, flow, getRoot} from 'mobx-state-tree';
import {IFormStore} from './form';
import {str2rules, validate as doValidate} from '../utils/validations';
import {Api, Payload, fetchOptions} from '../types';
import {ComboStore, IComboStore, IUniqueGroup} from './combo';
import {evalExpression} from '../utils/tpl';
import findIndex = require('lodash/findIndex');
import {isArrayChilrenModified, hasOwnProperty, isObject} from '../utils/helper';
import {flattenTree} from '../utils/helper';
import {IRendererStore} from '.';
import {normalizeOptions} from '../components/Select';
import find = require('lodash/find');
import {iRendererStore} from './iRenderer';
interface IOption {
value?: string | number | null;
label?: string | null;
children?: IOption[] | null;
disabled?: boolean | null;
visible?: boolean | null;
hidden?: boolean | null;
}
const ErrorDetail = types.model('ErrorDetail', {
msg: '',
tag: ''
});
export const FormItemStore = types
.model('FormItemStore', {
identifier: types.identifier,
type: '',
unique: false,
loading: false,
required: false,
rules: types.optional(types.frozen(), {}),
messages: types.optional(types.frozen(), {}),
errorData: types.optional(types.array(ErrorDetail), []),
name: types.string,
id: '', // 因为 name 可能会重名,所以加个 id 进来,如果有需要用来定位具体某一个
unsetValueOnInvisible: false,
validated: false,
validating: false,
multiple: false,
delimiter: ',',
valueField: 'value',
labelField: 'label',
joinValues: true,
extractValue: false,
options: types.optional(types.array(types.frozen()), []),
expressionsInOptions: false,
selectedOptions: types.optional(types.frozen(), []),
filteredOptions: types.optional(types.frozen(), [])
})
.views(self => {
function getForm(): any {
return getParent(self, 2);
}
function getValue(): any {
return getForm().getValueByName(self.name);
}
function getLastOptionValue(): any {
if (self.selectedOptions.length) {
return self.selectedOptions[self.selectedOptions.length - 1].value;
}
return '';
}
function getErrors(): Array<string> {
return self.errorData.map(item => item.msg);
}
// function selectedOptions(options:Array<Option>=(self.options as any).toJS()) {
// return value2array(getValue(), {
// multiple: self.multiple,
// delimiter: self.delimiter,
// valueField: self.valueField,
// options: options
// })
// }
// function filteredOptions(data:object):Array<IOption> {
// let options:Array<IOption> = self.options;
// options = options.filter(item => {
// let filtered = getExprProperties(item, data);
// return filtered.visible !== false && !filtered.hidden;
// });
// let parentStore = getForm().parentStore;
// if (parentStore && parentStore.storeType === ComboStore.name) {
// let combo = parentStore as IComboStore;
// let group = combo.uniques.get(self.name) as IUniqueGroup;
// let selectedOptions:Array<any> = [];
// group && group.items.forEach(item => {
// if (self !== item) {
// selectedOptions.push(...item.selectedOptions().map(item => item.value))
// }
// });
// if (selectedOptions.length) {
// options = options.filter(option => !~selectedOptions.indexOf(option.value))
// }
// }
// return options;
// }
return {
get form(): any {
return getForm();
},
get value(): any {
return getValue();
},
get prinstine(): any {
return (getParent(self, 2) as IFormStore).getPristineValueByName(self.name);
},
get errors() {
return getErrors();
},
get valid() {
const errors = getErrors();
return !!(!errors || !errors.length);
},
get lastSelectValue(): string {
return getLastOptionValue();
},
// selectedOptions,
// filteredOptions,
getSelectedOptions(value: any = getValue()) {
if (value === getValue()) {
return self.selectedOptions;
} else if (typeof value === 'undefined') {
return [];
}
const selected = Array.isArray(value)
? value.map(item =>
item && item.hasOwnProperty(self.valueField || 'value')
? item[self.valueField || 'value']
: item
)
: typeof value === 'string'
? value.split(self.delimiter || ',')
: [
value && value.hasOwnProperty(self.valueField || 'value')
? value[self.valueField || 'value']
: value
];
if (value && value.hasOwnProperty(self.labelField || 'label')) {
selected[0] = {
[self.labelField || 'label']: value[self.labelField || 'label'],
[self.valueField || 'value']: value[self.valueField || 'value']
};
}
const selectedOptions: Array<any> = [];
self.filteredOptions.forEach((item: any) => {
let idx = findIndex(selected, seleced => {
return isObject(seleced)
? seleced === item[self.valueField || 'value']
: String(item[self.valueField || 'value']) === String(seleced);
});
if (~idx) {
selected.splice(idx, 1);
selectedOptions.push(item);
}
});
selected.forEach((item, index) => {
let unMatched = (value && value[index]) || item;
if (unMatched && (typeof unMatched === 'string' || typeof unMatched === 'number')) {
unMatched = {
[self.valueField || 'value']: item,
[self.labelField || 'label']: item
};
}
unMatched && selectedOptions.push(unMatched);
});
return selectedOptions;
}
};
})
.actions(self => {
function config({
required,
unique,
value,
rules,
messages,
delimiter,
multiple,
valueField,
labelField,
joinValues,
extractValue,
type,
id
}: {
required?: any;
unique?: any;
value?: any;
rules?: string | {[propName: string]: any};
messages?: {[propName: string]: string};
multiple?: boolean;
delimiter?: string;
valueField?: string;
labelField?: string;
joinValues?: boolean;
extractValue?: boolean;
type?: string;
id?: string;
}) {
const form = self.form as IFormStore;
if (typeof rules === 'string') {
rules = str2rules(rules);
}
typeof type !== 'undefined' && (self.type = type);
typeof id !== 'undefined' && (self.id = id);
typeof messages !== 'undefined' && (self.messages = messages);
typeof required !== 'undefined' && (self.required = !!required);
typeof unique !== 'undefined' && (self.unique = !!unique);
typeof multiple !== 'undefined' && (self.multiple = !!multiple);
typeof joinValues !== 'undefined' && (self.joinValues = !!joinValues);
typeof extractValue !== 'undefined' && (self.extractValue = !!extractValue);
typeof delimiter !== 'undefined' && (self.delimiter = (delimiter as string) || ',');
typeof valueField !== 'undefined' && (self.valueField = (valueField as string) || 'value');
typeof labelField !== 'undefined' && (self.labelField = (labelField as string) || 'label');
if (self.required) {
rules = rules || {};
rules = {
...rules,
isRequired: true
};
}
rules && (self.rules = rules);
if (value !== void 0 && self.value === void 0) {
form.setValueByName(self.name, value, true);
}
}
function changeValue(value: any, isPrintine: boolean = false) {
if (typeof value === 'undefined' || value === '__undefined') {
self.form.deleteValueByName(self.name);
} else {
self.form.setValueByName(self.name, value, isPrintine);
}
}
const validate: (hook?: any) => Promise<boolean> = flow(function* validate(hook?: any) {
if (self.validating) {
return self.valid;
}
self.validating = true;
clearError();
if (hook) {
yield hook();
}
addError(doValidate(self.value, self.form.data, self.rules, self.messages));
self.validated = true;
if (self.unique && self.form.parentStore && self.form.parentStore.storeType === 'ComboStore') {
const combo = self.form.parentStore as IComboStore;
const group = combo.uniques.get(self.name) as IUniqueGroup;
if (group.items.some(item => item !== self && self.value && item.value === self.value)) {
addError(`当前值不唯一`);
}
}
self.validating = false;
return self.valid;
});
function setError(msg: string | Array<string>, tag: string = 'bultin') {
clearError();
addError(msg, tag);
}
function addError(msg: string | Array<string>, tag: string = 'bultin') {
const msgs: Array<string> = Array.isArray(msg) ? msg : [msg];
msgs.forEach(item =>
self.errorData.push({
msg: item,
tag: tag
})
);
}
function clearError(tag?: string) {
if (tag) {
const filtered = self.errorData.filter(item => item.tag !== tag);
self.errorData.replace(filtered);
} else {
self.errorData.clear();
}
}
function setOptions(options: Array<object>) {
if (!Array.isArray(options)) {
return;
}
options = options.filter(item => item);
const originOptions = self.options.concat();
options.length ? self.options.replace(options) : self.options.clear();
syncOptions(originOptions);
}
let loadCancel: Function | null = null;
const loadOptions: (
api: Api,
data?: object,
options?: fetchOptions,
clearValue?: boolean,
onChange?: (value: any) => void
) => Promise<any> = flow(function* getInitData(
api: string,
data: object,
options?: fetchOptions,
clearValue?: any,
onChange?: (value: any) => void
) {
try {
if (loadCancel) {
loadCancel();
loadCancel = null;
self.loading = false;
}
self.loading = true;
const json: Payload = yield (getRoot(self) as IRendererStore).fetcher(api, data, {
autoAppend: false,
cancelExecutor: (executor: Function) => (loadCancel = executor),
...options
});
loadCancel = null;
if (!json.ok) {
setError(`加载选项失败,原因:${json.msg || (options && options.errorMessage)}`);
(getRoot(self) as IRendererStore).notify('error', self.errors.join(''));
} else {
clearError();
self.validated = false; // 拉完数据应该需要再校验一下
let options: Array<IOption> =
json.data.options || json.data.items || json.data.rows || json.data || [];
options = normalizeOptions(options as any);
setOptions(options);
if (json.data && typeof (json.data as any).value !== 'undefined') {
onChange && onChange((json.data as any).value);
} else if (clearValue) {
self.selectedOptions.some((item: any) => item.__unmatched) && onChange && onChange('');
}
}
self.loading = false;
return json;
} catch (e) {
const root = getRoot(self) as IRendererStore;
if (root.storeType !== 'RendererStore') {
// 已经销毁了,不管这些数据了。
return;
}
self.loading = false;
if (root.isCancel(e)) {
return;
}
console.error(e.stack);
getRoot(self) && (getRoot(self) as IRendererStore).notify('error', e.message);
}
});
function syncOptions(originOptions?: Array<any>) {
if (!self.options.length && typeof self.value === 'undefined') {
self.selectedOptions = [];
self.filteredOptions = [];
return;
}
const form = self.form;
const value = self.value;
const selected = Array.isArray(value)
? value.map(item =>
item && item.hasOwnProperty(self.valueField || 'value') ? item[self.valueField || 'value'] : item
)
: typeof value === 'string'
? value.split(self.delimiter || ',')
: value === void 0
? []
: [
value && value.hasOwnProperty(self.valueField || 'value')
? value[self.valueField || 'value']
: value
];
if (value && value.hasOwnProperty(self.labelField || 'label')) {
selected[0] = {
[self.labelField || 'label']: value[self.labelField || 'label'],
[self.valueField || 'value']: value[self.valueField || 'value']
};
}
let expressionsInOptions = false;
let filteredOptions = self.options
.filter((item: any) => {
if (!expressionsInOptions && (item.visibleOn || item.hiddenOn)) {
expressionsInOptions = true;
}
return item.visibleOn
? evalExpression(item.visibleOn, form.data) !== false
: item.hiddenOn
? evalExpression(item.hiddenOn, form.data) !== true
: item.visible !== false || item.hidden !== true;
})
.map((item: any, index) => {
const disabled = evalExpression(item.disabledOn, form.data);
const newItem = item.disabledOn
? self.filteredOptions.length > index && self.filteredOptions[index].disabled === disabled
? self.filteredOptions[index]
: {
...item,
disabled: disabled
}
: item;
return newItem;
});
self.expressionsInOptions = expressionsInOptions;
const flattened: Array<any> = flattenTree(filteredOptions);
const selectedOptions: Array<any> = [];
selected.forEach((item, index) => {
let idx = findIndex(flattened, target => {
return isObject(item)
? item === target[self.valueField || 'value']
: String(target[self.valueField || 'value']) === String(item);
});
if (~idx) {
selectedOptions.push(flattened[idx]);
} else {
let unMatched = (value && value[index]) || item;
if (unMatched && (typeof unMatched === 'string' || typeof unMatched === 'number')) {
unMatched = {
[self.valueField || 'value']: item,
[self.labelField || 'label']: item,
__unmatched: true
};
const orgin: any =
originOptions &&
find(originOptions, target => String(target[self.valueField || 'value']) === String(item));
if (orgin) {
unMatched[self.labelField || 'label'] = orgin[self.labelField || 'label'];
}
}
unMatched && selectedOptions.push(unMatched);
}
});
let parentStore = form.parentStore;
if (parentStore && parentStore.storeType === ComboStore.name) {
let combo = parentStore as IComboStore;
let group = combo.uniques.get(self.name) as IUniqueGroup;
let options: Array<any> = [];
group &&
group.items.forEach(item => {
if (self !== item) {
options.push(...item.selectedOptions.map((item: any) => item && item.value));
}
});
if (filteredOptions.length) {
filteredOptions = filteredOptions.filter(option => !~options.indexOf(option.value));
}
}
isArrayChilrenModified(self.selectedOptions, selectedOptions) && (self.selectedOptions = selectedOptions);
isArrayChilrenModified(self.filteredOptions, filteredOptions) && (self.filteredOptions = filteredOptions);
}
function setLoading(value: boolean) {
self.loading = value;
}
let subStore: any;
function setSubStore(store: any) {
subStore = store;
}
function reset() {
self.validated = false;
if (subStore && subStore.storeType === 'ComboStore') {
const combo = subStore as IComboStore;
combo.forms.forEach(form => form.reset());
}
clearError();
}
return {
config,
changeValue,
validate,
setError,
addError,
clearError,
setOptions,
loadOptions,
syncOptions,
setLoading,
setSubStore,
reset
};
});
export type IFormItemStore = typeof FormItemStore.Type;
export type SFormItemStore = SnapshotIn<typeof FormItemStore>;